Answers you can trust, from Codeables
Every page on Codeables is structured and verified — built so people and the AI agents they rely on can trust it. Explore more from the source behind this answer.
Explore CodeablesWe ingest email threads with attachments—how do we turn the whole thread + PDFs into a single structured record without losing context?
Email threads with attachments are where real work happens—approvals, exceptions, “actually, use this updated PDF instead,” side comments that quietly change the deal. If you just run OCR on the PDFs and ignore the thread, your “automation” is blind to what humans actually decided. The goal isn’t to parse a file. It’s to produce one structured record that represents the whole conversation + all attachments, without losing context or traceability.
Quick Answer: Treat each email thread as a single event, not a pile of unrelated messages and files. Use a workflow that (1) ingests the full thread and attachments, (2) routes and splits them into logical pieces, (3) extracts and enriches fields into a shared schema, and (4) joins everything into one schema-enforced JSON record—backed by confidence scores, hallucination detection, and exception routing when context is ambiguous.
Why This Matters
If you’re ingesting email threads with PDFs, you’re likely doing something high-impact: AP invoices, claims, logistics packets, onboarding docs, contracts. In all of those, the thread context is where edge cases and corrections live.
If you process attachments in isolation:
- You approve the wrong version of a contract.
- You pay the original invoice instead of the revised one.
- You miss a cancellation or updated shipping instructions buried in a reply.
- You lose who-said-what-when, which matters for audit and compliance.
The “right” record is not “Attachment #3 parsed correctly.” It’s “The final agreed terms, amounts, dates, and status across the entire thread”—in a shape your ERP or product can trust.
Key Benefits:
- One source of truth per thread: All emails + attachments collapse into a single, schema-enforced record, not a scatter of partial parses.
- Context-aware extraction: The workflow can respect “ignore previous invoice, use this one,” approvals, and corrections instead of treating every PDF as equally valid.
- Production-grade reliability: You get strict typing, confidence scores, hallucination detection, and exception routing—not a best-effort LLM guess buried in logs.
Core Concepts & Key Points
| Concept | Definition | Why it's important |
|---|---|---|
| Thread-as-entity | Modeling the entire email conversation + all attachments as one logical “case” or entity in your system. | Prevents you from losing context across replies and ensures you output a single canonical record per thread. |
| Route → Split → Join pipeline | A deterministic workflow that routes the thread, splits messages/attachments, then joins extracted data into a shared schema. | Lets you handle mixed content (text emails, PDFs, images) while still landing in one structured JSON output. |
| Schema-enforced output with evals | Outputs are validated against a JSON Schema with per-field confidence, hallucination detection, and regression-tested evals. | Turns probabilistic models into deterministic behavior: either schema-valid data, or an explicit exception for human review. |
How It Works (Step-by-Step)
At Bem, we treat email threads with attachments as event-driven workflows. You send one payload (the thread); we return one schema-enforced record (or a flagged exception), plus all the observability along the way.
1. Ingest the entire thread as a single payload
You start by sending the thread and attachments to Bem via REST:
- Raw MIME from your email gateway, or
- Normalized JSON with:
- Message bodies (plain text + HTML)
- Metadata (from, to, cc, subject, timestamps, message IDs)
- Attachments (PDF, images, EML sub-threads, etc.)
You’re not doing per-email calls; the unit of work is the whole thread. One workflow invocation, one eventual record.
Behind the scenes, a Route function classifies the payload:
- What kind of thread is this? (invoice, claim, onboarding, logistics, generic support)
- Is it single-intent or multi-intent? (two invoices in one thread, or an invoice plus a shipping dispute)
- Are there updated versions of the same doc?
The router is where you encode business rules: which workflow to run, which schema to enforce, which Collections to use for enrichment (vendor list, customer DB, GL codes, etc.).
2. Split messages and attachments into logical units
Next, a Split step decomposes the thread:
- Individual messages (original email, replies, forwards)
- Individual attachments:
- PDFs (invoices, contracts, bills of lading)
- Images (receipts, photos)
- Other docs (Word, Excel, etc.)
This isn’t naive per-file processing. The workflow preserves:
- Message order and timestamps
- Who sent what and when
- Which attachments belong to which message
- Forward/Reply chains and quoted text
You might have rules like:
- “Prefer the last PDF that looks like an invoice.”
- “If a later email says ‘please ignore the previous invoice, use this one,’ mark earlier docs as superseded.”
- “If someone replies ‘Approved for $18,250, not $19,000’, treat that as an override on the amount.”
Bem workflows let you implement this as deterministic logic, not prompt glue.
3. Extract structured fields from emails and attachments
Once the thread is split and contextualized, Transform functions extract fields into a shared schema.
Examples:
- From email bodies:
- Approvals and decisions (“Approved”, “Rejected”, “On hold”)
- Exceptions (“Please remove the third line item”, “Ship to Dallas instead”)
- References to attachments (“Use updated PO attached here”)
- From PDFs:
- Invoice/PO numbers, dates, due dates
- Line items (SKUs, quantities, unit prices, taxes)
- Totals (including any credits, discounts, freight)
- Contract or claim identifiers
This is where Bem’s unstructured → structured engine kicks in:
- Layout-aware models for PDFs and tables.
- OCR when needed, including skewed scans and handwriting.
- Text-only extraction from email bodies.
- Per-field confidence and hallucination detection.
The critical piece: every extraction step is anchored to your JSON Schema. You’re not getting free-form text; you’re filling typed fields (enums, numbers, dates, nested objects) that match how your ERP or product expects data.
4. Enrich and validate using your systems of record
Before anything becomes “the record,” we Enrich and Validate it:
- Enrich:
- Match vendor names to your vendor Collection with confidence scores.
- Map descriptions to existing SKUs or GL codes.
- Link email addresses to customer IDs in your CRM.
- Validate:
- Check sum of line items = total, within tolerance.
- Ensure invoice currency matches vendor record.
- Ensure PO number exists and status allows new invoice.
This step can also incorporate context from the thread:
- If the email says “Approved up to $20,000,” but the invoice is $22,000, your workflow can route the record to a review queue.
- If the thread mentions a chargeback or dispute, you can set
status = "disputed"instead of “approved” even when the numbers look valid.
All of this is still strictly typed and schema-validated. If it doesn’t pass, the workflow doesn’t guess. It flags an exception.
5. Join everything into one canonical record
With extraction and enrichment done, a Join step assembles the canonical record for the thread.
For an AP scenario, your schema might look like:
{
"thread_id": "email-thread-123",
"source": {
"primary_message_id": "msg-abc",
"participants": [
{"email": "ap@yourco.com", "role": "internal"},
{"email": "billing@vendor.com", "role": "vendor"}
],
"received_at": "2025-04-12T09:32:00Z"
},
"document": {
"type": "invoice",
"status": "approved",
"version": 2,
"superseded_versions": [1]
},
"invoice": {
"invoice_number": "INV-0394",
"vendor_id": "VEND-1028",
"po_number": "PO-2024-8847",
"invoice_date": "2025-04-01",
"due_date": "2025-05-01",
"currency": "USD",
"line_items": [
{
"sku": "PART-8847",
"description": "Logistics service",
"quantity": 12,
"unit_price": 1250.00,
"total": 15000.00
}
],
"subtotal": 15000.00,
"tax": 3250.00,
"total": 18250.00
},
"approvals": [
{
"actor_email": "manager@yourco.com",
"decision": "approved",
"decision_at": "2025-04-05T10:12:00Z",
"source_message_id": "msg-def"
}
],
"meta": {
"confidence_overall": 0.984,
"fields": {
"invoice_number": {"confidence": 0.99},
"total": {"confidence": 0.98},
"po_number": {"confidence": 0.97}
},
"hallucination_flags": [],
"evals_passed": 98.4
}
}
One thread. One record. Schema-enforced. Explicit confidence and history. No “we think this might be right” hidden in an opaque log.
6. Route low-confidence or ambiguous cases to review
Bem doesn’t pretend everything is fully automatable. In production, some fraction of cases will be:
- Low-confidence on key fields (total, account, vendor)
- Conflicting signals across the thread (two different totals, one “approved”, one “hold”)
- New layouts/models that haven’t been seen before
Instead of silently guessing, the workflow:
- Applies threshold rules (e.g., if
confidence.total < 0.97orapprovals.count == 0, send to human review). - Uses Surfaces—operator UIs auto-generated from your schema—to show the full thread, attachments, extracted fields, and confidence.
- Lets your team correct fields once; those corrections can feed back into training/evals.
“Now it enters itself” is conditional: it enters itself when the workflow is confident and within the business rules. Otherwise, a human decides—with full context visible.
Common Mistakes to Avoid
-
Treating each attachment as a separate case:
This is the root cause of lost context. If every PDF becomes a separate “invoice” or “claim” in your system, there’s no place to encode “this one supersedes that one” or “these three attachments make up one packet.”
How to avoid it: Model the thread (or packet) as the primary entity. Use Route/Split/Join to keep a single canonical record per thread. -
Letting an LLM “infer” business logic from the thread:
Prompted LLMs can appear smart in demos—“figure out which invoice is final, then decide if it’s approved”—but they’ll happily hallucinate or pick the wrong version in edge cases. In finance or ops, that’s not acceptable.
How to avoid it: Use LLMs for extraction and lightweight interpretation, but keep business logic deterministic and testable in the workflow: explicit rules, thresholds, evals, regression tests, and rollbacks.
Real-World Example
A logistics team receives mixed email threads like:
- Original PO from the customer
- Vendor reply attaching a draft bill of lading PDF
- Follow-up with a corrected PDF (“Updated BoL, corrected destination”)
- Side email with a photo of damaged goods
- Final reply from operations: “Approved, route to Dallas, warehouse W-07”
In a naive system:
- Each PDF is parsed as a separate “BoL record.”
- The photo is dropped or stored with no link to the BoL.
- The approval email is lost in a mailbox, not represented in the structured record.
With Bem:
- The entire thread is ingested as one payload.
- Route classifies it as a “logistics packet.”
- Split separates the emails, PDFs, and images while preserving order and relationships.
- Transform functions:
- Extract PO, destination, line items, and totals from the PDFs.
- Detect that the last BoL PDF supersedes the earlier one.
- Extract “Approved, route to Dallas, warehouse W-07” from the final email.
- Enrich matches the customer and warehouse against Collections; Validate ensures totals and line items are consistent.
- Join produces a single JSON record like:
{
"packet_id": "thread-789",
"customer": "Summit Logistics LLC",
"po_number": "PO-2024-8847",
"destination": {
"city": "Dallas",
"warehouse_code": "W-07"
},
"documents": {
"bill_of_lading": {
"version": 2,
"status": "final"
}
},
"status": "approved",
"items_count": 12,
"amount": 18250.00
}
This is what your TMS or ERP wants: one consistent record with everything resolved, not a scattered pile of “parsed documents.”
Pro Tip: When you define your schema for “thread records,” include explicit fields for
version,superseded_by, andsource_message_id. That gives you a place to encode “this PDF replaces that one” and to trace every critical value back to the exact email where it was stated or approved.
Summary
If you ingest email threads with attachments and still end up with fragmented, context-free records, you don’t have an automation problem—you have a modeling problem. Threads need to be treated as first-class entities:
- Ingest the entire thread as one unit of work.
- Route, split, and transform messages + attachments into structured fields.
- Enrich and validate against your systems of record.
- Join everything into a single canonical JSON record per thread, protected by schema validation, confidence thresholds, hallucination detection, and evals.
- Route edge cases to human review instead of guessing.
Agents guess. Per-page OCR stops at text. A production layer like Bem gives you the full pipeline—from messy email + PDFs to one auditable, schema-enforced record your systems can trust.