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:
skippedin the answer fromhcc.leads.upsertManyandhcc.campaigns.enroll, and in an import's counts. - A lead that stops matching while it's in the campaign stays in: use
.exitfor that. - The Do Not Call list applies as well.
skippedfromhcc.campaigns.enrollcounts a lead the list kept out, as that call enrolls.hcc.leads.upsertManyand an import read only the filter, as they answer before the lead reaches the campaign. withinisn'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'equalstruein any case, and'50'equals50. Words are compared exactly.x: nullmatches only a stored null, not a missing field.- A range or
withinfails 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'for50), or a CRM sync after a call we logged. A.enrollwith nowherehas 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.changedevent. 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
whereends 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
reenrollon keeps what it held. Deploy thewhereit left under again, or turnreenrolloff and back on with the samewhere, and it comes back on its next write if a field differs. A lead that left whilereenrollwas 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_calledthe filter ranges over, that lead is dialed again after each finish. Don't name such a field, or usereenroll: 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_exitedfires again when it leaves again, with a neweventId. A.dostep'seventIdis new on each run through.