SKILL.md
Tables
A table, called a scoring model in the API, is a set of rows with typed columns:
- Rows come from the table's root cube: the cube of its first
stream, so a companies table has one row per
crm_companiesrecord. A table that stores its ownidcolumn holds its own rows instead. - Columns are one of three kinds:
- external: read live from a cube member. The column has
streamId(which cube) andpath(which member). - stored: values kept in RevOS itself, typed in or written by actions. These need no stream.
- action: runs an integration action per row and keeps the result.
- external: read live from a cube member. The column has
- Streams list the cubes the table reads:
{ "id": "<cube name>" }for the root, plusjoinPath, the way to it from the root, for every other one. A stream holds nothing else: the cube itself knows where its data comes from. - Views are layouts of the table. The default view also decides which rows show, through an attached segment or an ad-hoc filter.
You reach tables through the RevOS MCP server's generic tools:
| Call | What it does |
|---|---|
api_read { "resource": "tables", "params": { "fields": "id,name,updatedAt" } } | list tables |
api_read { "resource": "tables", "id": "<table id>" } | one table: objectsColumns, streams, views, scoringFeatures |
api_write { "resource": "tables", "data": { "name": "…" } } | create (name only) |
api_write { "resource": "tables", "id": "<table id>", "data": { … } } | update name, objectsColumns, streams |
api_read { "resource": "table-views", "method": "list", "id": "<TABLE id>" } | the table's views |
api_write { "resource": "table-views", "id": "<VIEW id>", "data": { … } } | update a view: segmentId, filter, columns |
api_delete { "resource": "tables", "id": "<table id>" } | delete a table, for good (see Deleting) |
The record always goes under data. A body under any other key may be
ignored without an error. api_details({ resource: "tables", operation: "update" }) is the authority on the column and stream shapes. There is no
column endpoint: columns change through the table's objectsColumns array.
The three things to keep in mind
1. objectsColumns and streams are replaced whole. An update stores
the array you send instead of the stored one:
- A column you leave out is removed from the table and from every view.
namealone is a safe partial update.- Sending
objectsColumnswithoutstreamskeeps the stored streams. - A stream disappears by itself with the last column that read it (the root
stays), so you rarely send
streamsexcept to add one. - The server tidies what it stores: a path on the root is dropped, and a
non-root stream sent without
joinPathis stored as<root>.<id>. Read the table back to see what it kept.
2. A table compiles into the org's semantic model. RevOS generates a
Cube view per table, model_<id, - → _>, from its streams and external
columns, plus its stored columns and scores through the table's own cube
(model_stream_<id, - → _>___local, see columns.md). The server doesn't
check them on save. Any of these breaks every table, segment, and
query in the org until it's fixed:
- a stream
idthat isn't a cube; - a
joinPathhop that isn't a declared join; - an external column whose
pathisn't a member of its stream's cube; - an empty table name.
3. One row per root record. A field from a "many" cube doesn't fit. Before you add a column from any cube other than the root, find out how many of that cube each root record has. Look at the join between the two cubes, from both sides, because it may be declared on either:
| join found | root has… | a dimension column from it |
|---|---|---|
root → other: many_to_one or one_to_one | one | fine |
other → root: one_to_many | one | fine |
root → other: one_to_many | many | don't add |
other → root: many_to_one (e.g. crm_deals → crm_companies) | many | don't add |
"Deal stage" on a companies table is the last row of that table: each company has many deals, so there is no single stage to show, and the column would repeat or scramble rows. Don't add it, even as a caveat. Stop and tell the user why. Then offer what does fit:
- an aggregate from that cube as a measure column (
measure: true): a count, a sum, or a filtered count such as "won deals", if the cube has one or one is added to it (the cubes skill); - the same field on a table rooted on that cube (the Deals table);
- a filter on the rows through a segment ("companies with a won deal").
So every write follows the same loop:
- Read fresh (
api_readthe table by id) right before writing. Keep theobjectsColumnsandstreamsyou got. They are your undo. - Write the arrays with only your change applied.
- Verify at once:
api_read { "resource": "cubes", "method": "meta", "params": { "fields": "name", "pageSize": 100 } }must succeed. An error means the org's model no longer compiles. Restore the saved arrays immediately, and then tell the user.- Query the table's view over the columns you touched:
api_read { "resource": "cubes", "method": "query", "body": { "query": { "dimensions": ["<view>.<column name>"], "limit": 3 } } }. The view is namedmodel_<table id with - replaced by _>, and its members are the column names. An error names the bad member.
- Read the table back and check that the columns are what you meant.
Reading a table
get the table and describe it in the user's terms:
- what one row is (the root cube);
- each column, with its display name, type, and where its value comes from
(
cube.member, stored in RevOS, or an action); - which segment or filter scopes the default view (
views[].segmentId/filter); - whether the table is scored (
scoringFeatures).
Don't dump the JSON. For the table's actual rows or aggregates, query its view or the underlying cubes (see query-semantic-model).
Creating and editing
Read the reference before writing:
- New table: references/create.md
- Change a table (add, change, remove, or hide columns; add a related cube; rename; scope its rows): references/edit.md
- Column and stream rules (types, naming,
path,measure, action columns, join paths): references/columns.md
The cubes behind a table are a separate thing. If the member a column needs doesn't exist yet, it has to be added to the cube first (the cubes skill). Audiences are a separate thing too: build a segment with the segments skill, then attach it here.
Deleting
api_delete on tables removes the table for good. Nothing is soft
deleted, and there is no undo. It takes with it:
- the stored column values and the action results;
- the scores and the change history;
- the table's views, its view in the semantic model, and its own cube.
So never delete on an inference. Before the call:
-
Read the table and name it back to the user: what a row is, how many columns, which are stored or action columns, and whether it is scored.
-
Check what reads the table's cubes: its view
model_<id, - → _>and, if it has stored columns or scores, its own cubemodel_stream_<id, - → _>___local. Look for either name in:- the other tables'
streams, ids andjoinPaths (fields: "id,name,streams"); - the segments'
cubesandfilter(list full records: neither is infields); - the stored cubes'
joins(api_readoncubes,fields: "name,definition").
That covers views too: another table's view filter can only name a member of a cube that table joins, so it shows up in its
streams. Anything you find stops compiling with the table gone, and takes the org's whole model down with it (rule 2). Report it and stop: it has to be changed first, with the user, through its own skill. - the other tables'
-
Say what goes with it (the list above), and ask for an explicit yes for this table. Approval to delete one table doesn't cover another.
-
Then
api_delete, and confirm thatmetastill compiles and the table is gone from thelist. The delete drops several stores in turn and isn't atomic: if it errors or times out, read the list before trying again, and tell the user what state the table is in. -
If
metafails after the delete, there's nothing to restore: the table is gone. Don't try fixes at random. Read the error, which names the cube or member that's missing, find what still points at it (the places in step 2), and tell the user what has to change. Change it only with their yes.
The cubes and segments the table read stay, and so do the connected
systems: deleting a table writes nothing back to them. A table you created
yourself in a failed attempt is no exception: say so, and ask before
deleting it. A 403 means the user's role can't delete tables; someone
with a higher role in the organization has to do it.
Reporting back
After a write, tell the user:
- what changed, in their terms ("added Industry, read from the company");
- that the model compiled and the view returned rows;
- any judgment call you made, such as the join path or the column type.
If you had to restore, say so plainly, along with the error.