5 lessons from building a multi-signer document workflow in n8n

:waving_hand: Hey n8n Community,

Just wrapped a build that takes incoming documents from one team, bundles them with assets from a second team, and routes the package to two sequential signers, all triggered by emails. Some learnings that might save you a few hours.

1. Order your state machine for the human, not the system

Originally I wired: collect input from team A → send for signature → ask team B for supporting docs → loop back for re-sign. Total mess. Swapped to: collect from team A → wait for team B → bundle → single signature round. Rule I’m keeping: signers should never have to context-switch back to a document they thought was done. Re-route the state machine if you have to.

2. Filesystem-mode binary breaks $binary.data.toString('base64')

If your n8n runs in filesystem storage mode (the default for self-hosted), binary.data is just a marker string, not the actual file bytes. Calling .toString('base64') returns base64 of the marker, which is ~14 bytes of garbage that downstream APIs happily accept then mangle. Use this.helpers.getBinaryDataBuffer(0, 'data') inside a Code node to load real bytes.

3. The Merge trick for cross-node binary access

After a Sheets Lookup or Update, your binary is gone from the current item. Instead of $('NodeName').item.binary.data references everywhere, branch your binary-source node into two paths: one through your normal chain, one to a Merge node (Combine by Position) downstream. The Merge stitches the binary back exactly where you need it. Way cleaner than cross-node refs.

4. Parser nodes should be lenient on attachment types, strict on validation

I started by requiring a PDF attachment. Then real-world users sent two phone photos and zero PDFs. Then a PDF plus inline-embedded images from email signatures. Then nothing at all by accident. Build your parser to classify by mime type AND filename, accept what’s plausible, and only throw when there’s literally nothing to process. The downstream “should I run extraction?” decision belongs in an IF node, not in the parser.

5. n8n’s JSON body validator runs before expressions resolve

If your HTTP node body has a JSON expression like "documents": {{ JSON.stringify($json.documents) }}, n8n’s parser flags it as invalid JSON before the workflow runs, because at validation time it sees {{ ... }} as literal text, not parsed JSON. Fix: wrap the entire body in one big expression, prefixed with =:

={{ JSON.stringify({ template_id: ..., documents: $json.documents }) }}

Single expression, evaluates at runtime, validator stays happy.

Bonus: extraction from variable-quality input

For pulling structured data out of incoming attachments (could be a clean scan, could be a handwritten form photographed on a phone), I used my own Extractor. Way less brittle than OCR + regex.

  • n8n Cloud: search easybits in the nodes panel, it’s verified by n8n, so it shows up natively, no install step

  • Self-hosted: install the community node @easybits/n8n-nodes-extractor from Settings → Community Nodes

What’s the messiest real-world input your workflow has had to handle? Curious whether others are dealing with the same “user sends whatever, parser figures it out” situation, or if you’ve found a clean way to enforce structure upstream.

Best,
Felix

2 Likes

Lesson 2 is one that catches a lot of people off guard, especially after migrating from n8n Cloud to self-hosted. The getBinaryDataBuffer approach is the right fix - worth noting that if you’re on a newer n8n version you can also use $binary.data directly in certain nodes only when the storage is set to database mode, so switching to database binary storage is an alternative if you’d rather avoid the Code node overhead.

Lesson 5 about JSON body validation running before expressions is also extremely underrated - that one alone has saved me from hours of debugging silent payload failures.

1 Like

Easier option for the base64 bit is to use the node that we have for it. Search file and you will find it.

2 Likes

Hey @nguyenthieutoan, thank you for the kind words, and thanks as well for sharing the tip about $binary.data. I’m still working with a slightly older version of n8n, so I wasn’t aware of that feature yet. Always great to learn something new.

Hey @BramKn, that’s a great idea! I hadn’t even considered that approach initially, but it definitely sounds worth exploring. I’ll be sure to give it a try, thanks for the suggestion.

1 Like

Thanks, very useful

1 Like

Hey @maicy_frisser, thank you for the kind words! I share these kinds of lessons from time to time because I’m often working on real-life examples, and when I first started my n8n journey, I would have really benefited from seeing more practical, real-world challenges to better prepare myself.

great job you are doing here

1 Like