AI makes PDF.
Teach any LLM to generate professional, PDF/A + PDF/UA compliant documents from natural language. Drop one skill file into your agent, point it at a fast REST API, and ship.
Add it to your agent in 30 seconds
The fastest path: paste the prompt below into your agent and it'll fetch the skill file itself. Want to install it locally? Two coding-agent-friendly options below.
Use this skill to author PDFs: https://makespdf.com/skills/pdf-template-author.md
Any coding agent
Download the file, then register it as a skill, rule, or instruction set in your agent of choice — Claude Code, Codex, OpenCode, Cursor, Windsurf, Copilot, etc.
curl -o pdf-template-author.md \
https://makespdf.com/skills/pdf-template-author.mdThe makesPDF CLI
Prints the skill to stdout — redirect to wherever your agent looks. Also gives the agent validate and preview commands.
npx @makespdf/cli skill > pdf-template-author.md
# or
npm i -g @makespdf/cli && makespdf loginHow it works
The agent writes a tiny JavaScript snippet using a high-level builder DSL — things like doc(), page(), table(), totals(). Our API renders it to a compliant PDF in sub-second wall-clock. No layout knowledge required from the model: the DSL encodes professional document patterns (invoices, receipts, CVs, reports) as composable functions.
curl -X POST https://makespdf.com/api/v1/preview \
-H "Authorization: Bearer $MAKESPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"dsl": "const template = doc({ size: \"A4\" }, page(col(s(\"Hello {{name}}\")))); const sampleData = { name: \"World\" };",
"data": { "name": "Ada" }
}' -o hello.pdfHeadless agents auth in one click
Agents that can't ship a baked-in API key use the OAuth 2.0 device authorization flow (RFC 8628). The agent prints a short code, the user opens a URL, approves once, and the agent receives a persistent key. Same flow as gh auth login — no secrets to manage, no loopback listener required. Full details in the API reference →
Which models can author a PDF?
Internal benchmark — each model gets the skill file as its system prompt, then runs the five test cases below. Up to 5 cycles per test, with structured catalog feedback fed back to the model between cycles.
Build a template from scratch
Given only the data, produce a working DSL template. The data uses non-generic field names (gstRate, gstAmount, NZD currency) the model must respect.
Show instructions + data
The invoice is for a New Zealand company (Solaris Energy) that installs solar panels. Include the GST number and use NZD currency. The data has a `gstRate` and `gstAmount` instead of generic "tax" fields — use those exact field names. The company has an `email` and `gst` field.
{
"invoiceNumber": "INV-7742",
"date": "31 March 2026",
"dueDate": "30 April 2026",
"company": {
"name": "Solaris Energy Ltd",
"address": "12 Harbour View, Level 3",
"city": "Auckland 1010, New Zealand",
"email": "billing@solaris.nz",
"gst": "GST 123-456-789"
},
"customer": {
"name": "Pacific Fresh Exports",
"address": "88 Wharf Road",
"city": "Tauranga 3110, New Zealand",
"email": "accounts@pacificfresh.co.nz"
},
"items": [
{
"description": "Solar panel installation (20kW system)",
"qty": 1,
"unitPrice": "18,500.00",
"amount": "18,500.00"
},
{
"description": "Inverter — SolarEdge SE7600H",
"qty": 3,
"unitPrice": "2,100.00",
"amount": "6,300.00"
},
{
"description": "Mounting hardware and cabling",
"qty": 1,
"unitPrice": "3,200.00",
"amount": "3,200.00"
},
{
"description": "Electrical certification",
"qty": 1,
"unitPrice": "450.00",
"amount": "450.00"
},
{
"description": "Project management",
"qty": 20,
"unitPrice": "95.00",
"amount": "1,900.00"
}
],
"subtotal": "30,350.00",
"gstRate": "15%",
"gstAmount": "4,552.50",
"total": "34,902.50",
"currency": "NZD",
"paymentTerms": "Net 30. Direct credit to Solaris Energy Ltd, ANZ Bank, Account 06-0123-0456789-00. Please reference INV-7742."
}Rewire an existing template
Given the working assets/samples/invoices/invoice-clean-dsl.js template plus data with a different schema (French TVA, EUR, renamed fields), update the template to fit.
Show schema diff
| Kind | Before | After |
|---|---|---|
| renamed | customer | client |
| renamed | customer.country | (merged into client.city) |
| renamed | taxRate | tvaRate |
| renamed | tax | tvaAmount |
| renamed | item.rate | item.unitPrice |
| added | — | currency (was hardcoded $, now EUR) |
| added | — | company.siret, company.tva |
| added | — | client.ustId |
| added | — | legalNotice (must render at bottom) |
| removed | notes | — |
Show instructions + data
The data structure has changed — the customer field is now called `client`, tax is now French TVA (`tvaRate`, `tvaAmount`), currency is EUR, and the company has SIRET and TVA identifiers. There is also a `legalNotice` field that should appear at the bottom.
{
"invoiceNumber": "FAC-2026-031",
"date": "31 March 2026",
"dueDate": "15 April 2026",
"company": {
"name": "Lumière Digital",
"address": "5 Place de la Bourse",
"city": "33000 Bordeaux, France",
"siret": "SIRET 123 456 789 00012",
"tva": "FR 32 123456789"
},
"client": {
"name": "Weinkeller Schmidt GmbH",
"address": "Römerberg 15",
"city": "60311 Frankfurt, Germany",
"ustId": "DE 987654321"
},
"items": [
{
"description": "Refonte site web e-commerce",
"qty": 1,
"unitPrice": "8,500.00",
"amount": "8,500.00"
},
{
"description": "Intégration passerelle de paiement",
"qty": 1,
"unitPrice": "2,200.00",
"amount": "2,200.00"
},
{
"description": "SEO — audit et optimisation",
"qty": 1,
"unitPrice": "1,500.00",
"amount": "1,500.00"
},
{
"description": "Formation CMS (2 sessions)",
"qty": 2,
"unitPrice": "400.00",
"amount": "800.00"
}
],
"subtotal": "13,000.00",
"tvaRate": "20%",
"tvaAmount": "2,600.00",
"total": "15,600.00",
"currency": "EUR",
"paymentTerms": "Virement bancaire sous 15 jours. IBAN: FR76 3000 6000 0112 3456 7890 189. BIC: AGRIFRPP. Mention: FAC-2026-031.",
"legalNotice": "TVA intracommunautaire FR 32 123456789. Pénalités de retard: 3x le taux d'intérêt légal."
}Multi-page invoice with footer
50 line items that span 3+ pages. Tests template:each, repeating page header, and a footer with thisPage() / totalPages() — totals must appear only on the final page.
Show instructions + data
This invoice has 50 line items, so the table will span multiple pages. Use a repeating page header (with the company name) and a footer that shows "Page X of Y" using `thisPage()` and `totalPages()`. Make sure the totals block appears only on the last page (after the table), not after every page.
{
"invoiceNumber": "INV-9001",
"date": "31 March 2026",
"dueDate": "30 April 2026",
"company": {
"name": "Northwind Consulting",
"address": "200 King St W, Suite 1500",
"city": "Toronto, ON M5H 3T4, Canada",
"email": "ar@northwind.example"
},
"customer": {
"name": "Globex Manufacturing",
"address": "1 Industrial Pkwy",
"city": "Hamilton, ON L8E 5K2, Canada"
},
"items": [
{
"description": "Consulting hours — Sprint 1, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 1, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 1, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 1, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 1, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 2, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 2, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 2, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 2, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 2, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 3, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 3, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 3, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 3, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 3, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 4, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 4, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 4, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 4, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 4, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 5, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 5, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 5, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 5, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 5, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 6, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 6, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 6, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 6, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 6, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 7, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 7, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 7, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 7, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 7, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 8, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 8, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 8, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 8, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 8, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 9, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 9, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 9, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 9, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 9, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 10, day 1",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 10, day 2",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 10, day 3",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 10, day 4",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
},
{
"description": "Consulting hours — Sprint 10, day 5",
"qty": 8,
"unitPrice": "175.00",
"amount": "1,400.00"
}
],
"subtotal": "70,000.00",
"taxRate": "13%",
"tax": "9,100.00",
"total": "79,100.00",
"currency": "CAD",
"paymentTerms": "Net 30. Wire transfer to RBC, Account 12345-678901."
}Generate a resume / CV
Distinguishes models that learned the DSL from ones that pattern-matched on invoices. Different structure: name + summary, then iterated experience with nested bullets, skills, education.
Show instructions + data
Generate a clean single-column resume (CV) — name + title at the top, contact line, summary paragraph, then sections for Experience, Skills, and Education. Use `template:each` to iterate experience entries and bullets. Section headings should be visually distinct (bold, slight underline or top border).
{
"name": "Avery Chen",
"title": "Senior Software Engineer",
"contact": {
"email": "avery.chen@example.com",
"phone": "+1 415 555 0148",
"location": "San Francisco, CA",
"linkedin": "linkedin.com/in/averychen"
},
"summary": "Backend engineer with 9 years building distributed systems. Led migration of a 4-region payments platform from monolith to event-driven services with zero downtime.",
"experience": [
{
"role": "Staff Engineer",
"company": "Lumen Payments",
"period": "2022 – Present",
"bullets": [
"Designed event-driven settlement pipeline processing $2B/month",
"Mentored team of 6 engineers, ran biweekly architecture reviews",
"Cut p99 latency 4x via per-shard read replicas"
]
},
{
"role": "Senior Engineer",
"company": "Helix Robotics",
"period": "2018 – 2022",
"bullets": [
"Built fleet telemetry ingest handling 50k events/sec",
"Owned on-call rotation and SLO instrumentation"
]
},
{
"role": "Software Engineer",
"company": "Pixelforge",
"period": "2015 – 2018",
"bullets": [
"Shipped real-time collaboration features for vector editor"
]
}
],
"skills": [
"Go",
"Rust",
"PostgreSQL",
"Kafka",
"Kubernetes",
"Terraform"
],
"education": [
{
"degree": "B.S. Computer Science",
"school": "University of Waterloo",
"period": "2011 – 2015"
}
]
}Repair a broken template
Three deliberate errors: invalid font-size value, wrong style property name (align vs text-align), and a table-row / column-definition mismatch. Tests debugging — the most common real-world skill use.
Show instructions + broken DSL
This template fails validation. There are three problems: 1. `"font-size": "huge"` — invalid value; must be a number (points). 2. `align` is not a valid style property name — the correct property is `text-align`. 3. The table row uses a 2-element array for cells, but the column definition has widths that need to match. Verify the row cells match the column definitions exactly. Fix all three problems and output the corrected DSL.
// This template is broken. Fix it.
const template = doc({ size: "A4" },
page(
header(s("Quarterly Report — {{quarter}}")),
col(
h1("Summary", { "font-size": "huge" }),
s("{{summary}}"),
gap(12),
h2("Metrics"),
table([["Metric", "60%"], ["Value", "40%", "right"]], "row in metrics", [
[s("{{row.name}}"), s("{{row.value}}", { align: "right" })],
]),
),
),
);
const sampleData = {
quarter: "Q1 2026",
summary: "Revenue up 18% YoY. Churn flat at 2.1%.",
metrics: [
{ name: "Revenue", value: "$4.2M" },
{ name: "Churn", value: "2.1%" },
],
};
Four-turn conversational edit chain
Closest test to the real /ai/chat use case: starts from a working invoice, fires four scripted plain-English edit requests, and verifies every prior intent still holds on the final template. Each turn gets up to five catalog-feedback cycles before moving on — same two-layer loop the chat applies via commit_dsl.
Show the four turns
- Make the totals row bold.
- Change the document font to NotoSans, size 10.
- Add a centered "Thank you for your business." line below the totals (small, italic).
- Add a 1pt bottom border under the table header row so it visually separates from the line items.
Columns
- Total time — sum of wall-clock time across every attempt in every test. The most direct measure of "how long would this model take on real work?". A self-correcting model that takes 5 cycles to pass shows 5× the cost of a model that converges on the first try; per-call latency can be inferred as Total ÷ (Cycles × 6).
- Out tokens — sum of output tokens across all attempts in the run.
- DSL — final-attempt count of tests where the model's DSL parsed and validated against the catalog without errors. This says nothing about whether the PDF actually rendered.
- Rendered — count of tests that produced a PDF end-to-end (valid DSL and the render pipeline succeeded). The real success metric: a 5/5 here is a model you can ship.
- Skill — first-attempt compliance score (0–1), mean of the static-check modules that apply to each generated template. Scoring only the first attempt isolates raw skill comprehension from self-correction (later cycles get structured catalog feedback). See the two modules below.
- Run cost — sum of input + output token cost across all attempts at published OpenRouter prices.
Column widths follow the skill's guidance
Numeric columns use auto instead of a fixed % width (so wide values don't wrap and drop), and at least one column uses 1fr / auto / auto-stretch to absorb surplus width when a description column is present.
Every variable reference resolves
Each {{...}} reference in the generated DSL points at a path that exists in the model's own sampleData — no broken bindings, typos, or made-up fields.
| Cycles | ||||||||
|---|---|---|---|---|---|---|---|---|
| claude-opus-4.7 | 27,709 | 6/6 | ⌀ 1.8 cyc · 100% clean | 6/6 | 0.99 | 3m 55s | $2.85 | |
| gpt-5-mini | 30,126 | 6/6 | ⌀ 1.7 cyc · 100% clean | 6/6 | 0.93 | 4m 14s | $0.12 | |
| gpt-5.4-nano | 33,241 | 6/6 | ⌀ 1.8 cyc · 100% clean | 6/6 | 0.99 | 4m 17s | $0.10 | |
| qwen3.5-122b-a10b | 30,604 | 6/6 | ⌀ 2.5 cyc · 100% clean | 6/6 | 0.99 | 5m 39s | $0.24 | |
| gpt-5.5 | 21,076 | 6/6 | ⌀ 1.8 cyc · 100% clean | 6/6 | 0.99 | 5m 45s | $2.04 | |
| grok-4-fast | 47,018 | 6/6 | ⌀ 1.8 cyc · 100% clean | 6/6 | 0.98 | 7m 58s | $0.08 | |
| deepseek-v4-flash | 27,682 | 6/6 | ⌀ 2.0 cyc · 100% clean | 6/6 | 0.99 | 8m 46s | $0.05 | |
| gpt-5.4 | 32,122 | 6/6 | ⌀ 1.7 cyc · 100% clean | 6/6 | 0.99 | 9m 28s | $1.13 | |
| claude-haiku-4.5 | 99,966 | 6/6 | ⌀ 2.0 cyc · 100% clean | 6/6 | 0.99 | 10m 30s | $0.85 | |
| gpt-5 | 36,121 | 6/6 | ⌀ 1.8 cyc · 100% clean | 6/6 | 1.00 | 12m 28s | $0.71 | |
| claude-sonnet-4.6 | 62,245 | 6/6 | ⌀ 1.7 cyc · 100% clean | 6/6 | 0.99 | 12m 58s | $1.83 | |
| gemini-2.5-pro | 119,230 | 6/6 | ⌀ 2.8 cyc · 100% clean | 6/6 | 0.99 | 18m 1s | $1.79 | |
| glm-4.6 | 28,540 | 6/6 | ⌀ 2.5 cyc · 100% clean | 6/6 | 0.99 | 28m 42s | $0.20 | |
| grok-4 | 72,744 | 6/6 | ⌀ 3.0 cyc · 100% clean | 6/6 | 1.00 | 42m 46s | $2.42 | |
| kimi-k2.6 | 126,422 | 6/6 | ⌀ 2.0 cyc · 100% clean | 6/6 | 1.00 | 85m 33s | $0.66 | |
| gpt-5.1-codex-mini | 76,441 | 5/6 | ⌀ 2.5 cyc · 83% clean | 6/6 | 1.00 | 8m 50s | $0.25 | |
| deepseek-v4-pro | 40,512 | 5/6 | ⌀ 2.5 cyc · 83% clean | 6/6 | 0.99 | 26m 36s | $0.20 | |
| glm-4.7 | 94,671 | 5/6 | ⌀ 3.0 cyc · 83% clean | 6/6 | 0.99 | 61m 3s | $0.34 | |
| grok-4.20 | 91,564 | 4/6 | ⌀ 2.8 cyc · 67% clean | 6/6 | 0.99 | 18m 14s | $0.75 | |
| qwen3.6-max-preview | 82,797 | 4/6 | ⌀ 2.8 cyc · 67% clean | 6/6 | 0.99 | 57m 34s | $0.98 | |
| mistral-medium-3-5 | 18,954 | 5/6 | ⌀ 2.5 cyc · 83% clean | 5/6 | 1.00 | 2m 15s | $0.73 | |
| gpt-5.4-mini | 45,728 | 5/6 | ⌀ 2.0 cyc · 83% clean | 5/6 | 0.99 | 6m 11s | $0.43 | |
| llama-4-maverick | 14,375 | 5/6 | ⌀ 2.0 cyc · 83% clean | 5/6 | 0.99 | 7m | $0.05 | |
| qwen3-32b | 63,960 | 3/6 | ⌀ 2.8 cyc · 50% clean | 4/6 | 0.93 | 26m 10s | $0.05 | |
| kimi-k2 | 14,289 | 3/6 | ⌀ 2.7 cyc · 50% clean | 3/6 | 1.00 | 6m 37s | $0.25 |
Refreshed periodically. Click a column header to sort, or a model row to see per-test status, errors, and download the rendered PDF / generated DSL. All models are routed through OpenRouter. Download as Markdown or JSON — leaderboard + per-model narrative + failing-DSL snippets (markdown only), sized to drop straight into an AI assistant.
Next steps
- Full agent setup walkthrough — per-tool instructions, validation, error handling
- API reference — every endpoint, every option
- Read the raw skill file — single source of truth for the DSL
- llms-full.txt — LLM-crawler manifest with the skill inlined