Example: sign-ups in your app, but missing in Halo until first login
What we saw
New accounts showed up in the customer’s database and analytics on signup day, but Contacts in Halo stayed empty until those users opened the authenticated app days later (email verification pending, invite not accepted yet, etc.). A Created Account series with audience filters on Signed Up showed 0 matched for today’s sign-ups even though the product had created users that morning.Root cause
The customer followed the widget quickstart: load the SDK and callidentify() when the user is known on the dashboard. They did not call POST /api/sdk/users/identify from their signup API.
Halo only creates a contact after identify (SDK or REST), Stripe sync, or an integration import. A row in the customer’s Postgres (or similar) does not sync by itself.
Fix
- In your signup handler: Call server-side users/identify (and companies/identify when applicable) in the same request that creates the user and team. Pass
signed_up_atfrom your product’susers.created_at. - Keep widget identify on login so traits refresh and sessions link correctly.
- Backfill accounts created during the gap before deploy.
- Re-check the series matched count after identifies land.
Example: sign-ups today, but 0 matched (missing role on identify)
What we saw
A Created Account series used:| Setting | Value |
|---|---|
| Trigger | Matches the audience filter (segment_match) |
| Contact type | Users only |
| Audience | Filter by criteria |
| Filters | Role is owner AND Signed Up on or after 2026-05-27 |
What the data showed
Halo did have new users for that org on May 27, 2026:| Contact | signed_up_at | custom_fields.role |
|---|---|---|
| [email protected] | Set (same day) | (empty) |
| [email protected] | Set (same day) | (empty) |
| [email protected] | Set (same day) | (empty) |
| [email protected] | (empty) | (empty) |
identify() call was not sending role at signup.
Older contacts in the same org did have role: owner (from a later backfill or manual sync), but none of those also had signed_up_at on or after the filter date. The AND combination correctly evaluated to 0 matched.
Root cause
The Role audience field maps tocustom:role, which reads end_users.custom_fields->>'role'. It is not Intercom’s intercom_role (user / lead). It is only populated when your app passes role in traits on identify:
signed_up_at, but without role, so the second filter excluded everyone.
Fix
- In your app: Pass
role(and keep passingsigned_up_at) on every identify at account creation, using the same string your filter expects (owner,admin, etc.). - In Halo: Re-open the series rules panel and confirm the matched count updates (usually within a few seconds after the preview API runs).
- For contacts who already signed up without
role: Backfill the trait with the Update / Backfill API or CSV import, both of which leave activity data untouched. Do not loopidentify()to backfill, it bumps every user’slast_seento today. See Importing & Backfilling Data. Temporarily removing the Role filter is an option if you need to include today’s sign-ups before the backfill runs.
How to verify before you activate
- Open Contacts and filter or search for a user who should match.
- Open their profile and check Role (under custom traits) and Signed Up.
- In the series builder, confirm the trigger node matched count matches your expectation.
- Click View who currently matches (or the equivalent audience preview) and spot-check a few emails.
Other common reasons for 0 matched
Contact does not exist yet (signup without server identify)
If you only identify in the browser after login, users who signed up but have not opened the app yet do not exist in Halo. They cannot match any audience filter and will not enroll in series until their firstidentify(). Fix with server-side identify at signup (see example above).
signed_up_at is empty
Audience filters on Signed Up read the signed_up_at column only. There is no fallback to created_at. Contacts created today via identify still won’t match a date filter if you never sent signed_up_at.
See User Traits → How automations and segments read signed_up_at.
Wrong field for “role”
| You filter on | Where the value must live |
|---|---|
Role (custom:role) | identify({ role: "..." }) → custom_fields.role |
| Intercom-style role | custom:intercom_role (from Intercom sync; values like user, lead) |
user / lead values.
Contact type mismatch
Users only excludes leads. If sign-ups are captured as leads first (widget form, no verified identify), they won’t appear in the matched count until promoted tocontact_type = user.
Subscription list or suppression
With Subscription list as the audience source, opt-in lists with no subscribers return 0. The preview also subtracts bounced and unsubscribed addresses from the headline count.Stripe or company filters resolve to nobody
Virtual fields such asstripe:subscription_status pre-resolve to user ID sets. If that set is empty, the whole audience can resolve to zero matches.
Series is still in draft
0 matched is about filters, not activation. But remember: no one enrolls until the series is active (except that you can still use the preview while drafting).segment_match vs unsaved trigger
The matched count uses the saved audience filters on the automation row. If you changed the trigger in the UI but have not saved, the preview may not reflect what you see in the panel until you save.
Matched vs enrolled
| Term | Meaning |
|---|---|
| Matched | Contacts who pass audience filters right now |
| Enrolled | Contacts who have been queued into the series at least once (lifetime, after activation) |
Where to go next
Triggers & Conditions
Entry triggers, audience filters, and activation rules.
User Traits
role, signed_up_at, and what identify must send.