HCC Docs

Campaigns

A campaign is a sequence in your code: what to try, in order, and the goals and exits that end it for a lead at any moment. We place the calls, and keep the rules no code can change.

import { defineCampaign, defineQueue, offer } from '@hcc/sdk';

export const sales = defineQueue('sales', { skills: { sales: 1 } });
export const vips = defineQueue('vips', { skills: { vip: 1 } });

export const renewals = defineCampaign<{ stage?: string; vip?: boolean }>('renewals')
  .goal((lead, event) => event.disposition === 'meeting_booked' || lead.fields.stage === 'customer')
  .exit((_lead, event) => event.disposition === 'not_interested')
  .call(renewalFlow) // the flow takes the call, with no rep
  .wait('30m')
  // Your provider: we don't send SMS. It can run more than once, so pass `eventId` along.
  .do((lead, { eventId }) =>
    sms.send(lead.phone, 'Sorry we missed you.', { idempotencyKey: eventId }),
  )
  .wait('3d')
  .call((lead) => (lead.fields.vip ? vips : sales)) // a rep takes the call
  .onVoicemail((call) => call.play('https://cdn.example.com/sorry-we-missed-you.mp3').hangup());

Name the shape of lead.fields with defineCampaign<MyFields>. It isn't checked at runtime, and a CSV's values arrive as strings.

Turn it on

hcc deploy deploys it. A new one starts paused in every environment, test included, so a deploy never starts it dialing.

  • Turn it on in the dashboard (Campaigns, open it, Resume), or with hcc.campaigns.resume(renewals) from a script.
  • Deploying again never changes the status of one that exists: a running campaign stays running.

Add leads

Leads come in through the API or a CSV. A lead needs a timezone to be dialed.

await hcc.leads.upsertMany([{ phone: '+14155550123', name: 'Ana', timezone: 'America/New_York' }], {
  campaign: 'renewals',
});

hcc.leads.upsert overwrites the lead with the same ref, or the same number if you give none. To add only, pass { ifExists: 'refuse' }: if a lead has that ref, or with no ref any lead has that number, a CRM's included, nothing is written and it throws HccApiError with code: 'lead_exists' and the leads in details.leads. A new ref on a number with leads adds a second lead, as people share lines. The dashboard's Add lead form adds the same way.

Leads you already have don't enter by themselves: see Existing leads.

Choose the steps

A rep, a flow or an AI agent can be any step, the first included. A campaign with no steps is one call on your only queue.

StepWhat happens
.call(queue)Keeps a free rep, dials the lead, and joins the rep when they answer. The rep sees nothing until then.
.call(flow)Dials with no rep, so only your limits hold it back. The flow takes the call, with c.input.lead. An AI agent is a flow that connects one.
.call(offer(queue))Shows the lead on a rep's desk, and their click dials it. Reps only see dialing when your code asks. A lead they skip waits 5 minutes.
.call(handler)Runs ahead of the dial, in batches, so a slow server never slows one. Returns a target, or c.to(target, { from, input }), or c.skip() for the next step.
.wait('3d')A lead in a wait uses no memory and makes no calls. Good to the minute.
.do(handler)Any code, off the dial path. It can run more than once: ctx.eventId says it's the same run. Pass it along to whatever you call. If it keeps failing, the lead parks as step_failed.