How to Extract Structured Data from Emailed Contract PDFs with a Mailhook
Point your contract PDFs at a Spojit mailhook address and let a Knowledge node in Query mode pull parties, dates, and key terms into a clean record stored in MongoDB.
What This Integration Does
Legal and procurement teams receive signed contracts as PDF attachments all day, and someone has to open each one, read it, and copy the parties, effective date, renewal date, and payment terms into a system of record. This workflow removes that manual step. You forward (or have a counterparty send) the contract to a dedicated Spojit mailhook address, and within seconds a Knowledge node in Query mode reads the document and writes a structured record you can search, report on, and chase for renewals.
The run model is push-based. A Mailhook trigger fires the moment a message lands on its unique address, with no mailbox or polling involved. An Attachment node fetches the PDF bytes, a Knowledge node embeds the document into a transient collection and queries it with a forced JSON shape, and a Connector node writes the result to MongoDB. Each email is deduplicated per message, so the same contract arriving twice does not create two records. The workflow runs asynchronously and leaves one new document in your contracts collection per processed email; re-sending the same message will not re-run it, but a genuinely new email always starts a fresh run.
Prerequisites
- A workflow with a Mailhook trigger and a generated address (see the related guide below).
- A MongoDB connection added under Connections, with write access to a database and a
contractscollection. - Contracts that arrive as real PDF attachments (not inline links or scanned image-only files without a text layer). The Knowledge node can OCR images, but text-based PDFs extract most reliably.
- Knowledge available in your plan, since the Knowledge node and Agent-style queries consume AI credits.
Step 1: Set up the Mailhook trigger
Add a Trigger node and set Trigger Type to Mailhook. Enter an optional Address prefix such as contracts (1 to 24 characters, default mh), then click Generate email address. Spojit produces a unique address in the form contracts-<random16>@mailhook.spojit.com. Copy it and configure your contract senders or a forwarding rule to deliver mail there. To narrow what runs, set an optional From allowlist (for example your legal team and known counterparties) and a Subject regex. The trigger fires whether the address is in To, Cc, or Bcc. Its output is available downstream as {{ input }}, including {{ input.subject }}, {{ input.from }}, {{ input.replyTo }}, and {{ input.attachments }}.
Step 2: Fetch the contract PDF with an Attachment node
Add an Attachment node directly after the trigger. The designer will not save an Attachment node unless a Mailhook trigger is present, so this only works in mailhook workflows. Set Mode to Single so you get the first matching file as a single object. Set the Content type filter to application/pdf and the Filename pattern to *.pdf to ignore signature images or logos in the email signature. Turn on Fail if no attachment matches so a contract email with no PDF stops the run cleanly instead of writing an empty record. The node outputs:
{
"filename": "msa-acme-2026.pdf",
"contentType": "application/pdf",
"size": 184213,
"content": "JVBERi0xLjcKJ..."
}
The content field is the base64 of the PDF, ready to feed straight into the Knowledge node. Keep attachments under the 10 MB per-attachment and 25 MB per-run defaults.
Step 3: Embed the contract into a transient collection
Add a Knowledge node in Embed mode. In the Collection dropdown choose Transient, which auto-creates a throwaway collection scoped to this single run, shared with later nodes in the same run, and cleaned up automatically when the run finishes. Transient mode needs no file name or embedding model, which is ideal for one-off "embed then query then discard" extraction on a single document. Set Document Type to PDF and set Document Input to the attachment bytes:
{{ attachment.content }}
Set an Output Variable such as embedResult so you can confirm the chunk count in the execution logs. This step turns the raw contract into searchable chunks for the query in the next step.
Step 4: Query the contract for structured fields
Add a second Knowledge node in Query mode and again select Transient in the Collection dropdown so it reads the document embedded in Step 3 during this same run. Write a focused Prompt that names the fields you want, and use the Response Schema to force a reliable JSON shape so every record has the same keys. A prompt like the following works well:
Extract the contract details from this document. Return the legal
names of all parties, the effective date, the renewal or expiry
date, the total contract value, the payment terms, and the
governing law. Use ISO 8601 (YYYY-MM-DD) for all dates. If a field
is not stated, return null for it.
Set the Response Schema to define the exact output, for example fields named parties (array), effectiveDate, renewalDate, contractValue, paymentTerms, and governingLaw. Leave Result Count at its default unless contracts are long, choose your synthesis Model, and set an Output Variable such as contract. The structured result is then available as {{ contract.parties }}, {{ contract.effectiveDate }}, and so on.
Step 5: Write the record to MongoDB
Add a Connector node, select your MongoDB connection, and use Direct mode with the insert-documents tool so the write is deterministic and costs no AI credits. Target your database and the contracts collection, and map a single document built from the extracted fields plus useful provenance from the trigger and attachment:
{
"parties": {{ contract.parties }},
"effectiveDate": "{{ contract.effectiveDate }}",
"renewalDate": "{{ contract.renewalDate }}",
"contractValue": "{{ contract.contractValue }}",
"paymentTerms": "{{ contract.paymentTerms }}",
"governingLaw": "{{ contract.governingLaw }}",
"sourceFile": "{{ attachment.filename }}",
"sourceFrom": "{{ input.from }}",
"sourceSubject": "{{ input.subject }}",
"messageId": "{{ input.messageId }}",
"receivedAt": "{{ input.receivedAt }}"
}
Storing messageId on the record gives you a stable key to detect duplicates later with find-documents if you ever need to reconcile against re-forwarded mail.
Step 6: Confirm receipt by email (optional)
Because a Mailhook trigger never replies to the sender, add a Send Email node if you want a confirmation. It sends from Spojit's built-in mail service with no connection needed. Set Recipients to {{ input.replyTo }}, give it a Subject such as Contract logged: {{ contract.parties }}, and write a short Body summarizing the captured fields. Remember that external recipients must be on the org allowlist under Settings > General > Email recipients, and that sends count toward your monthly email allowance.
Tips
- Use the Response Schema on the query node every time. Forcing JSON keeps each MongoDB document uniform so downstream reports and renewal queries never break on a missing field.
- To capture more than one contract per email, set the Attachment node Mode to
Multiple, then add a Loop node over{{ attachment.attachments }}and run the embed, query, and insert steps inside the loop body. - Keep the embed and query on Transient for single-document extraction. If you instead want a permanent, searchable archive of every contract, embed into a persistent collection created under the Knowledge section of the sidebar and use the same embedding model for both embed and query.
- Ask Miraxa to scaffold the skeleton for you with a prompt such as "Build a workflow that watches a mailhook, fetches the PDF attachment, extracts contract fields with the Knowledge node, and inserts them into MongoDB", then fine-tune fields in the properties panel.
Common Pitfalls
- Adding the Attachment node to a non-mailhook workflow: it only saves when the trigger is a Mailhook. Email-trigger workflows handle attachments differently.
- Scanned, image-only PDFs with no text layer extract poorly. Set Document Type to images-via-OCR if your contracts are scans, and expect lower accuracy than on native PDFs.
- Forgetting date normalization. Without an explicit instruction to use
YYYY-MM-DD, contracts written in different regional formats produce inconsistent dates that are hard to query in MongoDB. - Oversized files. Attachments above the 10 MB per-attachment or 25 MB per-run defaults will not be fetched, so large multi-document bundles may need splitting before they reach the mailhook.
Testing
Before pointing live contract mail at the address, send yourself one short, known PDF to the generated mailhook address and watch the run in the execution history. Confirm the Attachment node returns a non-zero size, the embed node reports a chunk count, the query node's contract output matches the document, and a single record lands in MongoDB. Use find-documents in a throwaway Connector node (or your database tool) to verify the shape, then add a From allowlist and Subject regex on the trigger so only genuine contract emails run in production.