Wiki · Workflow companion

AR aging action plan

Collections, automated. 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