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

# Segments

> Define an audience with a filter, keep it fresh, and use it to trigger outreach.

A **segment** is a named audience: a filter over your people, the custom
objects related to them, and the events they generated, saved once and
reused. Instead of re-describing "premium customers who haven't paid in 30
days" every time you launch an initiative, you define it as a segment and
point a journey's trigger at it.

Segments are how you decide who a conversation reaches. A win-back journey,
an NPS follow-up, an onboarding nudge: each one starts from a segment, not
from a hand-picked list of people.

## Discover what you can filter on

You cannot guess your own attribute names, so start by reading the catalog
instead of writing a filter blind:

```bash theme={null}
curl "$BASE/segments/catalog" -H "Authorization: Bearer $BOOM_KEY"
```

The response lists everything filterable in your organization, each with the
exact `attr` token to use and the operators valid for it:

* **`personAttributes`**: typed columns on the person (`email`, `status`,
  `createdAt`) plus every custom `attributes.<key>` your org has ingested.
* **`computedVariables`**: virtual `computed.<key>` values (an aggregate or
  expression over related data) resolved at filter time, with no stored
  value of their own.
* **`customObjectTypes`**: each type's own filterable attributes, addressed
  as `object.attributes.<key>` once you reach that type through a
  relationship.
* **`reachableObjects`**: the custom-object types reachable from a person,
  and every relationship path that gets you there, direct or through one
  or two intermediate objects.
* **`operatorReference`**: every operator's expected `value` shape
  (`scalar`, `list`, `date`, `dateRange`, `relativeWindow`, `dateOffset`,
  `none`), so you don't have to guess whether an operator wants a string, a
  list, or a window.

The catalog is generated from the same registry the compiler enforces at
save time, so it never lists an attribute or operator combination that
would actually be rejected.

## Build a filter

A segment's filter is a `FilterExpression`: a tree of predicates joined by
`AND`/`OR`. Each predicate is one of a few kinds:

| Kind               | Matches on                                                                      | Example use                                   |
| ------------------ | ------------------------------------------------------------------------------- | --------------------------------------------- |
| `person_group`     | Person columns and attributes                                                   | `plan_tier = premium`                         |
| `has_relationship` | A related custom object, optionally several hops away                           | Has an order with `status = refunded`         |
| `event`            | Whether an event did or did not happen, with property filters and a time window | Did not do `payment_made` in the last 30 days |

A segment for premium customers who have gone quiet on payments. Note the
`event` predicate: events reach Boom through the API, not through the
[database sync](/connect-your-database), which syncs people, custom objects and
relationships only. If your payment history arrives by database sync, filter on
a custom object's date attribute with `has_relationship` instead.

```json theme={null}
{
  "kind": "person",
  "op": "AND",
  "predicates": [
    {
      "kind": "person_group",
      "filters": [
        { "attr": "attributes.plan_tier", "type": "STRING", "op": "eq", "value": "premium" }
      ]
    },
    {
      "kind": "event",
      "eventName": "payment_made",
      "occurred": false,
      "window": { "op": "in_last_n", "value": { "amount": 30, "unit": "days" } }
    }
  ]
}
```

Operators worth knowing:

| Type            | Operators                                                                                                     |
| --------------- | ------------------------------------------------------------------------------------------------------------- |
| STRING          | `eq`, `neq`, `in`, `not_in`, `contains`, `not_contains`, `starts_with`, `ends_with`, `is_null`, `is_not_null` |
| NUMBER          | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `is_null`, `is_not_null`                               |
| BOOLEAN         | `eq`, `is_null`, `is_not_null`                                                                                |
| DATE (relative) | `in_last_n`, `in_next_n`, `more_than_n_ago`, `more_than_n_from_now`, `exactly_n_from_today`                   |
| DATE (specific) | `on`, `before`, `after`, `between`                                                                            |

<Note>
  `contains` and `not_contains` accept a single substring or a list. A list
  means "any of these": `contains` matches a row with any needle,
  `not_contains` keeps a row with none of them. To exclude several values,
  pass one `not_contains` with all of them, not a comma-joined string.
</Note>

## Author a segment end to end

The catalog, validate, preview, create, evaluate, and members endpoints
form one loop. An agent (or a script) can walk it without ever opening the
dashboard.

<Steps>
  <Step title="Read the catalog">
    `GET /api/v1/segments/catalog` (`segments_catalog`). See what's
    filterable before writing anything.
  </Step>

  <Step title="Validate the filter">
    `POST /api/v1/segments/validate` (`segments_validate`) with a
    `filterExpression`. This is a dry-run compile against your live catalog.
    Nothing is saved; a bad attribute or operator fails here with the exact
    field to fix.

    ```bash theme={null}
    curl -X POST "$BASE/segments/validate" \
      -H "Authorization: Bearer $BOOM_KEY" \
      -H "Content-Type: application/json" \
      -d '{"filterExpression": { "kind":"person", "op":"AND", "predicates":[...] } }'
    ```
  </Step>

  <Step title="Preview the match count">
    `POST /api/v1/segments/preview` (`segments_preview`) runs the same
    filter and returns `{ "count": N }`, the number of people it currently
    matches. No segment is created.
  </Step>

  <Step title="Create the segment">
    `POST /api/v1/segments` (`segments_create`) with `name`, `slug`, and the
    `filterExpression`. Creating a segment compiles and stores the filter,
    it does not evaluate it: the response comes back with `memberCount: 0`
    regardless of how many people actually match.

    ```bash theme={null}
    curl -X POST "$BASE/segments" \
      -H "Authorization: Bearer $BOOM_KEY" \
      -H "Content-Type: application/json" \
      -d '{"name":"Quiet premium customers","slug":"quiet-premium",
           "filterExpression": { "kind":"person", "op":"AND", "predicates":[...] } }'
    ```
  </Step>

  <Step title="Evaluate it">
    `POST /api/v1/segments/{slug}/evaluate` (`segments_evaluate`) runs the
    filter now and writes the membership rows, returning
    `{ "entrants", "leavers", "total" }`. Nothing is a member until an
    evaluation runs at least once.
  </Step>

  <Step title="Read the members">
    `GET /api/v1/segments/{slug}/members` (`segments_members_list`) lists
    the active members, newest first: `name`, `phoneNumber`, `externalId`,
    `email`, `enteredAt`. Keyset-paginated with `next_cursor`, same pattern
    as every other list endpoint.
  </Step>
</Steps>

`PATCH /api/v1/segments/{slug}` (`segments_update`) updates any field except
the slug, which is permanent. Changing `filterExpression` recompiles the
filter but does not re-evaluate it, so the member set stays exactly as it
was until the next evaluate call.

## Keeping a segment fresh

A segment's membership is a set of rows, not a live query, so it only
changes when an evaluation runs. How often that happens is
`evaluationCadence`:

* **`REACTIVE_ONLY`** (the default). The segment re-evaluates the specific
  people a CDP write touches, usually within seconds of that write, plus a
  daily backstop sweep to catch anything a reactive update missed. Nothing
  else triggers it, so an explicit `segments_evaluate` call is how you force
  a full refresh.
* **`HOURLY`** or **`DAILY`**. A scheduled sweep evaluates the whole segment
  on that cadence, independent of any particular write. Useful for filters
  that depend on the passage of time itself, like "hasn't paid in 30 days,"
  which can flip with no new data at all.

Whichever cadence you choose, a freshly created or just-edited segment
shows a stale or empty member set until the next evaluation actually runs.

<Warning>
  Set the cadence explicitly when you create a segment over the API. The default
  for API-created segments is `REACTIVE_ONLY`, and this page's own example is the
  case it handles worst: "no payment in 30 days" turns true from the passage of
  time, not from a write, so nothing reactive fires when a customer crosses the
  line. Ask for `HOURLY` or `DAILY` on any filter whose answer changes with the
  calendar, or evaluate on your own schedule.
</Warning>

### How a segment reaches outreach

A segment does not attach to an initiative directly. It becomes the
**trigger** on the journey behind that initiative: the journey's entry node is
configured with `triggerType: segment` and a `segmentId`. Once that journey is
published, every evaluation that adds someone to the segment enrolls them,
carrying along whatever data the segment projects for that person.

<Note>
  Wiring that trigger is an app job today. It needs the segment's internal id,
  and this API identifies segments by `slug`, so set a segment trigger in the
  Boom app rather than over the API.
</Note>

## Limits and gotchas

<Warning>
  `segments_evaluate` runs synchronously over the API and MCP, unlike the
  dashboard's "Evaluate now" button, which queues the evaluation in the
  background and returns immediately. On a large organization, a synchronous
  evaluate call can take a while and holds the request open the whole time.
  If you just need the segment defined and don't need its membership
  refreshed right away, skip the evaluate call and let its cadence run it.
</Warning>

* **Create and update never evaluate.** `memberCount` is `0` right after
  `segments_create`, and a filter change from `segments_update` leaves the
  existing member set untouched until you call `segments_evaluate`.
* **Weekend freeze.** A segment with `skipWeekendEvaluation` set rejects an
  explicit evaluate on Saturday or Sunday (in your organization's timezone)
  with `400 weekend_evaluation_skipped`.
* **Deleting requires organization admin over MCP.** `segments_delete` is a
  soft delete: it disappears from every list immediately, and its journey
  triggers are removed so no journey keeps enrolling from it. It is
  idempotent, `{ "removed": false }` on a segment that is already gone.
* **Pagination.** `segments_list` and `segments_members_list` are
  keyset-paginated, defaulting to 100 rows and capped at 1000; loop on
  `next_cursor` until it comes back `null`.
* **Preview returns a count only.** It does not return a sample of matching
  people over the API or MCP, only the total.
* **Slugs are permanent.** Pick one deliberately; `segments_update` cannot
  rename it.

## Related

<CardGroup cols={2}>
  <Card title="Customer Data Platform" icon="layers" href="/cdp/overview">
    The people, objects, and events a segment's filter reads.
  </Card>

  <Card title="Events" icon="bolt" href="/events">
    How the events an `event` predicate matches against get recorded.
  </Card>

  <Card title="Journeys" icon="route" href="/journeys">
    Set a segment as a journey's trigger to turn membership into outreach.
  </Card>

  <Card title="Use MCP" icon="plug" href="/use-mcp">
    Author segments conversationally with the same tools documented here.
  </Card>
</CardGroup>
