Okra create
Agent skills for OkraPDF — PDF extraction, document chat, structured data extraction
npx -y skills add okrapdf/skills --skill okra-createAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Generate designed PDFs via OkraPDF's pdf-render-agent — baked-design reports (magazine, editorial, report, poster), custom agent-written HTML covers via Cloudflare Browser Rendering, or fully composed output through codemode. Triggered by "create a pdf", "generate a report", "design a cover", "make a pdf with html", or any PDF-generation task where visual quality matters.
SKILL.md
9.4 KB, ~2.6k tokens by cl100k_base, as published. Nobody here has run it
OkraPDF — Create
One Worker, three modes to produce designed PDFs:
POST /render— baked design system from minimax-pdf; pick atype(report, magazine, editorial, poster…) and send content blocks. Container renders body in Python (reportlab), Browser Rendering renders the cover, Worker merges.POST /browser/html-to-pdf— agent writes raw HTML; Chromium returns a PDF. Pixel-perfect custom covers and one-pagers.POST /codemode/run— agent submits JS that composespdf.render+browser.htmlToPdf+pdf.mergein one request. Custom cover + baked body in a single call.
Architecture
Request → Worker
│
├─ DO Container (Python: palette → cover-html → body → merge)
└─ env.BROWSER (Chromium via @cloudflare/puppeteer)
- Python container (~520MB image) — reportlab + matplotlib + pypdf.
Handles
/tokens,/cover-html,/body,/mergeendpoints. Durable-Object-backed, warm after first request, sleeps after 5 min. - Cloudflare Browser Rendering — renders agent-authored HTML via
puppeteer.launch(env.BROWSER, { keep_alive: 600000 }). - Worker Loader (codemode) — sandboxed JS runs with injected host fns.
Mode 1: baked designs (POST /render)
One call in, one merged PDF out. Worker orchestrates all stages.
curl -X POST https://pdf-render-agent.steventsao.workers.dev/render \
-H 'content-type: application/json' \
--data @spec.json -o out.pdf
Request shape:
{
"title": "Q3 Review",
"type": "report", // or: proposal, resume, portfolio, academic,
// general, minimal, stripe, diagonal,
// frame, editorial, magazine, darkroom,
// terminal, poster
"author": "Strategy Team",
"date": "April 2026",
"accent": "#2D5F8A", // muted hex; avoid default blue
"cover_bg": "#1A1A1A", // optional
"abstract": "...", // magazine/darkroom only
"cover_image": "https://...jpg", // magazine/darkroom/poster
"skipCover": false, // true = body-only (~400ms warm)
"content": [ // minimax-pdf block types:
{ "type": "h1", "text": "Executive Summary" },
{ "type": "body", "text": "Justified paragraph with <b>markup</b>." },
{ "type": "callout", "text": "Highlighted insight." },
{ "type": "bullet", "text": "Point one." },
{ "type": "numbered", "text": "Counter resets automatically." },
{ "type": "table",
"headers": ["Col", "B", "C"],
"col_widths": [0.5, 0.25, 0.25], // fractions of usable width
"rows": [["a","1","2"]],
"caption": "..." },
{ "type": "chart", "chart_type": "bar",
"labels": ["Q1","Q2"], "datasets": [{"label":"Rev","values":[100,200]}] },
{ "type": "code", "text": "print('x')", "language": "python" },
{ "type": "math", "text": "E = mc^2", "label": "(1)" },
{ "type": "divider" }, { "type": "pagebreak" }, { "type": "spacer", "pt": 18 }
]
}
Perf (warm container):
skipCover: true→ ~400ms-2s (body only, via container alone)- full pipeline → ~6-8s (adds Browser Rendering cover + merge)
Mode 2: custom HTML cover (POST /browser/html-to-pdf)
Agent writes the HTML. Browser Rendering returns a PDF.
curl -X POST https://pdf-render-agent.steventsao.workers.dev/browser/html-to-pdf \
-H 'content-type: application/json' \
-d '{"html":"<!doctype html>...","width":794,"height":1123}' \
-o cover.pdf
Request:
{
"html": "<!doctype html>...",
"width": 794, // px, default 794 (A4 @ 96dpi)
"height": 1123, // px, default 1123
"waitForSelector": "#ready", // optional
"waitForTimeoutMs": 5000
}
Non-negotiable template rules (see templates/editorial-hero.html):
@page { size: 794px 1123px; margin: 0; }— locks PDF page dims.html, body { width: 794px; height: 1123px; overflow: hidden; }— viewport anchor.- Use CSS Grid rows (no absolute positioning) to guarantee no overlaps.
grid-template-columns: minmax(0, 1fr) 220px;— theminmax(0, …)prevents content from expanding a flexible column past its share.overflow: hiddenon the page container — last-line defense against bleeds.@importGoogle Fonts at the top; Worker useswaitUntil: "domcontentloaded"(notnetworkidle0— fonts can stall that).
See templates/editorial-hero.html for a working magazine-cover reference.
Mode 3: agent composition via codemode (POST /codemode/run)
Agent submits JS. Worker injects host fns: pdf.render, pdf.merge,
browser.htmlToPdf. Sandbox has globalOutbound: null — only the host fns
are reachable.
// user.js (the string you send as body.code)
export default async function ({ pdf, browser }) {
const cover = await browser.htmlToPdf({ html: MY_COVER_HTML });
const body = await pdf.render({ ...spec, skipCover: true });
const out = await pdf.merge([cover.pdfBase64, body.pdfBase64]);
return { pdfBase64: out.pdfBase64, size: out.size };
}
curl -X POST https://pdf-render-agent.steventsao.workers.dev/codemode/run \
-H 'content-type: application/json' \
-d '{"code":"...","returnPdf":true}' -o final.pdf
Build the request from Python/Node with JSON.stringify to embed HTML
and spec safely:
req = {
"code": f"""
const COVER_HTML = {json.dumps(cover_html)};
const BODY_SPEC = {json.dumps(body_spec)};
export default async ({{ pdf, browser }}) => {{
const cover = await browser.htmlToPdf({{ html: COVER_HTML }});
const body = await pdf.render(BODY_SPEC);
const out = await pdf.merge([cover.pdfBase64, body.pdfBase64]);
return {{ pdfBase64: out.pdfBase64, size: out.size }};
}};
""",
"returnPdf": True
}
Accent color — pick from content, not a palette
The minimax-pdf design system rule: don't default to blue. Muted, desaturated, pulled from the document's industry.
| Context | Suggested hex |
|---|---|
| Legal / compliance / finance | #1C3A5E, #2E3440 |
| Healthcare | #2A6B5A, #3A7D6A |
| Tech / engineering | #2D5F8A, #3D4F8A |
| Sustainability | #2E5E3A, #4A5E2A |
| Creative / arts | #6B2A35, #5A2A6B, #8A3A2A |
| Academic / research | #2A5A6B, #2A4A6B |
| Premium / luxury | #1A1208, #4A3820 |
Gotchas learned the hard way
Browser Rendering hangs on first call.
- Cause: missing
keep_aliveonpuppeteer.launch()orwaitUntil: "networkidle0"with@imported fonts. - Fix (already in Worker):
puppeteer.launch(env.BROWSER, { keep_alive: 600000 })+setContent(html, { waitUntil: "domcontentloaded" }).
Content clips on the right edge of the PDF.
- Cause: Worker didn't set viewport before
setContent; Chrome used its default viewport, layout math didn't match PDF size. - Fix (in Worker):
page.setViewport({ width: 794, height: 1123 })beforesetContent;page.pdf({ width: "794px", height: "1123px" }).
Table cells wrap digits like 90.74 → 90.7 / 4.
- Cause:
col_widthsfraction too small for the numeric precision. - Fix: give numeric columns ≥ 0.10; data fits at 0.11-0.12 for 4-char decimals.
"specialized" wraps as "specializ/ed" in table rows.
- Cause: Category col < 0.13 of usable width.
- Fix: either widen to 0.13+ or abbreviate to "spec" / "VLM".
Reportlab justification stretches short paragraphs.
- Every
bodyblock is full-justified. For closing 1-2 sentence paragraphs, fold into the previous body block or accept the rivering.
Trailing caption block orphans to a nearly-empty last page.
- Drop the trailing caption or inline it in the last body paragraph.
Worker endpoints at a glance
GET /health container liveness
GET /browser/ping launch+close diagnostic (~3.5s)
GET /browser/limits puppeteer.limits(env.BROWSER)
POST /render body: RenderSpec -> application/pdf
POST /browser/html-to-pdf body: {html, width?...} -> application/pdf
POST /codemode/run body: {code, returnPdf?} -> json | application/pdf
Deploy your own Worker
The reference impl lives in okra meta-repo at
apps/api/packages/pdf-render-agent/. Wrangler config requires:
"containers": [{ "class_name": "PdfRender", "image": "./Dockerfile",
"max_instances": 5, "instance_type": "standard-1" }],
"durable_objects": { "bindings": [{ "name": "PDF_RENDER", "class_name": "PdfRender" }] },
"worker_loaders": [{ "binding": "LOADER" }],
"browser": { "binding": "BROWSER" }
Needs: Workers Paid (Containers + Browser Rendering both require it). First deploy pushes the ~520MB container image to CF's registry.
Template library
templates/editorial-hero.html— magazine cover with big display number, side attribution, title + lede + TOC + footer. CSS Grid rows, all safety rails in place. Works as-is viaPOST /browser/html-to-pdf. Swap the content between<body>tags or parametrize later with a builder function.