Service hold trigger
Service hold is the heaviest customer-facing action the platform takes. It blocks new orders, signals to the customer that we've stopped extending credit, and creates a sales-rep follow-up that has to happen fast.
The cascade is gated by Mike (the HITL gate) even when the AR aging cron triggers it. This is intentional: an automated hold would damage relationships in cases where there's context (customer on a payment plan, dispute pending) that the data doesn't reflect.
Risk level 4 (high). Reversal exists but the customer-facing notice is hard to un-send.
Trigger conditions
- AR aging tick detects a customer with
days_overdue > 90 AND total_open > 25000— chains here. - Mike manually triggers from /admin-dashboard.html when a phone call reveals trouble.
- Customer health prediction (R558) surfaces a high-risk customer with prior dispute history.
- Repeated invoice disputes from the same customer trigger an AR review that recommends hold.
Preconditions are deliberately loose (warn, not block): days_overdue >= 60 AND ar_balance >= 10000. The intent is to let Mike see the proposal even for borderline cases — the HITL gate is the real safety.
The 4 beats
-
01
Flag customer on hold
UPDATE
customers SET entitystatus='hold', last_modified=now WHERE id=?. The hub UI and Pages Functions read this field; new order forms refuse the customer. -
02
Notify sales rep
A draft email to the sales rep lists the AR balance, days overdue, prior holds, and recommended next call. Status=pending_review.
-
03
Draft customer-facing notice
A draft email to the customer explaining the hold and the path back (payment, payment plan, dispute resolution). Mike polishes before send.
-
04
NS customer credit_hold flag
Enqueue NS push to set
credit_hold=trueon the customer record. Blocks new SOs at the NS level.
What's different after the workflow runs
customers.entitystatus='hold'in D1.- NS customer record has
credit_hold=true. - Sales rep has a notification email awaiting approval.
- Customer has a draft notice awaiting Mike's polish.
auto_decisionsrecords this hold decision for next-time reflection.- reflexion_log carries a
service_hold,customer:?tagged row.
What can go wrong and how to recover
D1 has the hold but NS doesn't. Drainer cron retries within 15 min. Worst case: customer can place an order through a non-platform path (rare). Manual recovery via POST /admin/ns-push/retry.
If Mike approves the draft before reviewing, the customer might get a notice on a borderline case. Recovery is a follow-up email + relationship management. R560's atomic-claim prevents accidental bulk-approve here.
Standard path: dispute flows through customer_invoice_dispute, which can stage an ar_hold_review proposed action to lift the hold.
If prior_holds > 0, the proposal note flags it. Mike weighs the pattern when approving.
Adjacent workflows + diagrams
Code paths + invariants
| Concern | Where |
|---|---|
| Workflow contract | workflow_definitions WHERE workflow_type='service_hold_trigger' |
| D1 flag | customers.entitystatus='hold' |
| NS flag | customer.credit_hold=true |
| Decision audit | auto_decisions table |
| Reflexion tag | service_hold,customer:<id> |
| Risk level | 4 |
| Expected duration | ~15 min |
| Trigger | event · sources=ar_aging_tick_threshold, manual |