Tables and segments
Understand what one row means on each table, then save a filter as a named segment that re-runs live and share it with your team.
A segment is a saved live filter over one grain: companies, contacts, or persons.
1. How segments work
- What is stored: only the definition, a name, a grain, and a filter. Never a member list.
- What runs: the filter compiles to one query over the grain's live columns; membership is whoever matches at the moment you read.
- What that buys: a segment can never be stale, and editing the filter re-defines membership instantly, no rebuild.
- What moves: when a record's data changes, its membership flips immediately, and each enter or exit becomes a row on the changes feed.
Segments are private to their creator by default, shareable explicitly, with one optional default view per grain.
2. Create a segment
A name, a grain, and a filter:
In this definition:
entityKindpicks the grain; onlynameis strictly required.filteris a boolean group (and/or, nestable) over condition nodes.- Conditions can reference any column the grain serves: fields, event counts, promoted event fields, identity links, scores.
GET /observe/companies/fieldsis the catalog of what is filterable.
3. Example segments
| Segment | Grain | Filter reads |
|---|---|---|
| Active paying companies | company | stripe_status = active |
| Slack-active accounts | company | evc__slack__message ≥ 50 |
| High engagement | company | sc__engagement ≥ 70 |
| Champions we know | person | title contains founder, active affiliation |
| At-risk payers | company | paying base segment, derived, sc__engagement < 40 |
Anything the grain serves as a column is filterable: fields, event counts, promoted event fields, identity links, scores.
4. Derive one grain from another
baseSegmentId builds a derived segment: start from a base on another grain, pivot through the affiliation edges, layer a filter on the result. "Contacts at active paying companies" is a contact segment derived from the company segment above.
Bases must be root segments; chains of derived segments are refused.
5. Share and set defaults
A segment belongs to the email that created it, stamped server-side. Shares grant view or edit per person; one deployment-wide default per grain defines the view everyone starts on. Service-key callers see everything.
6. Poll for changes
Because membership is live, "entered the segment" is a real signal. The consumer is one loop:
Seed with GET /observe/segments/{id}/member-ids, keyset-paged until hasMore is false.
Poll with POST /observe/segments/{id}/changes?since={cursor}. Each row is a member transition, enter, exit, or re-enter, with a monotonic seq; save nextSince after processing.
Delivery is at-least-once: the log commits before you read it, so a crashed poller re-reads instead of losing. Dedupe per member id on your side.
The edge cases are explicit, never silent:
- The first call bootstraps and returns no rows; seed via member-ids.
- A cursor ahead of the log or behind the prune watermark returns a reconcile signal (
410 CURSOR_EXPIRED). - A concurrent poll is a 409. Derived segments have no feeds (400); poll their base.
Alerts, CRM sync, and downstream automation build on exactly this loop.