Disposition actions
How a disposition’s actions array decides what happens to a lead after the call ends —
and which of those outcomes survive the reset buttons.
This page is for whoever edits dispositions in Settings. If you just want to know what each button does on a call, read What each disposition does instead.
| Authority | the dispositions table |
| Action types | 6 |
| Default status | called |
| Validation | none |
| Failure mode | silent skip |
1. Where the authority lives
Section titled “1. Where the authority lives”dispositions is a team-scoped database table, and the live rows are the only thing the
runtime reads — the dialer panel, the guards, analytics and the AI classifier all query
the table for the team in question.
DEFAULT_DISPOSITIONS in the code is a bootstrap seed for brand-new teams only. It is
consulted when inserting slugs a team does not have yet, never to correct a row that
already exists.
So editing the seed changes nothing for an existing team, and editing one team’s row
changes nothing for the next team. A change that should apply to both has to be made
twice, deliberately. Edit live rows in Settings or with a direct UPDATE.
2. What runs, in order
Section titled “2. What runs, in order”recordCallTerminal(listLeadId, 'called')— writescalledand bumpsattempts. Runs for every disposition, before any action.executeDispositionActions(...)— runs each non-auto_advanceaction in array order. Aset_prospect_statushere overrides step 1.- Advance to the next prospect — gated on
shouldAdvance, which defaults to true.
Actions are best-effort, not a transaction. Each one is individually try/caught — a handler that throws is logged and swallowed, the remaining actions still run, and the disposition still saves.
There is no schema validation on write. An unknown type logs
Unknown disposition action type to the server console and is skipped, so a typo in the
JSON is silently inert.
3. The six action types
Section titled “3. The six action types”add_to_dnc — suppress the number team-wide
Section titled “add_to_dnc — suppress the number team-wide”{ "type": "add_to_dnc", "reason": "Bad number", "source": "bad-number" }- Suppresses
calls.dialed_number, not the lead’s primary phone. On a cascade attempt those differ, and DNC’ing the wrong one kills a working line while leaving the dead one dialable. - DNC is the only state that survives a re-import. Everything else here is per-list and comes back when the lead is imported again.
sourcedefaults to"disposition". Set it to keep a dead line out of the genuine-opt-out bucket in the compliance export.
set_prospect_status — override the default called
Section titled “set_prospect_status — override the default called”{ "type": "set_prospect_status", "status": "completed" }Accepts completed, bad-number, qualified, pending. Runs with bumpAttempts: false
— without it every such disposition would burn two attempts toward exhaustion instead of
one.
bad-number,dnc,completedandexhaustedare never downgraded. Once a row reaches one, a later softer webhook cannot re-open it.status: "pending"on a direct-dialbusycall is diverted — it becomes a randomised 30–60 minutecallbackpark, orexhaustedon a fourth consecutive busy. Onlypendingis affected.
schedule_callback — book the follow-up, and render the inputs
Section titled “schedule_callback — book the follow-up, and render the inputs”Presence of this action is what makes the callback date and notes fields appear in the dialer panel. It is a UI switch as much as a behaviour.
| Shape | Behaviour |
|---|---|
| absent | No inputs shown. Row stays at called. |
{} |
Optional. Blank is legitimate — no error, nothing scheduled. |
{ required: true } |
Dialer guard and server both reject a save with no date. |
Only callback is required, because booking the callback is the outcome.
target-not-available was required for six days in July and relaxed: agents often cannot
know when to call back, and the hard block pushed them toward less accurate dispositions.
When a date is given, routing goes one of three ways — no client on the source list, or a
same-day snooze, both flip in place; a cross-day callback copies the lead onto the
per-(client, tag) follow-up list and deliberately leaves the source row at called,
because flipping both would surface the prospect on two lists at once and double-dial.
tag_prospect — attach a tag to the lead, not the row
Section titled “tag_prospect — attach a tag to the lead, not the row”{ "type": "tag_prospect", "tags": ["needs-research"] }Tags attach to the LEAD, not the list_lead row. A lead tagged once carries that tag
on every list it belongs to, forever. A tag is evidence that a disposition happened
somewhere, once — it is not a reliable marker of the current state of a particular list row.
To ask what a row’s outcome is, read the latest calls.disposition for that
(lead_id, list_id). Not the tag.
send_webhook — fire-and-forget POST
Section titled “send_webhook — fire-and-forget POST”5s default timeout, no retry, no signature, no delivery log. Body carries callId,
prospectId, disposition, callDuration, teamId, timestamp. Not used by any live
disposition today.
auto_advance — move to the next prospect
Section titled “auto_advance — move to the next prospect”Advancing is the default — this action exists to switch it off. Last occurrence in the array wins. It has no handler; the executor reads it, and the Twilio status webhook re-derives the same answer independently to finish a deferred voicemail-drop advance. That is why the helper lives in the types module with no DB imports: the two callers must never disagree.
4. Which reset revives which status
Section titled “4. Which reset revives which status”The status a disposition parks at decides which buttons on the list page can pull the lead back into the queue. ● means that button revives it.
| Status | Reset Re-Dialable | Reset Called | Reset No-Answer | Reset Exhausted | Reset Selected |
|---|---|---|---|---|---|
pending |
● | · | · | · | ● |
no_answer |
● | · | ● | · | ● |
failed |
● | · | · | · | ● |
called |
· | ● | · | · | ● |
callback |
· | · | · | · | ● |
exhausted |
· | · | · | ● | ● |
completed |
· | · | · | · | ● |
bad-number |
· | · | · | · | ● |
skipped_tollfree |
· | · | · | · | ● |
Three things to take from that
Section titled “Three things to take from that”called is revivable by a single button. Reset Called exists to work a list again, and
it cannot distinguish a deliberate outcome from a dial that went nowhere. Anything meant to
be a decision must not park there.
completed is the “deliberately done, not a compliance DNC” bucket. It survives every
bulk reset. disqualified and wrong-contact-no-referral use it, and since 28 August 2026
so do need-research and call-corp.
Reset Selected has no status filter at all. It revives anything, including completed
and bad-number. That is the deliberate checkbox-driven escape hatch — no status protects
against it, by design.
5. Choosing actions for a new disposition
Section titled “5. Choosing actions for a new disposition”1 · Should this lead ever be dialed again on this list?
| Answer | Use |
|---|---|
| Never, and it is a compliance matter | add_to_dnc — the only state that survives re-import |
| Never, but not a compliance matter | set_prospect_status: completed + a tag so “why” stays answerable |
| The number itself is dead | add_to_dnc + set_prospect_status: bad-number |
| Yes, later, at a known time | schedule_callback |
| Yes, on the next pass through the list | no status action — called is correct here |
2 · Is this a decision, or just an unremarkable call outcome?
A decision — researched, disqualified, routed to HQ, wrong contact — must park where Reset
Called cannot reach. Leaving it at called means it comes back the next time anyone works
the list, and the agent’s work is silently discarded. This is the failure need-research
shipped with.
3 · Does the agent need to pick a callback time?
Add schedule_callback — it is what renders the inputs. Mark it required only when the
disposition is meaningless without a date.
4 · Should the queue move on?
Almost always yes, which is the default. Include it explicitly anyway — every live row does, and its absence reads as an oversight rather than an intent.
Shapes to copy
Section titled “Shapes to copy”// Terminal decision, not a DNC — the safe default for any triage outcome[{"type":"tag_prospect","tags":["<slug>"]}, {"type":"set_prospect_status","status":"completed"}, {"type":"auto_advance","enabled":true}]
// Optional follow-up, stays re-dialable on the next pass[{"type":"schedule_callback"}, {"type":"auto_advance","enabled":true}]
// Dead line[{"type":"add_to_dnc","reason":"Bad number","source":"bad-number"}, {"type":"set_prospect_status","status":"bad-number"}, {"type":"auto_advance","enabled":true}]6. Errata
Section titled “6. Errata”- No
set_prospect_statusmeans the row parks atcalledand Reset Called revives it. (§4) - Tags are lead-scoped, not row-scoped. Never scope a cleanup by tag alone — scope by
the latest
calls.dispositionfor that(lead_id, list_id). (§3) - The seed and the live table are independent by design. Fixing one does not fix the other. (§1)
- Actions are best-effort. Each is try/caught individually; a failure is logged, not surfaced, and does not stop the disposition saving. (§2)
- No validation on the actions JSON. An unknown
typeor a misspelled key is silently inert. The editor is a raw JSON textarea. (§2) set_prospect_status: pendingis not alwayspendingon direct-dial busy calls. (§3)- Changing a disposition’s actions is retroactive to nothing. Rows already parked keep the status they were given — a fix needs a backfill sweep. (§3)