Wiki · Workflow companion

Service hold trigger

A customer crosses both AR-balance and days-overdue thresholds. The cascade flags the customer in D1, notifies the sales rep, drafts a customer-facing notice, and pushes a credit_hold flag to NS so new orders can't open.

Stub · Contract present, runner stub-only
What this is

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.

When to use it

Trigger conditions

Heuristic

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.

Step-by-step what happens

The 4 beats

  1. 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.

    Writes customers
    Time ~50ms
    Kind d1_write
    Status stub
  2. 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.

    Writes outbound_email_log
    Time ~80ms
    Kind hitl_email_draft
    Status stub
    Note target · sales_rep
  3. 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.

    Writes outbound_email_log
    Time ~80ms
    Kind hitl_email_draft
    Status stub
    Note target · customer
  4. 04

    NS customer credit_hold flag

    Enqueue NS push to set credit_hold=true on the customer record. Blocks new SOs at the NS level.

    Writes NetSuite customer record
    Time ~3–8s
    Kind ns_push
    Status stub
    Note retry · 3 attempts exponential
Outcomes

What's different after the workflow runs

D1 hold flag
Set
≤ 5s
NS hold flag
Set
≤ 15 min
Sales rep
Notified
draft waiting
Customer notice
Drafted
awaiting polish
Failure modes

What can go wrong and how to recover

NS push fails after 3 retries

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.

Customer-facing notice sent prematurely

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.

Customer disputes the hold

Standard path: dispute flows through customer_invoice_dispute, which can stage an ar_hold_review proposed action to lift the hold.

Prior hold history

If prior_holds > 0, the proposal note flags it. Mike weighs the pattern when approving.

Related

Adjacent workflows + diagrams

For developers

Code paths + invariants

ConcernWhere
Workflow contractworkflow_definitions WHERE workflow_type='service_hold_trigger'
D1 flagcustomers.entitystatus='hold'
NS flagcustomer.credit_hold=true
Decision auditauto_decisions table
Reflexion tagservice_hold,customer:<id>
Risk level4
Expected duration~15 min
Triggerevent · sources=ar_aging_tick_threshold, manual
// Trigger threshold (warn-only — HITL is the gate) if (days_overdue >= 60 && ar_balance >= 10000) { proposeServiceHold({ customer_id, days_overdue, ar_balance, reason }); } // D1 flag → NS push await db.run("UPDATE customers SET entitystatus='hold' WHERE id=?", id); await enqueueNsPush({ record: 'customer', id, payload: { credit_hold: true } });
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='service_hold_trigger' 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 · service_hold_trigger · risk_level · 4.

Inputs (required + optional)

FieldTypeDescription
customer_idintegerRequired.
hold_reasonstring'ar_aging' | 'credit_block' | 'compliance' | 'manual'
hold_severitystring'soft' | 'hard'

D1 tables written

TableOperationTrigger
proposed_actionsINSERTHold proposal
customer_programsINSERT (post-HITL)program_type=service_hold
ns_pending_pushesINSERTSet NS customer status
eventsINSERT (customer.service_hold)downstream block in SO creation

Endpoints called

MethodPathPurpose
POST/api/workflow/executeRunner entrypoint
POST/api/customer/:id/hold/releaseRelease hold

Events fired

event_typeWhenSubscribers
customer.service_holdPost-HITLSO creation guard
customer.service_hold.releasedOn releaseaudit
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 · Hold set but customer still received a new SO

Hold enforcement at SO creation is unwired. Today it's just a flag.

  1. Cancel the SO: Mike or AR cancels in NS.
  2. Add precondition: propose_sales_order must check active service_hold.
  3. Long-term: Wire the kill at SO creation, not just at HITL.

Scenario · Hold released but customer_programs row still active

Release endpoint didn't update the row.

  1. Update directly: UPDATE customer_programs SET status='released', released_at=datetime('now') WHERE customer_id=? AND program_type='service_hold' AND status='active'
  2. Push to NS: Set NS customer status back to active.

Scenario · Hold reason was wrong — customer paid 2 days ago

Stale AR data triggered the hold.

  1. Verify AR: SELECT total_open FROM customer_aging WHERE customer_id=?
  2. Release: POST /api/customer/<id>/hold/release with reason 'paid'
  3. Apologize: Customer-facing apology if order was blocked.

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.