Bulk customer notification risk 4 · highComms & triagereflexion on

workflow_type: batch_notify · owner: — · contract v2

Send templated email to top-N customers

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: hitl_approval event: proposed_actions approved with action_type=bulk_customer_email invoker: Mike (single-admin) risk_level: 4i Manual Mike invokes risk 4 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) recipients: SELECT id, companyname, email FROM customers WHERE __filter__ AND isinactive !=… prior_sends: SELECT customer_id, MAX(sent_at) FROM outbound_email_log WHERE related_workflow…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] recipients_cap: recipients.length <= COALESCE(max_recipients, 50) [warn] no_recent_spam: recipients have NOT been emailed in last 48 hours [block] kill_switch: NOT kill:high_risk_opsi Preconditions 3 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_batch_notify entity_ref: workflow:batch_notify:run_<run_id> approver: mike (single-admin) risk_gate: >= 3 (this workflow = 4) approval window: typical <= 60 min envelope: proposed_actions row staged by runneri Mike approves stage_proposed_action risk ≥ 3 gate Repeat the same fan-out for each item in a collection (recipients, aging buckets, sub-workflows). STUB today. FAN-OUT: per_recipient_persona… kind: loop_over_recipients (not in contract — diagram-only annotation)i per_recipient_persona… loop_over_recipients STUB Stage a review item for Mike's HITL approval (similar to stage_proposed_action but for review-only items, not write proposals). STUB today. FAN-OUT #hitl_per_recipient -> hitl_review: review kind: hitl_review status: STUB (src/lib/workflow_runner.ts)i hitl_per_recipient hitl_review STUB Send an outbound email (transactional notice, alert, or recipient blast). STUB today - the runner stages intent but doesn't deliver yet. FAN-OUT #send -> email: recipient kind: send_email status: REAL (src/lib/workflow_runner.ts)i send send_email 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: batch_notify 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: all_recipients_logged window: 60s sql_check: SELECT COUNT(DISTINCT customer_id) FROM outbound_email_log WHERE workflow_run_i… scheduler: verify cron @ :08/:38 (R563) result row: workflow_verify_resultsi all_recipients_logged ≤60m Legend User UI Agent logic Policy Tool action Context / trace

1 · Trigger FIRES WHEN…

kind
hitl_approval
event
"proposed_actions approved with action_type=bulk_customer_email"

2 · Inputs required

namerequiredtype / hint
customer_cohort_filterrequired
subjectrequired
body_templaterequired
attachments[]optional
max_recipientsoptional

3 · Context loaded D1 queries run before fan-out

name
recipients
tables
customers
SELECT
  id, companyname, email
  FROM customers
  WHERE __filter__
  AND isinactive != 'T'
  AND email IS NOT NULL
name
prior_sends
tables
outbound_email_log
SELECT
  customer_id, MAX(sent_at)
  FROM outbound_email_log
  WHERE related_workflow='batch_notify'
  GROUP BY customer_id

4 · Preconditions checked before any fan-out

checkruleseverity
recipients_caprecipients.length <= COALESCE(max_recipients, 50)block
no_recent_spamrecipients have NOT been emailed in last 48 hourswarn
kill_switchNOT kill:high_risk_opsblock

5 · HITL gate

Risk level 4 ≥ 3 — runner stages a proposed_actions row before fan-out runs. Mike must approve in proposed-actions.html before any side-effect step executes (real or stub).

action_type
workflow_batch_notify (proposal envelope)
entity_ref
workflow:batch_notify:run_<run_id>
approver
mike (single-admin)

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

#1per_recipient_personalize loop_over_recipients STUB

loop kind
loop_over_recipients
action
stage proposed_action per customer with personalized body
stub — not yet implemented in src/lib/workflow_runner.ts (kind loop_over_recipients hits the placeholder branch at line ~340 and emits step status 'stub'). Documented intent only.

#2hitl_per_recipient hitl_review STUB

description
Mike reviews per-recipient (or accepts cohort-wide approval)
stub — not yet implemented in src/lib/workflow_runner.ts (kind hitl_review hits the placeholder branch at line ~340 and emits step status 'stub'). Documented intent only.

#3send send_email STUB

idempotency_key
workflow_id+customer_id
circuit_breaker
abort_after_5_consecutive_failures
stub — not yet implemented in src/lib/workflow_runner.ts (kind send_email 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=batch_notify, run_id, narrative)runner automatic (reflexion_enabled=1)
log_runINSERT workflow_runsdeclared in contract
reflexionINSERT reflexion_log with tags=batch_notifydeclared in contract

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

name
all_recipients_logged
SELECT
  COUNT(DISTINCT customer_id)
  FROM outbound_email_log
  WHERE workflow_run_id=?

9 · Retry policy

max_attempts
3
backoff
exponential
base_ms
2000
max_ms
30000
alert_on_final_failure
true
per_recipient
true

10 · What changes when this workflow runs aggregated side effects

systemtable / resourceactionstatussource
D1workflow_run_logINSERT (run summary)REALrunner automatic
D1reflexion_logINSERT (tags=batch_notify)REALrunner automatic
Eventworkflow.completed (or workflow.failed)fireREALrunner automatic
D1workflow_verify_resultsINSERT pending × 1REALrunner verify staging
D1proposed_actionsINSERT (HITL gate envelope)REALrunner HITL gate
Looprecipientsiterate inner stepsSTUBfan-out #1 (per_recipient_personalize)
D1proposed_actionsINSERT (hitl_review)STUBfan-out #2 (hitl_per_recipient)
Email RoutingrecipientsendSTUBfan-out #3 (send)