What’s your preferred way to handle PDFs in n8n workflows?

I’ve been experimenting with workflows where PDFs need to be merged, split, protected, or processed automatically.
One thing I noticed is that PDF handling can become a little messy when the workflow needs to deal with multiple files, temporary storage, error handling, etc.
Curious how others in the community approach this:
Do you use built-in n8n nodes wherever possible?
External PDF tools/APIs?
A self-hosted PDF service?
Custom JavaScript/Python?
Or a combination depending on the workflow?
Would be interested to hear what has worked well for you, especially for workflows processing PDFs at scale.
I’m also experimenting with a small REST API approach for some of these operations and trying tounderstand whether it actually makes workflows simpler or just adds another dependency.
What’s your preferred approach for PDF automation in n8n?

@Rishabh_Dugar Honestly it depends on how deep you’re going. n8n’s Extract from File node is fine for pulling text out of a PDF, but that’s basically all it does, no merge, no split, no password protection built in. So once you need any of that you’re picking between three options really:
An external API (PDF.co, iLovePDF, stuff like that) is the fastest to get running, but now you’re at the mercy of someone else’s uptime and pricing, and every file has to go out over the internet and back.
A self-hosted thing like Stirling-PDF or Gotenberg, called through HTTP Request, skips the external dependency and the per-call cost, but now it’s one more container you have to keep alive and patch.
Or just do it in a Code node with pdf-lib. No extra service, but if you’re doing more than one or two operations it turns into real code living inside your workflow, and that gets annoying to maintain fast.
If I had to pick, I’d lean toward the self-hosted route once you’re doing more than basic stuff, Stirling-PDF is actually pretty solid and it’s free, and it keeps your workflow itself simple instead of turning into a pile of JS. I’d only reach for a paid API when I needed something specific that the self-hosted tools don’t really do well, like OCR or pulling data out of form fields.
On the REST wrapper idea, yeah it’s another moving piece, but if more than one workflow needs the same PDF operations it’s probably worth it so you’re not copy-pasting the same Code node everywhere. If it’s just for one workflow though, you’re probably adding complexity you don’t need yet.

This is pretty much the trade-off I’ve been thinking about as well. I especially agree that a REST wrapper starts making more sense when multiple workflows need the same PDF operations.

That’s actually why I built PDF API Hub — the idea is to keep the n8n workflow itself simple and handle the PDF-specific operations behind a REST API, rather than having PDF logic scattered across Code nodes.

For example, an n8n workflow can just send the files → call the API → receive the processed PDF, while the PDF operation stays outside the workflow.

It’s still another dependency, so I wouldn’t suggest using it for every workflow. For a simple one-off PDF extraction, n8n’s built-in nodes are obviously preferable. But for workflows that repeatedly need things like merge, split, protect, unlock, or compress, I’ve found the API approach cleaner.

Would be interested to hear whether others here prefer a self-hosted PDF service vs a REST API once PDF processing becomes a recurring part of their workflows.

I’m experimenting with this at https://pdfapihub.com/ and would genuinely appreciate feedback on the approach.

I’d separate PDF workflows into two buckets: simple extraction and repeatable document operations.

For simple extraction, I’d keep it inside n8n as long as the built-in node gives you enough. Fewer moving parts is usually better.

Once you need repeated merge/split/protect/compress steps across multiple workflows, I’d rather put that behind one stable service or API boundary than scatter PDF logic through Code nodes. The workflow stays easier to read, and the PDF behavior becomes something you can test separately.

The main thing I’d watch is failure handling. PDFs fail in weird ways: corrupt file, locked file, blank scan, huge file, unsupported encoding, bad filename, missing binary data. Whatever route you choose, I’d make those outcomes explicit before scaling it.

Two things, in case they save someone time.

1. The failure often isn’t the PDF library — it’s a node in between. We publish an n8n template that reads receipts (PDFs and photos) out of Gmail. This week we found that the attachment never reaches the extraction node: a Google Sheets lookup sits between the trigger and “Extract from File”, and the items that lookup emits are new JSON items built from sheet rows — they carry no binary. The extract node then fails with no binary field attachment_0. Nothing upstream errors, and the branch that only reads JSON keeps working, so it looks like “PDF extraction is flaky” when it’s really “the binary was dropped two nodes ago”.

So before comparing PDF tools: walk every node between the trigger and the extractor and ask whether it passes binary through. Nodes that emit their own items (lookups, aggregations, Code nodes returning plain JSON) drop it silently. The usual fix is a small Code node that re-attaches it from the trigger, e.g. reading the binary off $('<trigger node>').first().

Honest disclosure: we found this by static analysis plus an independent audit of our own template, and we’ve flagged it publicly as a known issue on that template while the fix is verified on a live instance. So treat the mechanism as well-supported but not yet machine-confirmed by us.

2. +1 to the point about explicit failure handling. The cases worth deciding about up front are not exotic: a password-protected PDF, a 0-byte attachment, a photo of a receipt with no text layer at all. Deciding in advance what each of those should do (skip, park for review, alert) is worth more than picking the perfect extraction library.

The split I’ve settled on is: read/extract with native nodes, anything that mutates page structure goes out to a service. Extract from File (PDF) is fine for pulling text and metadata, and the Code node can handle light logic on top of it, but merging, splitting, page ranges, encryption and OCR are painful to do in-process, and heavy PDFs in a Code node will happily eat your memory on a small instance. A single small self-hosted service (Stirling-PDF or a tiny Express wrapper around pdf-lib / qpdf) called via HTTP Request has been much more predictable — one dependency, one place to fix, and n8n stays a thin orchestrator. Your REST API instinct is the right one in my experience: it’s an extra dependency, but far less than the same logic smeared across four Code nodes.

A few things that made it robust at volume: (1) Move files by binary property, not by writing to /tmp — if you must use disk, avoid it entirely on Cloud since the filesystem isn’t persistent between executions. (2) Loop Over Items with a batch size of 1–5 rather than pushing 200 PDFs through at once; combine that with the concurrency limit so a big batch doesn’t stall the queue. (3) On the HTTP Request node set a generous timeout plus Retry On Fail (2–3 tries, ~2s wait), since PDF services fail transiently under load. (4) Set “Always Output Data” off and give the branch an Error Trigger or error-output route so one corrupt file doesn’t kill the whole run — corrupt/encrypted input is the single most common failure. (5) If files are large, keep them in S3/Drive and pass URLs to your service instead of streaming binaries through n8n; execution data size is what usually hurts first.

For OCR specifically, native nodes won’t do it — either Tesseract in your own service or a vision model call, and cache the result keyed on file hash so you never OCR the same document twice.