Sales inbox: qualify and route
Poll the sales mailbox, classify each email as lead / existing customer / vendor, create Iris records for new leads and forward the rest to the right team.
Drag to pan, pinch to zoom. This is the real graph the template installs.
Purpose
Poll the sales mailbox, classify each email as lead / existing customer / vendor, create Iris records for new leads and forward the rest to the right team.
How it works
What it does
- Every 10 minutes fetch the next unread sales email.
- Classify into
new_lead | existing_customer | vendor | other. - new_lead → map → create an Iris record (
entity_type: leads); anything else is forwarded to the account team with the label in the subject.
Nodes 7
- 01Every 10 minutestrigger.scheduleTrigger
- 02Fetch sales inboxmail.fetchDestination
- 03Lead, customer or vendor?ai.classifyAI
- 04Routecontrol.branchLogic
- 05Lead recorddata.mapData
- 06Create CRM leadhttp.requestHTTP
- 07Forward to account teammail.sendDestination
View template JSON
{
"format": "mantle-workflow/v1",
"id": "sales-inbox-lead-router",
"name": "Sales inbox: qualify and route",
"graph": {
"nodes": [
{
"id": "schedule",
"type": "trigger.schedule",
"label": "Every 10 minutes",
"config": {
"interval_sec": 600,
"timezone": "UTC"
},
"worker_type": "generic-worker",
"position": {
"x": 60,
"y": 220
}
},
{
"id": "gmail_fetch",
"type": "mail.fetch",
"label": "Fetch sales inbox",
"config": {
"label": "inbox",
"unread_only": true,
"max_scan": 20,
"mark_read": true
},
"worker_type": "generic-worker",
"position": {
"x": 380,
"y": 220
}
},
{
"id": "ai_classify",
"type": "ai.classify",
"label": "Lead, customer or vendor?",
"config": {
"model": "claude-haiku-4-5",
"labels": [
"new_lead",
"existing_customer",
"vendor",
"other"
],
"text": "From: {{ steps.gmail_fetch.from }}\nSubject: {{ steps.gmail_fetch.subject }}\n\n{{ steps.gmail_fetch.text }}",
"temperature": 0
},
"worker_type": "ai-worker",
"position": {
"x": 700,
"y": 220
}
},
{
"id": "branch_route",
"type": "control.branch",
"label": "Route",
"config": {
"mode": "first_match"
},
"worker_type": "generic-worker",
"position": {
"x": 1020,
"y": 220
}
},
{
"id": "map_lead",
"type": "data.map",
"label": "Lead record",
"config": {
"mappings": {
"email": "steps.gmail_fetch.from",
"subject": "steps.gmail_fetch.subject",
"notes": "steps.gmail_fetch.snippet",
"leadSource": "'sales-inbox'"
}
},
"worker_type": "generic-worker",
"position": {
"x": 1340,
"y": 30
}
},
{
"id": "create_lead",
"type": "http.request",
"label": "Create CRM lead",
"config": {
"method": "POST",
"url": "https://crm.example.com/api/leads",
"json": {}
},
"worker_type": "generic-worker",
"position": {
"x": 1660,
"y": 30
}
},
{
"id": "send_forward",
"type": "mail.send",
"label": "Forward to account team",
"config": {
"to": "accounts@example.com",
"subject": "FWD: {{ steps.gmail_fetch.subject }} ({{ steps.ai_classify.response.label }})",
"body_text": "From: {{ steps.gmail_fetch.from }}\n\n{{ steps.gmail_fetch.text }}"
},
"worker_type": "generic-worker",
"position": {
"x": 1340,
"y": 410
}
}
],
"edges": [
{
"from": "schedule",
"to": "gmail_fetch"
},
{
"from": "gmail_fetch",
"to": "ai_classify"
},
{
"from": "ai_classify",
"to": "branch_route"
},
{
"from": "branch_route",
"to": "map_lead",
"branch": "true",
"condition": {
"match": "all",
"rules": [
{
"left": "response.label",
"op": "equals",
"right": "new_lead"
}
]
},
"label": "new lead"
},
{
"from": "map_lead",
"to": "create_lead"
},
{
"from": "branch_route",
"to": "send_forward",
"branch": "false",
"label": "everything else",
"priority": 1
}
]
}
}Requirements
The connections and permissions each step needs, from the node contracts in the catalog.
- GmailOAuth 2.0
Used by Fetch sales inbox.
Credential: {"client_id", "client_secret", "refresh_token"} or {"username", "password"}
- Downstream edges should gate on found == true so an empty tick is a clean no-op.
- has_attachment is applied to the parsed message on IMAP — there is no portable SEARCH term for it — so it costs a fetch per scanned message.
- A raw `query` replaces unread_only too. Without an is:unread / UNSEEN / isRead eq false term of its own, a poll re-matches the same message forever.
- after / before are YYYY/MM/DD for Gmail and IMAP (converted by to_imap_date), but Outlook filters receivedDateTime in OData and needs an ISO-8601 instant.
- AnthropicAPI key
Used by Lead, customer or vendor?.
Credential: {"api_key"}
- The provider adapter is chosen from the connection; `model` must belong to it.
- SendGridAPI key
Used by Forward to account team.
Credential: {"api_key"} or {"username", "password"}
Setup
- Gmail connection on Fetch sales inbox; AI connection on the classifier; Iris connection on Create Iris lead; SendGrid/SMTP on Forward to account team.
- Change
entity_typeand the mapping to your Iris schema.
Sample input and output
Synthetic examples: node-contract samples or placeholders, never customer data.
Input
Runs every 10 min (UTC). There is no incoming payload: the first step fetches the data.
Output
Create CRM lead http.request
{
"body": null,
"headers": {},
"status_code": 0
}Forward to account team mail.send
{
"body": null,
"message_id": "",
"provider": "",
"sent": true,
"status_code": 0,
"thread_id": ""
}Failure handling
What the graph does when a step fails, read from its retry settings, error branches and error policies.
- A step that fails, after any retries, fails the run. The failed step is kept with its input and error.
See it run, then make it yours.
Templates install as drafts. Edit any step, and Workflow Health re-checks the graph on every save.