Wiki · Workflow companion

Inbound email triage

Email arrives at one of the 5 GFS mailboxes (bids@, ar@, vendors@, specs@, general@). Classify intent, extract entities, route to the right downstream workflow, log everything, and stage a draft response for Mike's review.

Stub · Contract present, runner stub-only
What this is

Inbound email triage

Inbound email triage is the front door for the platform. Every email that hits one of the five Cloudflare Email Routing mailboxes flows through here first. Without this workflow, the other workflows wouldn't know they had work to do.

The cascade is deliberately small: classify, extract entities, route, log, draft response. Each step is cheap and read-only. The real work happens downstream in the workflow this dispatches to (bid_amendment_arrives, customer_invoice_dispute, vendor_cost_update, etc).

Risk level 2 (low). Misclassification is recoverable — Mike sees every triaged email on /intake.html and can re-route.

When to use it

Trigger conditions

Heuristic

Unknown senders get severity=warn on the not_spam check, not blocked. Mike sees them on intake.html and decides — this catches new vendors and new customers that haven't been added yet.

Step-by-step what happens

The 3 beats

  1. 01

    Log inbound email

    INSERT into inbound_email_log with email_uuid, from, to, subject, body, classification, extracted_entities, and the workflow_routed_to target.

    Writes inbound_email_log
    Time ~80ms
    Kind d1_write
    Status stub
  2. 02

    Route to downstream workflow

    Based on classification, dispatch: bid_amendment → bid_amendment_arrives; invoice_dispute → customer_invoice_dispute; vendor_cost → vendor_cost_update; spec_sheet → new_assembly_item. Other classifications surface for manual handling.

    Writes workflow_runs (downstream)
    Time ~50ms dispatch + downstream
    Kind dispatch_workflow
    Status stub
  3. 03

    Draft response (conditional)

    If classification is not spam and the email requires a response, AI drafts via propose_email_to_customer. Mike approves before send.

    Writes outbound_email_log
    Time ~3s
    Kind hitl_email_draft
    Status stub
    Note conditional · requires_response
Outcomes

What's different after the workflow runs

Log row
Persisted
≤ 2s
Routed
Yes
≤ 5s downstream
Draft response
Conditional
requires_response
Surface on intake
Yes
real-time
Failure modes

What can go wrong and how to recover

Misclassification

Mike re-classifies on /intake.html. The original log row keeps the AI's classification + Mike's correction for next-time training.

CF Email Routing webhook fails

Email gets bounced back to sender by CF. No log row exists. Mike's recovery path is forwarding the email to a manual intake endpoint.

Unknown sender

Logged with severity=warn. Surfaces on intake.html for manual triage — most often a new vendor that needs adding via new_ingredient_vendor_onboard.

Spam / no-action email

Logged with classification=spam, no downstream dispatch, no response draft. Counts toward weekly anomaly trend if volumes spike.

Related

Adjacent workflows + diagrams

For developers

Code paths + invariants

ConcernWhere
Workflow contractworkflow_definitions WHERE workflow_type='inbound_email_triage'
Routing sourceCloudflare Email Routing → src/email.ts
ClassifierAI prompt in src/email.ts → classify_intent
Log tableinbound_email_log
5 mailboxesbids@, ar@, vendors@, specs@, general@
Reflexion tagemail_triage,classification:<cls>
Risk level2
Expected duration~5 min
Triggerevent · sources=cf_email_routing
// Dispatch map (R550) const dispatch = { bid_amendment: 'bid_amendment_arrives', invoice_dispute: 'customer_invoice_dispute', vendor_cost: 'vendor_cost_update', spec_sheet: 'new_assembly_item', }; await runner.invoke(dispatch[classification], { email_uuid });
Changelog

Dated trail · spot stale claims

Dated trail of when this doc was last touched, what changed, and what to look at if it feels stale.

DateRoundChangeTouched by
2026-05-26R586Added CHANGELOG · SCHEMA · RUNBOOK · BACKLOG sections — wiki became best-in-class operating documentation.Mike + Claude
2026-05-25R584/R585Wiki originally shipped — 8-section structure (hero / what / when / steps / outcomes / failure-modes / related / for-developers).Mike + Claude
If today is more than 60 days past the latest changelog row, treat live system behavior as the source of truth. The doc may have drifted — verify against the workflow contract in workflow_definitions WHERE workflow_type='inbound_email_triage' before acting on these claims.
Schema · data contract

The machine-readable spec

Canonical fields, table names, endpoint signatures. What code should match, what tests should assert. workflow_type · inbound_email_triage · risk_level · 2.

Inputs (required + optional)

FieldTypeDescription
email_idintegerinbound_email_log.id. Required.
from_emailstringSender email. Required.
subjectstringRequired.
body_markdownstringConverted body. Required.

D1 tables written

TableOperationTrigger
inbound_email_logUPDATE (classified_intent, action_taken)Classifier result
proposed_actionsINSERT (if route triggers a workflow)e.g. cost update from vendor email
eventsINSERT (email.classified)audit

Endpoints called

MethodPathPurpose
Email Routingincoming@globalfoodsolutions.co → src/email.tsAll 5 mailboxes route here
POST/api/email/reclassify/:idManual reclassification

Events fired

event_typeWhenSubscribers
email.classifiedPost-classificationaudit
email.routedPost-routing to a workflowdownstream workflow
Runbook · when it breaks

It broke at 2am — what now

Different from "how do I use this." This is the page Mike pulls up when something is wrong: logs to check, recovery steps, who to escalate to.

Scenario · Email classified wrong (vendor cost as 'general')

Classifier prompt didn't recognize the pattern.

  1. Reclassify: POST /api/email/reclassify/<id> with the correct intent.
  2. Improve prompt: Add the email as a test case in eval/golden-set/email-classification.yaml.
  3. Verify: Re-run the classifier; should now route correctly.

Scenario · Email never appeared in inbound_email_log

Email Routing didn't trigger or the email worker threw before logging.

  1. Check CF Email Routing: Dashboard → Email Routing → recent deliveries
  2. Worker logs: npx wrangler tail during the next send
  3. Verify route: All 5 mailboxes must route to the worker.

Scenario · Attachment didn't get converted to markdown

document_converter failed on the attachment.

  1. Check: SELECT error FROM document_conversions WHERE email_id=<id>
  2. Manual transcribe: Mike reads the attachment and enters key fields.
  3. Improve: Add attachment type to converter test set.

Logs to check

Kill switch · emergency stop

If this workflow is misbehaving in a high-impact way (creating bad proposed_actions in volume, pushing wrong things to NS), flip a kill switch:

See kill-switches-state-machine.html for the full state machine + recovery procedure.

Escalation

Primary: Mike Levine (single-admin) · mikelevine@globalfoodsolutions.co. For prolonged outage during business hours, notify warehouse lead + accounting lead so they can defer dependent work.

Backlog · open questions

What's not done · what's uncertain

What's not done, what's uncertain, what we punted. Captured so it survives context switches and doesn't die in someone's head.