80 operations, one grammar

Every operation is one line in a workflow file — the same declarative shape from merge to redaction to form filling. Each entry below links its usage and a ready-to-paste example. (Agents: this reference is also available as plain Markdown.)

Pages & layout

merge

Concatenates every file in the workflow's inputs (in order) into one PDF. List 2+ inputs; no params.

- merge: {}

split

Splits the working PDF into several output files — by explicit ranges or every N pages.

- split: { ranges: ["1-1", "2-12", "13-40"] }

split_invoices

Splits a PDF that concatenates many invoices/receipts into one PDF per invoice. Set output.folder (or the op's to param) as the destination. Detects boundaries from the page text ("Page 1 of N" resets, invoice-number changes, header keywords) and names each file from the detected invoice number/date/vendor via name. Scans: add ocr_first: true. Always writes a _manifest.csv (page ranges + metadata) so you can verify the split. Escape hatch: pass explicit starts (1-based page numbers).

- split_invoices: { name: "{invoice_number}" }

delete_pages

Removes the listed 1-based pages from the working PDF.

- delete_pages: { pages: [2, 7, 12] }

reorder_pages

Reorders pages to the given 1-based permutation (every page appears once).

- reorder_pages: { order: [1, 4, 2, 3, 5] }

rotate_pages

Rotates pages clockwise by a multiple of 90°. Omit pages to rotate all.

- rotate_pages: { pages: [3, 4], degrees: 90 }

flip_pages

Mirrors pages — a true flip, not a rotation. direction: horizontal (default) mirrors left↔right; vertical mirrors top↕bottom. Omit pages to flip the whole document. Useful for scans fed through a duplex feeder backwards, or for iron-on/transfer prints.

- flip_pages: { direction: horizontal, pages: [2, 4] }

insert_blank

Inserts a blank page before the 1-based position at. size copies a neighbor by default.

- insert_blank: { at: 2, size: Letter }

extract_pages

Keeps only the listed pages (drops the rest), in the order given.

- extract_pages: { pages: [1, 8, 9] }

crop

Crops pages to a rectangle [x, y, width, height] in points (origin bottom-left). Omit pages for all.

- crop: { box: [72, 500, 468, 240], pages: [4] }

scale_pages

Resizes pages to a named size (A4/Letter/Legal) or by a factor, scaling the content to fit.

- scale_pages: { size: A4 }

n_up

Places cols×rows source pages onto each output page (e.g. 2×1 = two-up handouts).

- n_up: { cols: 2, rows: 2 }

booklet

Imposes pages 2-up in saddle-stitch order for folded printing (pads to a multiple of 4). No params.

- booklet: {}

poster

Splits each page into a rows×cols grid of separate pages (in reading order). Use a large grid for poster printing, or **rows: 1, cols: 2** to halve a 2-up book scan back into single pages.

- poster: { rows: 1, cols: 2 }

single_page

Combines every page into one tall single page, stacked top-to-bottom. Optional gap adds blank space (points) between pages.

- single_page: { gap: 12 }

Stamps & overlays

watermark

Stamps a diagonal text (or image) watermark across pages. Image comes from a declared asset.

- watermark: { text: CONFIDENTIAL, opacity: 0.12, rotate: 45 }

stamp

Places positioned text or an image at (x, y) in points from bottom-left (not diagonal — that's watermark).

- stamp: { image: assets/paid-stamp.png, x: 400, y: 640, width: 120, opacity: 0.9, pages: [1] }

annotate

Adds a list of annotations to the PDF — free text boxes, text highlights (by find text or rect), sticky notes, shapes (rect/line/ellipse) and images. Each item names a type, a 1-based page, and a rect [x,y,w,h] or point at [x,y] in points, origin BOTTOM-LEFT (like crop/stamp). Colors are "#RRGGBB" or [r,g,b]. Invalid items are skipped.

- annotate: { annotations: [{ type: highlight, page: 1, find: "DRAFT", color: "#ffcc00" }] }

add_page_numbers

Stamps page numbers on every page. format uses {n} and {total}; position places them.

- add_page_numbers: { format: "Page {n} of {total}", position: bottom-right, start: 1, size: 9 }

overlay

Overlays another PDF's pages on top of the current document. over is a declared asset PDF.

- overlay: { over: assets/void-specimen-overlay.pdf, opacity: 0.35 }

Metadata, bookmarks & tables

set_metadata

Sets document metadata. Provide any of title/author/subject/keywords/creator/producer.

- set_metadata: { title: "2024 Annual Report", author: "Acme Corporation", subject: "Fiscal Year 2024 Results", keywords: ["annual report", "finance", "FY2024"] }

set_bookmarks

Replaces the outline with a given list of { level, title, page } entries (page is 1-based).

- set_bookmarks: { bookmarks: [{ level: 1, title: Introduction, page: 1 }, { level: 1, title: Installation, page: 4 }, { level: 2, title: "System Requirements", page: 5 }, { level: 1, title: Troubleshooting, page: 12 }] }

extract_bookmarks

Exports the outline/bookmarks to a JSON file (to). Does not change the PDF.

- extract_bookmarks: { to: output/textbook-outline.json }

extract_fields

Exports form fields (page/name/type/value) to a CSV file (to). Does not change the PDF.

- extract_fields: { to: output/w9-fields.csv }

extract_tables

Detects tables and writes each to a CSV under the to directory. Does not change the PDF.

- extract_tables: { to: output/statement-tables }

pdf_info

Writes a read-only report of the PDF — page count, per-page size/rotation, metadata, encryption, fonts, image/field/annotation counts — as JSON to to. Leaves the PDF unchanged.

- pdf_info: { to: output/report-info.json }

compare_pdfs

Compares the working document with a second PDF (against, a workflow-relative path) and writes a diff report + highlighted images to output.folder. Pages are ALIGNED first (by text), so an inserted/deleted page is reported as added/removed instead of shifting every later page. Writes _report.md (per-page unified text diff), _summary.json (the change set as data), and one change_*.png per changed/added/removed page. mode: text skips images; dpi sets image resolution. Add side_by_side: true for one shareable diff.pdf (each changed page shown base-vs-against with the changed side highlighted). tolerance (0-255) ignores anti-aliasing noise in the visual fallback.

- compare_pdfs: { against: input/contract-v2.pdf, side_by_side: true }

set_view_preferences

Controls how the PDF OPENS in a reader (not its content): page_layout (e.g. TwoPageLeft for a book spread), page_mode (FullScreen for a kiosk/presentation, UseOutlines to reveal bookmarks), open_page + zoom ("fit"/"fit-width"/a percent), and window chrome (hide_toolbar/menubar, fit_window, center_window, display_doc_title).

- set_view_preferences: { page_mode: FullScreen, page_layout: SinglePage, open_page: 1, zoom: fit }

Text, image & Markdown extraction

extract_text

Extracts the PDF's text to a sidecar file (to). Does not change the PDF. Add clean: true to tidy the text: drop running headers/footers, rejoin hyphen-split words, and reflow hard-wrapped lines back into paragraphs (each pass can also be toggled on its own). page_markers: true labels each page.

- extract_text: { to: output/lease-text.txt, clean: true }

extract_markdown

Extracts page content as Markdown (tables included) to to. engine: auto/pymupdf4llm/markitdown, or marker (Surya OCR + layout) for scanned books — best quality, but slow and needs the marker-pdf backend. remote ("user@host") offloads Marker to a GPU box over SSH — minutes on a 4090 vs hours on a CPU; requires the pdfStudio.allowRemoteRender setting.

- extract_markdown: { to: output/annual-report.md, engine: pymupdf4llm }

extract_images

Extracts every embedded image to files under the to directory. Does not change the PDF.

- extract_images: { to: output/catalog-images }

render_pages

Renders each page to a raster image (PNG/JPG) under to at dpi. Different from extract_images (embedded images). Set format: svg for vector output (infinite zoom, tiny files, no DPI needed).

- render_pages: { to: output/deck-thumbnails, dpi: 200, format: png }

replace_image

Replaces an embedded image. selector picks the target ({ page, object_name }); image is the replacement path.

- replace_image: { selector: { page: 1, object_name: Im0 }, image: assets/new-logo.png }

replace_text

Finds literal text and replaces it IN PLACE — the workflow-shaped answer to "edit the PDF": re-date a template, fix a recurring typo, swap an entity name across a whole folder (put a glob in inputs and it batches). The matched text is truly deleted, and the replacement lands on the original baseline in the original size and color. Matching is per LINE (text that wraps won't match) and literal (not regex); ignore_case / whole_word refine it, pages restricts it, and replace: "" deletes the match. **Font honesty:** an embedded, subsetted original font can't render NEW glyphs, so the replacement uses the closest base-14 font (serif→Times, mono→Courier, else Helvetica, keeping bold/italic) — use it for utility edits, not typography-preserving ones. Run preview: true first for a report of every match with nothing changed.

- replace_text: { find: "ACME Corp", replace: "Initech LLC" }

Redaction & cleanup

redact

Permanently removes content in rectangles on a page. rects are [x, y, w, h] in points. Add rasterize: true to also flatten the WHOLE document to an image-only PDF afterward, so nothing hidden (text layer, metadata, off-page content) can survive — the safest way to share.

- redact: { page: 2, rects: [[72, 688, 250, 26], [72, 650, 210, 26]], rasterize: true }

auto_redact

Finds and permanently redacts matches across all pages. Match by literal text (a list), by named PII patterns (ssn, email, phone, credit_card, ein, ipv4, iban), and/or by custom regex. Literal matching is case-sensitive by default — add ignore_case: true, or whole_word: true so "Ann" doesn't hit "Anniversary". The run note reports any rule that found nothing, so verify. Add rasterize: true to also flatten the document to an image-only PDF so nothing hidden (text layer, metadata, off-page content) survives — the safest way to share. **Preview first (recommended for redaction):** set preview: true for a dry run — it writes output/redaction-preview.md listing every match (page + matched text + which rule) and applies NOTHING. Review it, then remove preview to apply for real. **Prompt example** — you don't have to write the YAML; just tell your coding agent in plain English, e.g.: > "Create a workflow to redact my name (Jane Whitfield), account number 123456789, my email and any SSNs from statement.pdf, and give me a version that's safe to share." The agent then creates a .opw.yaml with auto_redact — your EXACT values in text, patterns: [email, ssn] for the shape-based ones, rasterize: true (because "safe to share" = flatten to an image), output.file at a new path — runs opw_validate, and renders it.

- auto_redact: { text: ["Jane Whitfield", "123456789"], patterns: ["email", "ssn"], ignore_case: true, rasterize: true }

sanitize

Strips JavaScript, embedded files, metadata, and links from the PDF. No params.

- sanitize: {}

remove_annotations

Removes all annotations (comments, highlights, link markup). No params.

- remove_annotations: {}

remove_images

Removes all images from the document. No params.

- remove_images: {}

remove_blank_pages

Detects and deletes blank pages (no text, no images, near-white). No params.

- remove_blank_pages: {}

ocr

Adds a searchable text layer to scanned pages via OCR. language is a Tesseract code (default eng).

- ocr: { language: "eng+fra" }

extract_js

Surfaces any JavaScript embedded in the PDF (document-level Names tree and the open action) to a Markdown report at to. Read-only — it does not change the PDF. To strip JavaScript, use sanitize.

- extract_js: { to: output/javascript.md }

Forms

create_form

Turns a template into a FILLABLE PDF. Type a marker where each field belongs, run a conversion op (office_to_pdf / markdown_to_pdf / html_to_pdf) to render the layout, and create_form finds each marker, DELETES it, and puts a real AcroForm field in its place — so it works with any source that leaves a text layer. **The easy way: use the TYPE as the tag** — [[text]], [[check]], [[date]], [[sign]] — as many times as you like; they're numbered in reading order (text_01, checkbox_01, checkbox_02…), so there's nothing to name and no config file. Fields fill their table cell automatically and get a visible border. Use a NAMED tag ([[employee_name]]) when you want a meaningful field name; add a fields_file YAML keyed by tag for types/tooltips/choices/widths. Either way the tag key BECOMES the PDF field name, so you can fill it with fill_form: { fields: { text_01: "Jane" } } and read it back with extract_form — no form pack needed. **Keep tags short:** a marker too long for its column wraps or gets clipped by the renderer, and create_form then FAILS the run rather than ship a form with a missing field and a mangled [[emplo on the page (in a narrow cell, set the marker's font small in Word — it's deleted anyway). preview: true dry-runs it; debug: true writes a copy with every field outlined.

- create_form: { debug: true }

fill_form

Fills a known PDF form from your records — no need to know the raw field names. form is a supported id (ds11, f1040, … — see the form_list MCP tool or the PDF Fill catalog); people points at your people.yaml and person chooses whose info fills the primary role. The form pack maps friendly keys → the form's real fields, handling checkboxes, radio groups, split SSN/date boxes, and dropdowns. roles binds relatives (spouse/parents) for forms that need them; values sets form-specific fields (e.g. applying_for). signature: { image, field } stamps a signature and flatten: true bakes a locked, print-ready final. **Run once with preview: true** for a dry-run report of exactly what will be filled. (Advanced: omit form and pass raw fields: { RealFieldName: value } to fill any AcroForm directly.)

- fill_form: { form: ds11, people: people.yaml, person: me }

extract_form

Reads filled forms back OUT to data — the inverse of fill_form. Because a form pack maps friendly keys to real fields, the same pack that fills a form reads one back, so every supported form is extractable with no extra setup. Point inputs at one PDF or a whole folder (forms/*.pdf — all matches fold into ONE table, they aren't batched). Each PDF is auto-identified by its field signature (pass form: to force one). Writes per-form <stem>.json, a combined forms.json, and **one CSV per form type** (w9.csv, f1040.csv — different forms have unrelated schemas, so they never share a table) into to; format: json/csv narrows it. A CSV's columns come from the form pack, in the form's own field order, so the header is stable across runs no matter which files are present or what a form left blank. **Re-runs are incremental**: forms.json doubles as a ledger keyed by content hash, so running again after dropping new files in extracts only what's new or changed and merges it into the table (resume: false re-reads everything). A PDF that matches NO pack — anything you built with create_form — is read **raw**: field name → value, into raw.csv. That's what closes the loop (template → fillable → filled → CSV) with nothing to author. Note a flattened PDF has no fields left to read.

- extract_form: { to: output/extracted }

fill_field

Sets an AcroForm field's value. field is the field name, value the text to set.

- fill_field: { field: "policyholder_name", value: "Marcus Webb" }

flatten

Bakes form fields + annotations into static page content (no longer editable). No params.

- flatten: {}

unlock_forms

Clears the read-only flag on form fields so they can be filled. No params.

- unlock_forms: {}

Attachments

extract_attachments

Extracts embedded file attachments to the to directory. Does not change the PDF.

- extract_attachments: { to: output/invoice-attachments }

add_attachments

Embeds a file as an attachment. file is a workflow-relative path; name defaults to its basename.

- add_attachments: { file: assets/line-items-2024.xlsx, name: line-items-2024.xlsx }

Encryption & permissions

encrypt

Password-protects the PDF (AES-256). Set user_password (to open) and/or owner_password. Put it last. Use ${ENV} to keep secrets out of the file.

- encrypt: { user_password: "${PDF_OPEN_PASSWORD}", owner_password: "${PDF_OWNER_PASSWORD}" }

decrypt

Removes password protection — supply the current password. Put it first so later ops can read the PDF.

- decrypt: { password: "${PDF_PASSWORD}" }

set_permissions

Restricts what viewers can do via an owner_password. allow toggles print/copy/modify/annotate/fill_form/assemble.

- set_permissions: { owner_password: "${PDF_OWNER_PASSWORD}", allow: { print: true, copy: false, modify: false, annotate: false } }

Optimize, repair & archival

compress

Reduces file size; deep image compression kicks in when Ghostscript/qpdf are available. quality 1-100.

- compress: { quality: 60 }

linearize

"Fast web view" — reorders the PDF for progressive loading. Put it last. No params.

- linearize: {}

repair

Repairs / rewrites a structurally-messy PDF. No params.

- repair: {}

decompress

Uncompresses content streams to produce a readable/debuggable PDF. No params.

- decompress: {}

rasterize

Flattens every page to a raster image (non-editable, non-selectable output) at dpi.

- rasterize: { dpi: 200 }

recolor

Recolors every page for comfortable reading. **mode: dark** (default) is a smart **dark mode** — it flips text and background to light-on-dark, but leaves embedded photos and logos looking normal (not a negative). mode: invert is a raw negative of everything; mode: grayscale drops color. Rasterizes at dpi, so the text becomes an image.

- recolor: { mode: dark }

scanner_effect

Makes a clean, born-digital PDF look like it was scanned — a slight page skew (alternating per page), softening, and grain. Rasterizes each page. Tune skew, noise, grayscale, dpi.

- scanner_effect: { grayscale: true, skew: 0.8 }

pdf_to_pdfa

Converts to PDF/A for long-term archival. No params.

- pdf_to_pdfa: {}

Convert to PDF

images_to_pdf

Builds a PDF from image inputs — set the workflow's inputs to image files (one page each). PNG/JPEG work with the bundled engine; PNG/JPEG/WEBP/TIFF/GIF/BMP/SVG (and HEIC/HEIF with the pillow-heif package) work when the Python backend is installed. No params.

- images_to_pdf: {}

html_to_pdf

Renders an HTML file to PDF — set the workflow's input to a .html file. engine auto-picks a system Chrome/Edge (best fidelity), then WeasyPrint, then the built-in engine.

- html_to_pdf: { engine: chrome }

markdown_to_pdf

Renders a Markdown file to a styled PDF — set the workflow's input to a .md file. engine auto-picks a system Chrome/Edge for the best result (no bundled browser); force story for a zero-dependency render.

- markdown_to_pdf: { engine: chrome }

url_to_pdf

Fetches a web page and renders it to PDF using a system Chrome/Edge when present. Needs no input file: use inputs: [] and pass the url.

- url_to_pdf: { url: "https://en.wikipedia.org/wiki/Portable_Document_Format", engine: chrome }

eml_to_pdf

Renders an .eml email (headers + body) to a styled PDF — set the workflow's input to the .eml file.

- eml_to_pdf: { engine: auto }

epub_to_pdf

Converts an EPUB ebook to PDF. Make it the first operation with a .epub input. The bundled PyMuPDF engine renders it by default; engine: calibre (with paper_size) paginates better when Calibre is installed.

- epub_to_pdf: { engine: auto }

office_to_pdf

Converts an Office document to PDF — set the input to a docx/xlsx/pptx/odt file and set from to its format.

- office_to_pdf: { from: pptx }

Convert from PDF

pdf_to_docx

Converts the PDF to a Word document written to to. Fidelity varies (LibreOffice).

- pdf_to_docx: { to: output/board-minutes-2024.docx }

pdf_to_pptx

Converts the PDF to a PowerPoint file written to to. Fidelity varies (LibreOffice).

- pdf_to_pptx: { to: output/investor-deck.pptx }

pdf_to_xlsx

Converts the PDF to an Excel file written to to. Fidelity varies (LibreOffice).

- pdf_to_xlsx: { to: output/expense-report-2024.xlsx }

pdf_to_html

Converts the PDF to an HTML file written to to. Fidelity varies (LibreOffice).

- pdf_to_html: { to: output/converted.html }

pdf_to_markdown

Converts the PDF to Markdown. Set the workflow's output.file to a .md path and the Markdown is written THERE directly (no stray PDF) — or pass to for a side artifact while the PDF passes through. engine auto-picks pymupdf4llm → markitdown → PyMuPDF; use marker (Surya OCR + layout, needs marker-pdf) for scanned books. remote ("user@host") runs Marker on a GPU box over SSH — minutes on a 4090 vs hours on a CPU; requires the pdfStudio.allowRemoteRender setting.

- pdf_to_markdown: { engine: auto }

pdf_to_epub

Converts the PDF to a **reflowable EPUB** ebook written to to — ideal for reading on Kindle and other e-readers (text reflows to the screen, unlike a fixed PDF). engine: auto uses Calibre's ebook-convert when it's installed (best reflow + formatting) and otherwise falls back to a bundled text-to-EPUB builder (always available; text-focused). Chapters come from the PDF outline when present, else every chapter_pages pages. title/author default to the PDF metadata. Best for prose/text books; complex layouts reflow imperfectly.

- pdf_to_epub: { to: output/book.epub }

Document intelligence (AI)

summarize

Summarizes the PDF's text with an LLM and writes a Markdown summary to to (or a .md output.file). style: bullets (default), abstract, or outline; focus narrows the topic. Runs a LOCAL model via Ollama by default; set ANTHROPIC_API_KEY for Claude. Requires the pdfStudio.allowAiRequests setting.

- summarize: { style: bullets, to: output/summary.md }

translate

Translates the PDF's text to lang with an LLM. By default writes the translation as Markdown to to. Set layout: true to instead render a translated PDF that keeps the original page geometry, images and tables — each text block is translated in place (best for Latin/CJK scripts; point output.file at the PDF). Chunks long documents automatically. Runs a LOCAL model via Ollama by default; set ANTHROPIC_API_KEY for Claude. Requires the pdfStudio.allowAiRequests setting.

- translate: { lang: Spanish, layout: true }

Finds passages by MEANING, not keywords — ask in plain language and get the matching passages back, ranked, with their page numbers. Embeds the document with a LOCAL model (nomic-embed-text via Ollama by default: ollama pull nomic-embed-text), so nothing leaves your machine. Needs the pdfStudio.allowAiRequests setting. Scanned PDF? Run ocr first.

- semantic_search: { query: "the refund policy for late cancellations", top_k: 5, to: output/search-results.md }

Digital signatures

sign

Digitally signs the PDF with a PKCS#12 certificate. cert is a .p12/.pfx path; use ${ENV} for the password.

- sign: { cert: assets/acme-legal.p12, password: "${CERT_PASSWORD}", field: Signature1, reason: "Approved by Legal" }

validate_signature

Validates the PDF's digital signatures and writes a JSON report to to. Does not change the PDF.

- validate_signature: { to: output/signature-report.json }

timestamp

Adds an RFC-3161 trusted timestamp from a TSA. tsa_url is the timestamp server (needs network).

- timestamp: { tsa_url: "http://timestamp.digicert.com" }

Install Lynx PDF Studio →