Inbound email triage risk 2 · lowComms & triagereflexion on

workflow_type: inbound_email_triage · owner: — · contract v2

Email arrives at one of the 5 mailboxes. Cascade: parse + classify + route to workflow, log to inbound_email_log, attempt entity resolution, draft response (HITL), surface in intake.html.

0 · Visual flow archify workflow graph (6 lanes: trigger → context → HITL → fan-out → post → verify)

Workflow flow
01 / Trigger 02 / Context + preconditions 03 / HITL gate (Mike approves) 04 / Fan-out cascade 05 / Post actions (log_run + reflexion + events) 06 / Verify (SQL / R2 / HTTP checks) How this workflow gets kicked off. Could be a chat-tool invocation, a cron tick, an inbound event (e.g. price.changed), or Mike clicking 'execute' from an admin page. TRIGGER kind: event sources: cf_email_routing invoker: Mike (single-admin) risk_level: 2i Manual Mike invokes risk 2 Before deciding anything, pull related data from D1 (the local mirror of NetSuite). Each query loads a slice of the entity's current state so the AI and Mike can review before any writes happen. LOAD CONTEXT (D1 queries before fan-out) sender_known: SELECT id, type FROM (SELECT id, 'customer' AS type FROM customers WHERE LOWER(… existing_thread: SELECT id, workflow_routed_to FROM inbound_email_log WHERE from_email=? AND sub…i Load context 2 D1 queries Safety checks that must pass before any writes. 'block' severity halts the run; 'warn' surfaces a warning but continues. Without these, a bad input could cascade into NetSuite. PRECONDITIONS (checked before fan-out) [block] valid_from: from_email LIKE '%@%' [warn] not_spam: sender_known IS NOT NULL OR is_known_domaini Preconditions 2 checks The HITL (Human-In-The-Loop) gate. The workflow stages a proposed_action and waits for Mike to approve in /proposed-actions.html. Only fires when risk_level >= 3. This is the invariant: no NS write happens without Mike's go-ahead. HITL GATE (Mike approves before fan-out) action_type: workflow_inbound_email_triage entity_ref: workflow:inbound_email_triage:run_<run_id> approver: mike (single-admin) risk_gate: >= 3 (this workflow = 2) approval window: typical <= 60 min envelope: proposed_actions row staged by runneri No HITL auto-execute (risk ≤ 2) low risk Write to the local D1 mirror (the read-side cache of NetSuite). Used for derived data and platform state. STUB today - per-tool d1_write logic lives in chat_tools/impls.ts. FAN-OUT #log -> D1: multiple kind: d1_write op: INSERT inbound_email_log status: REAL (src/lib/workflow_runner.ts)i log d1_write STUB Repeat the same fan-out for each item in a collection (recipients, aging buckets, sub-workflows). STUB today. FAN-OUT #route_to_workflow -> multiple (loop) kind: dispatch_workflow bid_amendment -> bid_amendment_arrives invoice_dispute -> customer_invoice_dispute vendor_cost -> vendor_cost_update spec_sheet -> new_assembly_item status: STUB (src/lib/workflow_runner.ts)i route_to_workflow dispatch_workflow STUB Draft an email and stage it as a proposed_action for Mike's review. The email is NOT sent - only drafted + queued. STUB today. FAN-OUT #draft_response -> email draft kind: hitl_email_draft if: classification != spam AND requires_response tool: propose_email_to_customer status: STUB (src/lib/workflow_runner.ts)i draft_response hitl_email_draft STUB Always-runs at the end of every workflow execution: writes a row to workflow_run_log with status, duration, step counts, and errors. Real implementation. POST ACTION: log_run -> D1: workflow_run_log fields: run_id, workflow_type, status, started_at, completed_at, summary_json source: runner automatic (always)i log_run workflow_run_log REAL Writes an entry to reflexion_log so the AI 'remembers' what happened. Only fires if the contract has reflexion_enabled=1. Future workflows can search this log for prior context. Real implementation. POST ACTION: reflexion -> D1: reflexion_log tags: inbound_email_triage reflexion_enabled: True fields: run_id, narrative, tags source: runner automatic (when reflexion_enabled=1)i reflexion reflexion_log REAL Fires a workflow.completed / workflow.partial / workflow.failed event into the event ledger so downstream subscriptions can react. Uses an idempotency_key so producer retries collapse. Real implementation (R564). POST ACTION: event -> Event ledger (recordEvent) types: workflow.completed | workflow.partial | workflow.failed idempotency_key per run_id source: runner automatici event workflow.completed REAL A post-execution sanity check. The runner stages this with status='pending'; the verify-scheduler cron (every :08 and :38 hourly) wakes up after the configured window (e.g. +24h) and executes the sql_check, then flips status to pass/fail/timeout. Real implementation (R564). VERIFY: email_logged window: 2s sql_check: SELECT id FROM inbound_email_log WHERE email_uuid=? scheduler: verify cron @ :08/:38 (R563) result row: workflow_verify_resultsi email_logged ≤60m A post-execution sanity check. The runner stages this with status='pending'; the verify-scheduler cron (every :08 and :38 hourly) wakes up after the configured window (e.g. +24h) and executes the sql_check, then flips status to pass/fail/timeout. Real implementation (R564). VERIFY: routed window: 5s sql_check: SELECT workflow_routed_to FROM inbound_email_log WHERE email_uuid=? scheduler: verify cron @ :08/:38 (R563) result row: workflow_verify_resultsi routed ≤60m Legend User UI Agent logic Policy Tool action Context / trace

1 · Trigger FIRES WHEN…

kind
event
sources
["cf_email_routing"]

2 · Inputs required

namerequiredtype / hint
email_uuidrequired
from_emailrequired
to_addressrequired
subjectrequired
body_textrequired
attachments[]optional

3 · Context loaded D1 queries run before fan-out

name
sender_known
tables
customers · vendors
SELECT
  id, type
  FROM (SELECT id, 'customer' AS type
  FROM customers
  WHERE LOWER(email)=LOWER(?)
  UNION SELECT id, 'vendor' AS type
  FROM vendors
  WHERE LOWER(email)=LOWER(?))
name
existing_thread
tables
inbound_email_log
SELECT
  id, workflow_routed_to
  FROM inbound_email_log
  WHERE from_email=?
  AND subject LIKE ?
  ORDER BY received_at DESC
  LIMIT 5

4 · Preconditions checked before any fan-out

checkruleseverity
valid_fromfrom_email LIKE '%@%'block
not_spamsender_known IS NOT NULL OR is_known_domainwarn

5 · HITL gate

Risk level 2 < 3 — no HITL gate. Fan-out runs immediately after preconditions pass.

6 · Fan-out targets 3 total · 0 real · 3 stub

#1log d1_write STUB

operations
["INSERT inbound_email_log"]
stub — not yet implemented in src/lib/workflow_runner.ts (kind d1_write hits the placeholder branch at line ~340 and emits step status 'stub'). Documented intent only.

#2route_to_workflow dispatch_workflow STUB

mapping
{"bid_amendment":"bid_amendment_arrives","invoice_dispute":"customer_invoice_dispute","vendor_cost":"vendor_cost_update","spec_sheet":"new_assembly_item"}
stub — not yet implemented in src/lib/workflow_runner.ts (kind dispatch_workflow hits the placeholder branch at line ~340 and emits step status 'stub'). Documented intent only.

#3draft_response hitl_email_draft STUB

tool
propose_email_to_customer
condition (if)
classification != spam AND requires_response
stub — not yet implemented in src/lib/workflow_runner.ts (kind hitl_email_draft hits the placeholder branch at line ~340 and emits step status 'stub'). Documented intent only.

7 · Post actions declared + runner-automatic

idactionsource
runner_log_runINSERT into workflow_run_log (run_id, workflow_type, status, started_at, completed_at, summary_json)runner automatic
runner_reflexionINSERT into reflexion_log (tags=inbound_email_triage, run_id, narrative)runner automatic (reflexion_enabled=1)
log_runINSERT workflow_runsdeclared in contract
reflexionINSERT reflexion_log with tags=email_triage,classification:?declared in contract

8 · Verify checks written to workflow_verify_results (pending — verify cron not yet wired)

name
email_logged
SELECT
  id
  FROM inbound_email_log
  WHERE email_uuid=?
name
routed
SELECT
  workflow_routed_to
  FROM inbound_email_log
  WHERE email_uuid=?

9 · Retry policy

max_attempts
3
backoff
exponential
base_ms
1000
max_ms
15000
alert_on_final_failure
true

10 · What changes when this workflow runs aggregated side effects

systemtable / resourceactionstatussource
D1workflow_run_logINSERT (run summary)REALrunner automatic
D1reflexion_logINSERT (tags=inbound_email_triage)REALrunner automatic
Eventworkflow.completed (or workflow.failed)fireREALrunner automatic
D1workflow_verify_resultsINSERT pending × 2REALrunner verify staging
D1unknownwriteSTUBfan-out #1 (log)
Cloudflare Workflowunknowninvoke instanceSTUBfan-out #2 (route_to_workflow)
D1proposed_actionsINSERT (email draft via propose_email_to_customer)STUBfan-out #3 (draft_response)