HCC Docs

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 happenedWhat's next
No answer, busy, or a machineThe next step
Canceled before anyone answered, because the rep leftThe same step, in 5 minutes
A real conversation (answered, and not a machine), no goal metThe sequence ends as exit
A real conversation, with a callback timeThe 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 .exit that matches makes it exit, anything else goal.

When a lead leaves

Every lead leaves with a reason, on its row as exitReason and in the campaign's counts as exit_<reason>.

reasonHow it got there
goalA .goal, from a call, a disposition, a CRM change, the lead calling in or hcc.campaigns.complete
exitAn .exit, from any of those, or a real conversation with no goal met
finishedIt ran out of steps
do_not_callDo Not Call at the dial, whether the Dialer's or a rep's click on a preview
removedhcc.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;
}
  • step and stepName are where its pointer stood: the step it was on or, in a wait, the step due next. A stepName is null if 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 turns exit or finished into goal; a late callback brings it back with no Event, and it leaves again later; a removed lead can be enrolled again, and one in a campaign with .enroll starts 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 exitReason from hcc.campaigns.leads if 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.