How a lead moves
Steps run in order. Steps never check whether a lead should still be there, because goals and exits end the sequence at any moment.
After a call
| What happened | What's next |
|---|---|
| No answer, busy, or a machine | The next step |
| Canceled before anyone answered, because the rep left | The same step, in 5 minutes |
| A real conversation (answered, and not a machine), no goal met | The sequence ends as exit |
| A real conversation, with a callback time | The callback replaces the next wait |
A callback time is set by the rep, or with c.hangup({ callbackAt }) in a flow.
Goals and exits
.goal and .exit run on every event for a lead, off the dial path: a call ended (call.ended), a disposition, a CRM change (lead.changed), the lead calling in (lead.called_back), and hcc.campaigns.complete(lead) (completed).
- A goal wins if both match.
- One met while a lead waits on the Dialer's list takes it off, and it's checked again at the moment of dialing, so the worst case is one call already under way.
- There's no branching: a handler can skip its own step.
hcc.campaigns.complete(lead, { campaign? })always ends the sequence, from your own code: an.exitthat matches makes itexit, anything elsegoal.
When a lead leaves
Every lead leaves with a reason, on its row as exitReason and in the campaign's counts as exit_<reason>.
reason | How it got there |
|---|---|
goal | A .goal, from a call, a disposition, a CRM change, the lead calling in or hcc.campaigns.complete |
exit | An .exit, from any of those, or a real conversation with no goal met |
finished | It ran out of steps |
do_not_call | Do Not Call at the dial, whether the Dialer's or a rep's click on a preview |
removed | hcc.campaigns.removeLead, onLead returning c.remove(), a number that can't be dialed, or the lead being deleted, through hcc.leads.delete or because its CRM record went or lost its number. A deleted lead's leadId no longer resolves |
Each time, a campaign.lead_exited Event goes to your webhook endpoints:
case 'campaign.lead_exited': {
const { eventId, campaign, leadId, reason, step, stepName } = event.data;
// Delivery is at least once: a repeat has the same eventId, which is also event.id.
if (await seen(eventId)) break;
await crm.closeLead(leadId, { campaign, reason });
break;
}stepandstepNameare where its pointer stood: the step it was on or, in a wait, the step due next. AstepNameisnullif the step has none.- A lead can leave more than once, each with a new
eventId, so use the latest. A disposition filed late on the call it left on turnsexitorfinishedintogoal; a late callback brings it back with no Event, and it leaves again later; aremovedlead can be enrolled again, and one in a campaign with.enrollstarts over when a field its filter names changes. - Sending it never holds up the lead. The Event is published after the lead has left, and if publishing fails it's logged, not retried: read
exitReasonfromhcc.campaigns.leadsif you need to reconcile. Once published, delivery retries with backoff.
Do Not Call
- A number going on the list doesn't end a lead by itself. It leaves when its next dial is refused, so a lead in a long wait hasn't left yet.
- A number already on it doesn't go in. It gets no row, no counters, no Events and no first step, whichever way it came: a new or changed lead,
hcc.campaigns.enroll, a CSV, a CRM or a backfill. - Taking it off the list doesn't put it in: its next write does, on a campaign with an
.enroll, or the next backfill.