Reference letters arrive in many different layouts. Here's what I learned making one workflow handle all of them

:waving_hand: Hey n8n Community,

Last week, I shared a workflow I built for Lisa, the recruiter at Mike’s company, that automatically parses reference letters from email attachments. Three things mattered more than I expected.

1. Good field descriptions are the whole game when inputs vary

Reference letters arrived in every possible shape, handwritten signatures on scans, typed letters with letterheads, single paragraphs, multi-page essays, some with bullet lists, some pure prose. Most of my time wasn’t writing code. It was writing descriptions for the extractor fields that anchored on the concept rather than where in the document the data lived.

The first version of my referee_name description said “name of the referee.” It kept extracting the candidate’s name instead, because the candidate was mentioned more often. Adding “the person writing the reference, not the candidate, look at the sign-off or signature block” fixed it on every layout I threw at it. Same pattern for relationship type, tone, would-rehire signal: describe the concept, give it a tiebreaker, name common confusions. With that approach, one schema handled scans, typed letters, and multi-page formats without branching logic. Most of the robustness lives in the descriptions, not the workflow.

2. One email, many attachments: build for the multi-attachment case from day one

Candidates routinely forward several reference letters in a single email. Most n8n examples assume one binary per item, which falls apart immediately here. The pattern that worked: a small Code node right after the Gmail trigger that splits the email’s binary keys into separate items, normalizing each to the key data. After that, a Loop Over Items node processes one letter at a time. The downstream nodes don’t know or care that the source email had three attachments.

The other piece is a Filter node before the extractor that drops logos, signature snippets, and footer images by mime type and filename. Reference letters always come as real PDFs or full scans, so the filter is cheap and saves API calls on junk. Worth designing in from the start rather than bolting on after the first logo-as-reference-letter row appears in your sheet.

3. Comparability is the product, not the extraction

The story isn’t “I parsed a letter.” It’s “Lisa had 47 letters in her inbox and no way to compare them.” That reframes everything downstream. Free-text tone descriptions are useless if you want to sort a column. So tone gets snapped to a fixed three-value enum (positive / neutral / hedged), would-rehire to (yes / no / unstated), and a deterministic JavaScript score on top, not the LLM grading itself, just weights on the structured fields.

The payoff: sort by candidate and score, and a whole inbox of prose collapses into a ranked, scannable table. That’s the moment Lisa actually saved time. The extraction was table stakes; the comparable output was the product.

Full workflow with sticky notes is here if you want to dig in: n8n-workflows/easybits-candidate-reference-check-parser-workflow/easybits_candidate_reference_check_parser.json at 850c7798a6f059900998c20ead0d7087750c17dc · felix-sattler-easybits/n8n-workflows · GitHub

Curious how others tackle robustness across varying document layouts when it comes to data extraction. Do you rely on better field descriptions, or do you branch your workflow based on document type?

Best,
Felix

2 Likes

The emphasis on field description quality over schema structure is a genuinely underrated insight - most people try to solve layout variance with more complex parsing logic when the real leverage is in how precisely you define what you’re looking for. The tiebreaker pattern for referee_name (“the person writing the letter, not the candidate, look at the sign-off block”) is exactly the kind of domain-specific grounding that makes structured extraction reliable across messy real-world docs.

1 Like

Hey @nguyenthieutoan, 100% agreed! I think many people assume that the technology itself needs to evolve further before we can build truly robust solutions. In reality, a lot of it comes down to how we use the technologies that are already available today.