HCC Docs

Who a campaign is for

.enroll says which leads a campaign takes. It's part of the definition, so it deploys with your code and changes the campaign's version. A deploy still enrolls nobody.

defineCampaign('renewals').enroll({
  where: {
    tier: 'gold', // equals
    churned: { not: true }, // a lead with no such field passes a `not`
    region: { in: ['us', 'ca'] },
    score: { gte: 50 }, // gt, gte, lt, lte: numbers or ISO dates
    renews_at: { within: '30d' }, // from now until that far ahead
  },
  reenroll: true, // the default
});

All of them have to pass, up to 10 fields. A .enroll with no where takes every lead. With no .enroll, nothing changes: onLead hears about new leads, and a lead that left stays out unless it was removed.

Who gets in

  • While it runs, it takes new and changed leads that match. A paused campaign takes only leads you name it for.
  • onLead. With none, this happens with no call to your server. With one, it runs only for leads that match, and still decides, so a priority stays there.
  • The filter always applies. A lead that names the campaign, through the API or a CSV, and doesn't match is skipped: skipped in the answer from hcc.leads.upsertMany and hcc.campaigns.enroll, and in an import's counts.
  • A lead that stops matching while it's in the campaign stays in: use .exit for that.
  • The Do Not Call list applies as well. skipped from hcc.campaigns.enroll counts a lead the list kept out, as that call enrolls. hcc.leads.upsertMany and an import read only the filter, as they answer before the lead reaches the campaign.
  • within isn't watched. It's read when a lead arrives or changes, and when a backfill reaches it. A lead doesn't enter by itself as its date comes near: run a backfill for that, on a schedule of yours.

How values compare

Values are compared as a CSV or a CRM holds them.

  • 'true' equals true in any case, and '50' equals 50. Words are compared exactly.
  • x: null matches only a stored null, not a missing field.
  • A range or within fails on a value it can't read, and never throws.
  • Dates are ISO 8601. With no time they mean midnight, and with no offset they mean UTC.
  • Milliseconds since 1970, as a number or a string, are read as dates too, because that's what HubSpot's date properties hold. Seconds since 1970 aren't.

Who comes back

reenroll says who comes back. On, a lead that left starts over from the first step when a field the filter names holds another value than it did when it left, and it still matches. Off, a lead that left stays out, a removed one too. A lead that left through Do Not Call never comes back, and a backfill never brings back a lead that left.

What counts as a change:

  • Only a field the filter names. Editing any other field brings nobody back, and neither does the same data sent again, the same value in another form ('50' for 50), or a CRM sync after a call we logged. A .enroll with no where has no field to watch, so nobody comes back to it.
  • It's measured from when the lead left. A value that changes while it's still in doesn't count, and one that changes and changes back doesn't either. What's compared is the value now, so a change made while the campaign was paused brings the lead back on its next write of any kind, if it matches.
  • Time passing is no change: with within, a lead that left returns only if the date itself changes.

What can hold it back:

  • Your goals and exits are asked first, with a lead.changed event. If one is true, the lead stays out as it left. If they fail for a lead, or run out of time, that lead doesn't start over and is asked again with backoff, while the others carry on. If they keep failing, it waits for its next write. A lead written again meanwhile doesn't start over on the old answer: the newer write asks again. A campaign with neither has no call for this.
  • A lead doesn't start over while we can see it shouldn't be dialed: a call of this campaign to it still live, or its number on your Do Not Call list. Nothing wakes it when that ends: it's looked at again on the lead's next write of any kind, and starts over then if the field still differs.

When you change the filter:

  • Another where ends this for leads that already left. A lead comes back only under the filter it left under, so that deploy brings nobody back, and they stay out until they've been through the campaign again. A lead that leaves from then on goes by the new one.
  • Restoring a filter restores this. A lead that left with reenroll on keeps what it held. Deploy the where it left under again, or turn reenroll off and back on with the same where, and it comes back on its next write if a field differs. A lead that left while reenroll was off held on to nothing, and stays out.

Once it's back:

  • Loops are possible on a field the filter names. If your code or a CRM workflow writes such a field after every call, with a new value each time, say a last_called the filter ranges over, that lead is dialed again after each finish. Don't name such a field, or use reenroll: false.
  • A lead that starts over is back in the counts and the report as in the campaign, so those show where it stands now. campaign.lead_exited fires again when it leaves again, with a new eventId. A .do step's eventId is new on each run through.