Skip to content

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

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.

  1. recordCallTerminal(listLeadId, 'called') — writes called and bumps attempts. Runs for every disposition, before any action.
  2. executeDispositionActions(...) — runs each non-auto_advance action in array order. A set_prospect_status here overrides step 1.
  3. 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.

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.
  • source defaults 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, completed and exhausted are never downgraded. Once a row reaches one, a later softer webhook cannot re-open it.
  • status: "pending" on a direct-dial busy call is diverted — it becomes a randomised 30–60 minute callback park, or exhausted on a fourth consecutive busy. Only pending is 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.

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.

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 · · · ·

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.

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.

// 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}]
  • No set_prospect_status means the row parks at called and Reset Called revives it. (§4)
  • Tags are lead-scoped, not row-scoped. Never scope a cleanup by tag alone — scope by the latest calls.disposition for 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 type or a misspelled key is silently inert. The editor is a raw JSON textarea. (§2)
  • set_prospect_status: pending is not always pending on 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)