> ## Documentation Index
> Fetch the complete documentation index at: https://docs.haloagents.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Why a series shows 0 matched contacts, why enrollment did not happen, and how to verify audience filters against real contact data.

The **matched** count on a series trigger node and in the Series Rules panel is a live preview of how many contacts currently pass your audience filters (after contact type narrowing and, where applicable, subscription consent). It is not the same as **enrolled**, which only grows after you activate the series and recipients are queued.

If you see **0 matched** but you know sign-ups happened today, work through the checklist below. The first two examples are real production patterns.

## 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 call `identify()` when the user is known on the dashboard. They did **not** call [`POST /api/sdk/users/identify`](/api-reference/users) 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

1. **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_at` from your product's `users.created_at`.
2. **Keep widget identify on login** so traits refresh and sessions link correctly.
3. **Backfill** accounts created during the gap before deploy.
4. **Re-check the series** matched count after identifies land.

See [Identify Users → Authenticated apps with delayed first login](/web-widget/identify-users#authenticated-apps-with-delayed-first-login).

***

## 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` |

The builder showed **0 matched**. The team expected new account creators from that day to appear.

### What the data showed

Halo **did** have new users for that org on May 27, 2026:

| Contact                                                               | `signed_up_at` | `custom_fields.role` |
| --------------------------------------------------------------------- | -------------- | -------------------- |
| [soluzionefacilenet@gmail.com](mailto:soluzionefacilenet@gmail.com)   | Set (same day) | *(empty)*            |
| [uniqueuniformsindia@gmail.com](mailto:uniqueuniformsindia@gmail.com) | Set (same day) | *(empty)*            |
| [andrew@andrew-millar.com.au](mailto:andrew@andrew-millar.com.au)     | Set (same day) | *(empty)*            |
| [saul@ritekit.com](mailto:saul@ritekit.com)                           | *(empty)*      | *(empty)*            |

So **four** new rows were created that day. **Three** satisfied the date filter. **Zero** satisfied **Role is owner**, because the product's `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 to `custom: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:

```typescript theme={null}
ha.identify(userId, {
  email: user.email,
  name: user.name,
  signed_up_at: new Date().toISOString(),
  role: "owner", // required for "Role is owner" to match
});
```

Sign-ups were flowing in with `signed_up_at`, but without `role`, so the second filter excluded everyone.

### Fix

1. **In your app:** Pass `role` (and keep passing `signed_up_at`) on every identify at account creation, using the same string your filter expects (`owner`, `admin`, etc.).
2. **In Halo:** Re-open the series rules panel and confirm the matched count updates (usually within a few seconds after the preview API runs).
3. **For contacts who already signed up without `role`:** Backfill the trait with the [Update / Backfill API](/api-reference/users-update) or CSV import, both of which leave activity data untouched. Do **not** loop `identify()` to backfill, it bumps every user's `last_seen` to today. See [Importing & Backfilling Data](/contacts/importing-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

1. Open **Contacts** and filter or search for a user who should match.
2. Open their profile and check **Role** (under custom traits) and **Signed Up**.
3. In the series builder, confirm the trigger node **matched** count matches your expectation.
4. 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 first `identify()`. 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 outreach campaigns and segments read `signed_up_at`](/contacts/user-traits#how-outreach-campaigns-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`) |

Filtering **Role is owner** will not match Intercom `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 to `contact_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 as `stripe: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) |

A contact can be matched but not yet enrolled if the series is draft, or if enrollment gates block them (already active in the series, company priority, exit filter at enroll time, missing email for a required subscription, etc.).

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Triggers & Conditions" icon="bolt" href="/outreach/triggers-and-conditions">
    Entry triggers, audience filters, and activation rules.
  </Card>

  <Card title="User Traits" icon="user" href="/contacts/user-traits">
    `role`, `signed_up_at`, and what identify must send.
  </Card>
</CardGroup>
