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 '@hccrun/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.
  • defineCampaign('renewals', { hosting: 'hcc' }) has us run its code. A hosted campaign runs its latest code, as a self-hosted one does. See Hosting.

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.

A lead's preferredLanguage, a BCP-47 tag such as es or pt-BR, is the language live translation expects on its calls. A CSV sets it from its language column: a cell that isn't a tag is reported, and the lead is imported without it. A rep's language (default en) is set with hcc.reps.update or on the dashboard's Reps page.

A flow can say it too, from a menu choice: call.setLanguage('es') plays nothing and replaces what the lead said. With { auto: false }, or a queue's translation: { auto: false }, translation never starts on its own on that call; your code and a rep still may. Under detectLanguage, a call whose language nothing gave is listened to once a rep is on it: about 10 to 20 seconds of the customer's speech, then call.language.detected. A detected language never replaces one your data gave.

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.
.text(handler)Your code writes a Text and we send it on your account, once. Returns c.send(body, { from }) or c.skip(). See Texts in a campaign.
.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.