How to Route Emailed Documents to the Right System with a Mailhook and Subworkflows

Give your team one intake email address that receives every inbound document, then have Spojit classify each one and hand it off to a specialized subworkflow built for that document type.

What This Integration Does

Many teams collect documents through a shared inbox: invoices, purchase orders, signed contracts, shipping manifests, and more all land in the same place, and someone has to read each one and forward it to the right process. This tutorial replaces that manual triage with a single Spojit workflow. You publish one Mailhook intake address, point your forwarding rules and vendor notifications at it, and let Spojit decide what each message is and where it should go. A short classification step reads the email and its attachment, then a Condition node dispatches the run to a dedicated Subworkflow per document type. Each subworkflow stays small, testable, and owned by the team that cares about it.

The run model is push based: any mail sent to the generated address starts a run within seconds, with no mailbox or OAuth to connect. The first Attachment node fetches the document bytes, a Connector node running the pdf connector turns the PDF into text, and an Agent mode step classifies the document into one of a fixed set of categories. The router branches on that label and invokes the matching child workflow, passing along the extracted text, the attachment, and the sender. Runs are deduplicated per message, and each child shows up as its own entry in execution history so you can trace exactly which document went where. Re-sending the same message will not double process it.

Prerequisites

  • A Spojit workspace where you can create workflows and generate a Mailhook address.
  • A Slack connection, so the routing workflow can post a triage summary to a channel. Add it under Connections -> Add connection.
  • The pdf connector, which is a built-in utility connector and needs no authentication.
  • One child workflow per document type you want to route to (for example an "Invoice intake" workflow and a "Purchase order intake" workflow). Each child must have its own trigger and accept an input object. You can stub these first and fill them in later.
  • A decision on your category list. This tutorial uses invoice, purchase_order, and other.

Step 1: Create the workflow and add a Mailhook trigger

Create a new workflow named something like "Document intake router". On the trigger node, set Trigger Type to Mailhook. Enter an optional Address prefix (1 to 24 characters, default mh) such as docs, then click Generate email address. Spojit produces a unique address in the form docs-<random16>@mailhook.spojit.com. Copy it and point your forwarding rules or vendor notifications at it.

The trigger output is available downstream as {{ input }} and includes {{ input.from }}, {{ input.subject }}, {{ input.text }}, {{ input.replyTo }}, and {{ input.attachments }}, where each attachment reference is { id, filename, contentType }. To cut noise, add an optional From allowlist or Subject regex filter so only mail you expect starts a run.

Step 2: Fetch the attached document with an Attachment node

Add an Attachment node right after the trigger. This node only saves inside a Mailhook workflow, and it pulls the actual bytes of an attachment that the trigger only referenced. Configure it as:

  • Mode: Single, so the output is one object you can pass straight on.
  • Content type: application/pdf, to grab the PDF and ignore signature images or logos in the email body.
  • Filename pattern: leave blank, or set *.pdf if senders sometimes attach extra files.
  • Fail if no attachment matches: leave off for now so emails with no PDF can still be routed to the other branch.

In Single mode the node outputs { filename, contentType, size, content }, where content is base64. Note the default limits of 10 MB per attachment and 25 MB per run. You will feed content directly into the PDF step next.

Step 3: Extract the document text with the pdf connector

Add a Connector node in Direct mode using the pdf connector and the extract-text tool. Map its document input to the base64 bytes from the previous step:

file: {{ attachment.content }}

Direct mode is deterministic and costs no AI credits, which is what you want for a predictable single tool call like this. Bind the output to a variable such as pdf_text. If your senders occasionally include multi document PDFs, you can use get-info first to check the page count, but for routing you only need the text. If the Attachment node found no PDF (an email with no match), this step has nothing to read, so handle that in the next step by treating empty text as other.

Step 4: Classify the document with an Agent mode connector node

Add a Connector node in Agent mode. Agent mode lets the agent reason over the document and return a clean label instead of you hand-coding keyword rules. Give it a prompt that includes the subject and the extracted text, and use a Response Schema to force structured JSON so the next step can branch reliably. A good prompt:

Classify this inbound document into exactly one category.
Subject: {{ input.subject }}
Document text:
{{ pdf_text.text }}

Allowed categories: invoice, purchase_order, other.
Return the category and a one-line reason.

Set the Response Schema so the output is predictable:

{
  "type": "object",
  "properties": {
    "category": { "type": "string", "enum": ["invoice", "purchase_order", "other"] },
    "reason": { "type": "string" }
  },
  "required": ["category", "reason"]
}

Bind the result to classification. You can now reference {{ classification.category }} downstream. Because the schema constrains the answer to your enum, a malformed or empty document still resolves to one of the three known values.

Step 5: Route to the right subworkflow with a Condition node

Add a Condition node that branches on {{ classification.category }}. On the branch where the value equals invoice, add a Subworkflow node whose Workflow is your "Invoice intake" child. On the branch where the value equals purchase_order, add a Subworkflow node pointing at your "Purchase order intake" child. Send the remaining (other) path to a fallback you control.

For each Subworkflow node, set the Input so the child receives everything it needs without re-fetching the attachment:

{
  "from": "{{ input.from }}",
  "replyTo": "{{ input.replyTo }}",
  "subject": "{{ input.subject }}",
  "documentText": "{{ pdf_text.text }}",
  "filename": "{{ attachment.filename }}",
  "fileContent": "{{ attachment.content }}",
  "reason": "{{ classification.reason }}"
}

The parent pauses while each child runs through its own trigger and nodes, then the child's final output returns to the parent. Keeping each child focused (one document type, one destination system) is what makes this pattern maintainable: a change to "Invoice intake" takes effect immediately for every parent that calls it, and each child appears as its own execution-history entry.

Step 6: Post a triage summary to Slack

After the Condition node, on a path that all branches converge to, add a Connector node in Direct mode using the slack connector and the send-message tool. Set the channel to your triage channel and build the text from the variables you have gathered:

New document routed: *{{ classification.category }}*
From: {{ input.from }}
File: {{ attachment.filename }}
Why: {{ classification.reason }}

This gives your team a live feed of what is arriving and where it went, without opening each run. If you would rather acknowledge the sender, add a Send Email node and address it to {{ input.replyTo }}, since a Mailhook is always async and never replies to the sender on its own.

Step 7: Handle the unmatched and oversized cases

For the other branch (and for emails with no PDF), route to a small child workflow or a Send Email node that flags the message for a human to look at, including {{ input.subject }} and the sender. This keeps unexpected mail visible instead of silently dropped. If you expect large scans, remember the 25 MB per run limit and the 30 day retention on received emails; for documents above the attachment limit, have the other branch notify a person to download the file directly.

Tips

  • Keep the category enum short. Three to five clear labels classify far more reliably than a dozen overlapping ones, and they map cleanly onto one Condition branch each.
  • Pass the already extracted text and file bytes into each child via the Subworkflow Input instead of re-running the Attachment and pdf steps inside every child. The Attachment node only works in the Mailhook parent anyway.
  • Use Miraxa, the intelligent layer across your automation, to scaffold this quickly. Try a prompt like "Build a workflow that watches a mailhook, extracts the PDF, classifies it as invoice or purchase order, and routes to a subworkflow", then fine tune fields in the properties panel.
  • If a document type later needs an approval step, add a Human node inside the relevant child rather than in the router, so the routing logic stays fast and the approval lives with the process that needs it.

Common Pitfalls

  • Adding the Attachment node without a Mailhook trigger: the designer refuses to save it. Confirm your trigger type is Mailhook before adding it.
  • Forgetting the Response Schema on the Agent mode classifier. Without it the label can come back as a sentence, and your Condition branches will not match the exact value invoice or purchase_order.
  • Expecting the Mailhook to reply to the sender. It never does. Use a Send Email node to {{ input.replyTo }} when you need to acknowledge receipt.
  • Pointing a child's Subworkflow Input at variables the child cannot see. The child only receives the input object you pass it, not the parent's other step variables, so map every field the child needs explicitly.

Testing

Before publishing the address widely, test on a small scope. Send one real invoice PDF to the generated Mailhook address and open the run in execution history. Confirm the Attachment node returned a non-zero size, the pdf extract-text step produced readable text, and the classifier returned the expected category. Verify the correct child appears as its own execution-history entry and that the Slack summary posted. Then repeat with a purchase order and with an email that has no PDF to confirm the other branch behaves. Once all three paths route correctly, share the intake address with your team and add your From allowlist or Subject regex filters to keep stray mail out.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.