Wiki · Workflow companion

AR aging action plan

Collections, automated. This is the dunning trigger for past-due invoices in the SO lifecycle. Every weekday morning, the platform groups open invoices by 30/60/90/120-day bucket, proposes a per-tier action plan, and queues drafted dunning emails for Mike's approval.

Real · Daily collections
What this is

The collections morning routine, formalized

Before this workflow, AR collections meant Mike opened the NetSuite AR aging report, mentally sorted customers into "need to call" / "send statement" / "escalate", drafted a few emails, and lost the rest of the morning. The AR aging action plan formalizes that into a daily cron that does the grouping, drafts the emails, and surfaces the priority queue.

Contract: workflow_definitions WHERE workflow_type='ar_aging_action_plan' — trigger is cron (weekdays 7am ET), risk level 3, expected duration 30 minutes including two HITL gates.

The defining property: lagging indicator. AR aging is what happens after a customer disengages. The proper leading-indicator pairing is the customer health substrate. This workflow handles the "already in the bucket" cases; customer health handles the "about to fall in" prediction.

When to use it

Trigger conditions

Bucket model

The four tiers and their actions

BucketDays past dueDefault actionTone
Current0–29No action — just monitor
30-day30–59Friendly statement email with PDF"just a reminder"
60-day60–89Direct collection email + flag in customer_health"this is now overdue"
90-day90–119Hold further orders, escalate to Mike for phone call"we need to talk"
120+ day120+Credit memo review + collections escalation + legal flagred zone — Mike personal touch
Per-customer override

Strategic accounts (Driscoll, top-10 by revenue) get softer default actions — the workflow respects customers.collection_tone_override. Cardinal at 90+ gets a "we need to talk" treatment; Driscoll at 90+ gets a polite reminder while Mike phones the AP contact directly.

Worked example

Cardinal $5K invoice goes 90+

Scenario

Tuesday 7am. The cron fires. Cardinal Foods has invoice INV-2026-0341 for $5,142.18 dated Feb 19, 2026 — that's 96 days past due, landing in the 90-day bucket. Cardinal's total open AR is $14,820 across three invoices, two of which are in 30-day. No collection_tone_override is set; standard treatment applies.

The workflow drafts: (1) a phone-call prompt for Mike with talking points and AP contact info, (2) an order hold flag on Cardinal pending payment, (3) a polite-but-firm escalation email referencing all three invoices with PDFs attached. Three rows land in proposed_actions. Mike approves the phone-call task and the email, defers the order hold pending the call. By 9am, the email is out, Cardinal's AP contact replies by 11am, and a check is in the mail by Thursday.

Step-by-step what happens

The four beats

  1. 01

    Aging snapshot (auto)

    get_ar_aging_summary queries ar_aging (refreshed nightly from NS), buckets by 30/60/90/120, and computes per-customer totals. The result is materialized into ar_action_snapshots for the day so the rest of the workflow operates against a stable read.

    Reads ar_aging, customers, invoices
    Writes ar_action_snapshots
    Time ~400ms
  2. 02

    Prioritize + draft (auto + HITL)

    Each customer in the snapshot gets a recommended action based on its highest bucket. Strategic-account overrides apply. The AI drafts the corresponding email/phone-prompt with full context — invoices, dates, amounts, last contact attempt. Drafts land in proposed_actions with action_type='collection_email' or 'phone_prompt'.

    Writes proposed_actions
    HITL step 2
  3. 03

    Mike reviews + approves (HITL)

    The morning queue is sorted by bucket severity (120 → 90 → 60 → 30). Mike can bulk-approve all 30-day reminders (low risk) and single-decide the 90/120 cases. R560 atomic claim prevents double-fire.

    Reads proposed_actions filtered by daily action plan
    HITL step 3 (per-action)
  4. 04

    Send + record (HITL approve → auto execute)

    Approved emails go via Email Routing. Phone-prompt rows land on Mike's task list. Order holds (if approved) write to customer_holds. Events ar.collection_sent and ar.hold_placed are emitted so customer health can react.

    Writes outbound_email_log, customer_holds, events
    Emits ar.collection_sent, ar.hold_placed
Outcomes

What's different after the workflow runs

30-day bucket
Notified
friendly reminder sent
60-day bucket
Escalated
direct collection email
90+ bucket
Hold + call
Mike on the phone
Snapshot
Daily
historical trail
Failure modes

What can go wrong and how to recover

Stale aging data

If the overnight NS sync of ar_aging didn't run, the workflow uses yesterday's data and tags the snapshot stale=true. Daily digest highlights this; manual refresh: POST /admin/sync/tier?tier=hot.

Payment-in-flight not credited

A customer may have wired payment that hasn't posted yet. Mike has a "skip — payment expected" option per row; the workflow records the deferral in customer_payments.expected_at and re-checks next cron.

Wrong contact email

If the customer's AP contact has stale email, the bounce lands in inbound triage. The customer health watcher picks up email.bounced and lowers the score. Fix in NS customer record; sync picks it up.

Related

Adjacent workflows + diagrams

For developers

Code paths + invariants

ConcernWhere
Workflow contractworkflow_definitions WHERE workflow_type='ar_aging_action_plan'
Cron schedulewrangler.toml — weekday 7am ET
Aging querysrc/chat_tools/impls.ts get_ar_aging_summary
Action proposersrc/chat_tools/impls.ts propose_collection_action
Strategic overridescustomers.collection_tone_override column
Daily snapshotar_action_snapshots — historical reproducibility
HITL invariantADR-031 — never send dunning email without approval
Event emissionar.collection_sent, ar.hold_placed, ar.bucket_moved
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='ar_aging_action_plan' 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 · ar_aging_action_plan · risk_level · 3.

Inputs (required + optional)

FieldTypeDescription
run_datedateWhen the aging snapshot was taken. Required.
aging_bucketsjson{'1-30':n, '31-60':n, '61-90':n, '90+':n}
threshold_total_opendecimalMin open balance to qualify; default $5000.
threshold_days_overdueintegerMin days overdue; default 30.

D1 tables written

TableOperationTrigger
proposed_actionsINSERT (one per tier)draft_per_tier loop_over_aging fan-out
customer_program_proposalsINSERTpause_on_hold_check flag
outbound_email_logINSERT (status=pending_review)sales_rep_notify hitl_email_draft
workflow_run_logINSERTrunner automatic
reflexion_logINSERTrunner automatic (reflexion_enabled=1)
ar_aging_summaryINSERTdaily_digest declared action

Endpoints called

MethodPathPurpose
POST/api/workflow/executeworkflow_type=ar_aging_action_plan
GET/api/ar-aging/snapshotLive aging snapshot
POST/api/proposed-actions/:id/decideMike approves per-tier drafts

Events fired

event_typeWhenSubscribers
ar.aging.scannedPost-runcustomer_health
customer.service_hold.proposedWhen pause_on_hold_check firesservice_hold_trigger
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 · Same customer flagged every day for 30 days

Customer not paying, but no escalation — the workflow only proposes a hold at 90+ days AND $25K open.

  1. Check thresholds: SELECT days_overdue, total_open FROM customer_aging WHERE customer_id=<id>
  2. Manual escalate: Mike calls; or stage a propose_customer_program with type=service_hold manually.
  3. Long-term: Backlog item — tiered escalation (30 → email, 60 → call, 90 → hold).

Scenario · Service hold proposed, Mike approved, customer still received new SO

Service hold isn't enforced at SO creation time — it's only a flag on customer_program_proposals.

  1. Verify hold is set: SELECT * FROM customer_programs WHERE customer_id=<id> AND program_type='service_hold' AND status='active'
  2. Patch SO creation: Add a pre-check in propose_sales_order that blocks if customer has active service_hold.
  3. Immediate fix: Cancel the SO manually in NS, refund deposit.

Scenario · AR aging cron didn't run — no proposed_actions today

Cron sweep failed or sync was stale.

  1. Preconditions: SELECT MAX(completed_at) FROM sync_log WHERE sync_type='scheduled_hot_customer_payments' — must be < 2 hours old.
  2. Manual trigger: POST /api/workflow/execute with workflow_type=ar_aging_action_plan.
  3. Investigate: npx wrangler tail while triggering; look for sync errors.

Scenario · Mike approved a dunning draft but customer never got the email

outbound_email_log might have status=approved but sent_at is null.

  1. Check: SELECT id, status, approved_at, sent_at, error FROM outbound_email_log WHERE entity_ref LIKE 'ar:%' ORDER BY created_at DESC LIMIT 5
  2. Recover: Re-trigger send via POST /admin/email/<id>/resend

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.