About segments
A skill for creating, editing, and applying RevOS audience segments through its MCP server. It emphasizes checking cube metadata, stored values, join paths, and saved results because API writes may not validate segment logic.
Key features
- 01Inspects cube metadata and joins
- 02Builds audience filters
- 03Checks stored values before conditions
- 04Avoids one-to-many fan-out errors
- 05Creates and updates segments
- 06Reads back writes for verification
Use cases
- 01Creating an audience from semantic-model data
- 02Editing segment conditions without dropping others
- 03Scoping a table view to a segment
MCP support
Uses the RevOS MCP server's generic tools on the segments resource: api_read lists and retrieves segments, while api_write creates and updates them. Cube metadata and queries are also accessed through these tools.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ revosai/ skills --skill segmentsAfter installing, type/segmentsin Claude Code to confirm it loaded.
Bundled filesevals/evals.jsonreferences/filter-syntax.md
Details
From the project READMESegments
A segment is a saved audience: one row per record of a root cube, kept when it matches a filter. It's defined over the semantic model (cubes), not over a table — tables only borrow a segment to scope their rows.
You reach it through the RevOS MCP server's generic tools on the segments
resource: api_read lists and gets, api_write creates (no id) and updates
(with id). api_details({ resource: "segments", operation: "create" }) is
the authority on the body shape; this skill covers what that schema can't
tell you.
The thing to keep in mind throughout: the server barely validates a segment when you save it. Member names, join paths, and whether a condition makes sense for the root are only checked by the RevOS UI's segment editor — and once a segment is attached to a table's default view, its conditions are compiled into that table's view, so a bad member there can break the org's model, not just the segment. Over the API a broken segment saves with a 200 and then fails, or worse, quietly matches the wrong records when it's evaluated. So the checks below are yours to do before writing — and reading the segment back after writing is how you find out what was actually saved.
Calling the tools
The argument shapes matter more than usual here, because a key in the wrong place is often ignored rather than rejected — a write with its body under the wrong key can return success, bump the segment's version, and change nothing.
// List / get / named reads — `params` for query-string filters, `body` for a named read that takes one
api_read { "resource": "segments", "params": { "filter": "name == \"Enterprise accounts\"" } }
api_read { "resource": "segments", "id": "<segment id>" }
api_read { "resource": "cubes", "method": "meta", "params": { "filter": "name == \"crm_deals\"" } }
api_read { "resource": "cubes", "method": "query",
"body": { "query": { "dimensions": ["crm_deals.stage"], "limit": 20 } } }
// Create (no id) / update (with id) — the record always goes under `data`
api_write { "resource": "segments", "data": { "name": "…", "type": "DYNAMIC", "cubes": [ … ], "filter": { … } } }
api_write { "resource": "segments", "id": "<segment id>", "data": { "filter": { … } } }
method (not operation) selects a named read like meta or query; a
cubes query goes under body.query, never params.
Creating a segment
1. Pick the root cube — it decides what one "member" is
"Companies that have a won deal" has companies as members; "won deals" has deals as members. Same data, different audience. Choose the cube whose rows are the things the user wants to end up with, and confirm with the user when it's genuinely ambiguous. Don't change the root after creation — the API won't stop you, but everything built on the segment assumes it; make a new segment instead.
Read the catalogue first, then the root's full definition:
{ "resource": "cubes", "method": "meta", "params": { "fields": "name,title,description", "pageSize": 100 } }
{ "resource": "cubes", "method": "meta", "params": { "filter": "name == \"crm_companies\"" } }
The full definition gives you the members (always written
<cube>.<member>), each dimension's type, which dimension has
primaryKey: true, meta.nameDimension when the cube declares one, and the
cube's joins. Never guess a member name — they're per-organization.
Prefer business-named cubes over generated plumbing — helper cubes with a sparse or missing description and a mechanical-looking name — unless the user is clearly asking about one of those.
2. Build cubes — the root first, then one entry per other cube you filter on
"cubes": [
{ "joinPath": "crm_companies", "primaryKeyDimension": "id", "nameDimension": "name" },
{ "joinPath": "crm_companies.crm_deals" }
]
cubes[0]is the root. ItsjoinPathis just the cube name. SetprimaryKeyDimensionto the root's primary-key dimension (the member name without the cube prefix) andnameDimensiontometa.nameDimensionwhen the cube declares one, otherwise to a dimension literally callednameif the cube has it — table creation from a segment uses both to build itsidandnamecolumns. If the root has no primary-key dimension, say so: the segment will evaluate onid, which may not exist.- Every other cube a condition references gets an entry whose
joinPathis the dotted path from the root to it, followingjoins:"root.middle.target". Check each hop against a cube'sjoins— and remember a cube only lists joins it declares, so ifAdoesn't listB, look atB's joins too before concluding there's no path. - When there are two genuinely different routes to the same cube, the path you pick changes the answer. Don't silently choose — ask which relationship the user means.
3. Write the filter — and watch for fan-out
filter is RevOS's own condition format (not a Cube.js query):
"filter": {
"combinationMode": "AND",
"items": [
{ "member": "crm_companies.industry", "type": "EQUAL", "value": "Software" },
{ "filterType": "Group", "combinationMode": "OR", "items": [
{ "member": "crm_companies.country", "type": "EQUAL", "value": "DE" },
{ "member": "crm_companies.country", "type": "EQUAL", "value": "AT" } ] }
]
}
The operator vocabulary, which operators fit which member types, date values,
and the validation rules the JSON schema doesn't show are in
references/filter-syntax.md — read it before
writing any condition beyond a simple EQUAL. Two rules come up constantly:
there is no multi-value "in" (value is one string or number, so "DE or
AT" is an OR group as above), and IS/IS_NOT only take TODAY,
IN_THE_FUTURE, or IN_THE_PAST.
Check the stored values before any EQUAL / NOT_EQUAL on a string.
Matching is exact and case-sensitive, and the user's words ("active",
"enterprise", "won") are often not how the source stores them (ACTIVE,
Enterprise, closedwon). A wrong value doesn't error — the segment just
comes out empty. One cheap query shows what's really there:
api_read { "resource": "cubes", "method": "query",
"body": { "query": { "dimensions": ["billing_subscriptions.status"], "limit": 20 } } }
Use the stored spelling, and if nothing matches the user's term, ask rather than guess.
Fan-out — the trap that saves fine and answers wrong. If the root has
many of another cube, a dimension condition on that cube doesn't mean
"members that have at least one such row" in any reliable way — it fans the
root out across the child rows and the UI refuses to save it for exactly this
reason. The API won't stop you. The join may be declared on either side,
so read both cubes' joins (Cube accepts snake_case and camelCase):
| join found | root has… |
|---|---|
root → other: many_to_one/belongsTo or one_to_one/hasOne | one |
other → root: one_to_many/hasMany or one_to_one/hasOne | one |
root → other: one_to_many/hasMany | many |
other → root: many_to_one/belongsTo (e.g. crm_deals → crm_companies) | many |
Rephrase the condition as a measure on the child cube instead, which aggregates per root row:
- "Companies with at least 3 deals" →
{ "member": "crm_deals.count", "type": "GREATER_THAN_OR_EQUAL", "value": 3 } - "Companies with any deal" →
crm_deals.countGREATER_THAN0.
If the child cube has no measure that expresses the condition (e.g. "has a deal in stage won" and there's no count-of-won-deals measure), don't write the fanning dimension condition anyway. Tell the user plainly that the semantic model doesn't support that condition from this root yet, and offer what it does support — a segment rooted on the child cube instead (won deals), or adding a measure to the cube.
Conditions on a cube the root has one of are fine as dimension conditions.
4. Choose type — DYNAMIC unless the user insists
- DYNAMIC (use this by default): re-evaluated automatically by RevOS a
few times a day (every 6 hours), so it follows the data. A new DYNAMIC
segment shows
status: "PENDING"until the next scheduled evaluation — say so, so the user isn't surprised byobjectCountbeing empty right after saving. An edited one keeps its old status andobjectCountuntil then, though a table scoped by it uses the new filter at once. - STATIC freezes the membership at evaluation time — but evaluating is
something the MCP tools can't trigger. A STATIC segment created here stays
PENDINGuntil someone presses Evaluate in the RevOS UI, and while it's pending a table scoped to it shows all rows, unfiltered. Only create one when the user explicitly asks for a frozen snapshot, and tell them to run Evaluate in the UI before relying on it.
type can't be changed after creation.
5. Name it, write it, report back
name is required (1–255 chars); add a one-line description of the
audience in plain words. Before writing, check for a namesake —
api_read({ resource: "segments", params: { filter: "name == \"…\"" } }).
If one exists, ask whether they mean that segment before making a second.
Then api_write({ resource: "segments", data: { name, description, type, cubes, filter } }).
Read it back with api_read by the returned id and compare its
cubes and filter to what you sent. They should match; if they don't, the
write didn't land the way you intended — fix the call rather than reporting
success.
Then tell the user, in a sentence or two: the root (what one member is), the
conditions in plain language, the type, and that it's PENDING until
evaluated. If you made a judgment call (a measure instead of a fanning
dimension, a particular join path), say which and why.
Editing a segment
api_write with the segment's id and only the fields that change. The
catch: each field you send replaces the stored one whole. filter and
cubes are single fields — send a filter with one new condition and every
condition you left out is gone.
-
api_read({ resource: "segments", id })fresh, right before writing. -
Look up how any new value is stored before you add it. The user's word for it ("startup plan", "churned", "EMEA") came from conversation, not from the data, and the existing conditions only prove how their values are spelled. Query the dimension the new condition is on:
api_read { "resource": "cubes", "method": "query", "body": { "query": { "dimensions": ["crm_companies.plan"], "limit": 20 } } }Use the stored spelling. If no stored value matches what the user said, ask instead of writing a condition that matches nothing.
-
Start from the current
filter(andcubes) and change only what the user asked for. -
If the new condition references a cube that isn't in
cubesyet, add itsjoinPathentry and send the wholecubesarray too. If you removed the last condition on some cube, drop that entry. -
Don't send
type— it isn't updatable — and don't change the root (cubes[0]): the API accepts it, but every table and card built on the segment assumes the old one. For a different root, create a new segment. -
After the write,
api_readthe segment again and check thefilteris the one you meant to save — a bumpedversionalone doesn't prove the change landed.
Every update bumps the segment's version (the UI can restore older
versions — worth mentioning if the user is nervous about an edit). A STATIC
segment goes back to PENDING when its filter or cubes change, and every
table scoped by it then shows all rows until someone presses Evaluate
in the UI — warn the user before such an edit. If the segment scopes a
table's default view, check that meta still compiles after the edit.
Scoping a table's rows with a segment
Tables don't carry filters; their view does. To make a table show only a segment's members, point the table's default view at the segment:
api_read({ resource: "table-views", method: "list", id: "<TABLE id>" })— the list hangs off the table, so it's the table's id here. Take the entry withisDefault: true.api_write({ resource: "table-views", id: "<that VIEW's id>", data: { "segmentId": "<segment id>" } }).
An attached segment overrides the view's own ad-hoc filter (which is kept,
and comes back on detach) — send "filter": {} with segmentId when the
view has one. "segmentId": null detaches it. Use a segment whose root cube
is the cube the table's rows come from (its main stream) — that's how the UI
pairs them, and the server doesn't check it. If the segment is STATIC and
not READY, warn that the table will show every row until it's evaluated.
Then check that meta still compiles: the segment's conditions and joins
are now part of the table's view.
Troubleshooting a segment
api_read the segment and look at status:
PENDING— saved, not evaluated yet. Normal right after a create or edit; DYNAMIC catches up on its own schedule, STATIC needs Evaluate in the UI.EVALUATING— in progress; check back.FAILED—evaluationErrorhas the reason, usually Cube's. The common causes, in order of likelihood: a member that doesn't exist (re-read the cubes'metaand correct the name), or a condition value of the wrong kind for its member. Fix the segment with an edit as above; it'll be re-evaluated on the next run.READYbut the wrong members or count — the usual suspects are a fanning dimension condition (see step 3), a root cube that doesn't match what the user meant by "member", or a join path that took the other route — or one that isn't a real join at all: evaluation then silently drops the segment's join paths and counts along Cube's own route, rather than failing.
Deleting
api_delete({ resource: "segments", id }) removes a segment for good, and
for a STATIC one its snapshot of members too. There is no undo. Before the
call:
-
Read the segment and name it back: its type, root cube, conditions, and member count.
-
A STATIC segment's cube. Check this before anything else: it's what can make the delete impossible. A STATIC segment compiles into a cube,
segment_<id, - → _>. Look for that name in:- the tables'
streams, ids andjoinPaths (fields: "id,name,streams"): a table started from the segment is rooted on its cube; - the other segments'
cubesandfilter(full records); - the stored cubes'
joins(api_readoncubes,fields: "name,definition").
With the segment gone, any of these stops compiling, and the org's whole model goes down with it. Stop and report what you found, and don't change anything else for this delete yet.
- the tables'
-
Views. The delete is refused (
409) while any table view has the segment attached.usageCountis the number of them — it's only on alist, not a get:params: { filter: "name == \"<name>\"", fields: "id,name,usageCount" }. At 0, skip ahead. Otherwise find them: list the tables, then each table's views (table-views,method: "list", the TABLE's id,filter: "segmentId == \"<segment id>\""), until you've foundusageCountof them. Tell the user which tables it scopes. Detaching (segmentId: null) makes those tables show every row, so detach only with their yes. -
Dashboard cards. Not counted in
usageCountand not a reason for a409: a card keeps itssegmentIdafter the segment is gone, and the dashboard shows it as unavailable. Find them withapi_read { "resource": "dashboard-cards", "params": { "filter": "segmentId == \"<segment id>\"" } }and tell the user which dashboards lose which cards. -
Ask for an explicit yes for this segment. Approval to delete one doesn't cover another.
-
api_delete, then check thatmetastill compiles and the segment is gone from the list. Ifmetafails, there's nothing to restore: read the error, find what still names the segment's cube (the places in step 2), and tell the user what has to change.
A segment you created yourself in a failed attempt is no exception: say so, and ask before deleting it.
A 403 on any write — create, update, delete, or attaching to a view — means
the user's role can't change segments; it needs Member. Not reachable
through these tools: evaluating, duplicating, and restoring an older
version — send the user to the RevOS UI.