aiintermediate15 min setupby Mantle

Inbox triage: classify and route email

Every 15 minutes, fetch the next unread email, classify it (urgent / billing / general) and route urgent ones to the on-call inbox with an auto-acknowledgement.

Every 15 minutesFetch next unreaClassify emailUrgent?Escalate to on-cAuto-acknowledge

Drag to pan, pinch to zoom. This is the real graph the template installs.

Purpose

Every 15 minutes, fetch the next unread email, classify it (urgent / billing / general) and route urgent ones to the on-call inbox with an auto-acknowledgement.

How it works

What it does

  • Runs on a 15-minute schedule, pulls the next unread inbox message via Gmail and marks it read.
  • ai.classify picks exactly one of urgent | billing | general (deterministic, temperature 0).
  • The Urgent? branch escalates urgent mail to the on-call address; everything else gets a polite auto-acknowledgement.

Nodes 6

  1. 01Every 15 minutestrigger.scheduleTrigger
  2. 02Fetch next unread emailmail.fetchDestination
  3. 03Classify emailai.classifyAI
  4. 04Urgent?control.branchLogic
  5. 05Escalate to on-callmail.sendDestination
  6. 06Auto-acknowledgemail.sendDestination
View template JSON
{
  "format": "mantle-workflow/v1",
  "id": "ai-email-triage",
  "name": "Inbox triage: classify and route email",
  "graph": {
    "nodes": [
      {
        "id": "schedule",
        "type": "trigger.schedule",
        "label": "Every 15 minutes",
        "config": {
          "interval_sec": 900,
          "timezone": "UTC"
        },
        "worker_type": "generic-worker",
        "position": {
          "x": 60,
          "y": 220
        }
      },
      {
        "id": "gmail_fetch",
        "type": "mail.fetch",
        "label": "Fetch next unread email",
        "config": {
          "label": "inbox",
          "unread_only": true,
          "max_scan": 10,
          "mark_read": true
        },
        "worker_type": "generic-worker",
        "position": {
          "x": 380,
          "y": 220
        }
      },
      {
        "id": "ai_classify",
        "type": "ai.classify",
        "label": "Classify email",
        "config": {
          "model": "claude-haiku-4-5",
          "labels": [
            "urgent",
            "billing",
            "general"
          ],
          "text": "Subject: {{ steps.gmail_fetch.subject }}\nFrom: {{ steps.gmail_fetch.from }}\n\n{{ steps.gmail_fetch.text }}",
          "temperature": 0
        },
        "worker_type": "ai-worker",
        "position": {
          "x": 700,
          "y": 220
        }
      },
      {
        "id": "branch_urgent",
        "type": "control.branch",
        "label": "Urgent?",
        "config": {
          "mode": "first_match"
        },
        "worker_type": "generic-worker",
        "position": {
          "x": 1020,
          "y": 220
        }
      },
      {
        "id": "send_escalation",
        "type": "mail.send",
        "label": "Escalate to on-call",
        "config": {
          "to": "oncall@example.com",
          "subject": "[URGENT] {{ steps.gmail_fetch.subject }}",
          "body_text": "From: {{ steps.gmail_fetch.from }}\n\n{{ steps.gmail_fetch.text }}"
        },
        "worker_type": "generic-worker",
        "position": {
          "x": 1340,
          "y": 30
        }
      },
      {
        "id": "send_ack",
        "type": "mail.send",
        "label": "Auto-acknowledge",
        "config": {
          "to": "{{ steps.gmail_fetch.from }}",
          "subject": "Re: {{ steps.gmail_fetch.subject }}",
          "body_text": "Thanks — we received your message and will reply within one business day."
        },
        "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_urgent"
      },
      {
        "from": "branch_urgent",
        "to": "send_escalation",
        "branch": "true",
        "condition": {
          "match": "all",
          "rules": [
            {
              "left": "response.label",
              "op": "equals",
              "right": "urgent"
            }
          ]
        },
        "label": "urgent"
      },
      {
        "from": "branch_urgent",
        "to": "send_ack",
        "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 next unread email.

    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 Classify email.

    Credential: {"api_key"}

    • The provider adapter is chosen from the connection; `model` must belong to it.
  • SendGridAPI key

    Used by Escalate to on-call, Auto-acknowledge.

    Credential: {"api_key"} or {"username", "password"}

Setup

  • Choose the Gmail connection on Fetch next unread email (adjust label / query to narrow the mailbox).
  • Choose an AI provider connection on Classify email; edit the label set to match your queues.
  • Set the on-call address and a SendGrid / SMTP connection on both send nodes.
  • Publish — the scheduler registers the interval automatically.

Sample input and output

Synthetic examples: node-contract samples or placeholders, never customer data.

Input

Runs every 15 min (UTC). There is no incoming payload: the first step fetches the data.

Output

Escalate to on-call mail.send

{
  "body": null,
  "message_id": "",
  "provider": "",
  "sent": true,
  "status_code": 0,
  "thread_id": ""
}

Auto-acknowledge 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.