Skip to content

Semantic layer workflows

21 min read · Updated 28 Oct 2026

Use the Nash MCP gateway to inspect and change semantic layers without editing the stored document directly. Describe a change in business terms and the server resolves it against the model, builds the definition, validates it, and protects the write with a revision check.

Connect your MCP client by following MCP setup. Semantic-layer reads require sl:read, edits require sl:write, and creating versions or importing a Snowflake semantic view also requires sl:version.

When you open the Semantic Layer Editor from a workbook, Back to Workbook in the header returns to that workbook and its originating sheet. The context stays available while you move between editor tabs and after refreshing the page, including when several workbooks use the same semantic layer. Opening the editor directly shows Create Workbook instead.

A period comparison can use arithmetic over field aggregates — the editor shows such a measure as SUM(Salesamount) / COUNT(DISTINCT SalesOrderNumber), which is stored as expression: '{m1} / {m2}' with each aggregate in its own m1/m2 block. Create a Time intelligence axis dimension, then enable Period over period settings on the measure’s Advanced tab and select that axis. Each period evaluates the ratio from its separately aggregated numerator and denominator; it does not average individual row ratios.

The formula is evaluated at the requested result grain. With a daily axis, each row compares daily values; without a visible axis, the KPI recalculates the formula over the whole selected period. A customer appearing on several days is counted once in that period’s distinct customer count. Daily ratios are not added together to produce the KPI. Growth is (current - previous) / ABS(previous); set a percentage format to display it as a percentage.

Whole-period comparisons without a visible axis do not support Time intelligence axis ranges whose boundary is the source data’s minimum or maximum date.

Use an explicit nestedCalculation when you want a reduction of per-bucket values. They are different from an ordinary period comparison: a nested SUM, for example, adds values calculated at its additional dimension’s grain. See Period axes and comparisons for the aligned YAML definition and Business Questions feature availability.

A comparison is computed over aggregate arithmetic. A formula that references other measures — selecting one, or combining several — is supported when every referenced measure is itself such an aggregate: it is computed over the aggregates they name. Unaggregated fields, conditional formulas, and references to measures that carry their own comparison or declare parameters are not supported as comparison bases.

When your authorization includes sl:read, call astrato_get_context at the start of the session. It returns:

  • your permissions and scopes;
  • the configured default semantic layer, if one exists;
  • the current draft revision and how revision checks work;
  • the supported authoring workflows.

A data-only client holding data:query cannot call astrato_get_context. Use tools/list and the server-provided instructions, then start the data flow with astrato_data_prepare_query.

If you already know which layer to use, pass its semanticLayerId to every subsequent tool that accepts it. Session-scoped calls carry the selected layer through sessionId. If the layer is not known, use the default returned by astrato_get_context. When there is no default, call astrato_semantic_layer_list, review the available layers, and select the intended one. If it is not clear which layer applies, stop and ask before continuing. The list is paginated: follow pagination.nextOffset while pagination.hasMore is true when the requested layer is not in the current page. Do not switch layers implicitly.

Use the smallest read that answers the question:

  1. astrato_semantic_layer_search for a focused question about a view, field, join, measure, dimension, or function.
  2. astrato_semantic_layer_get_summary for counts and current revision.
  3. astrato_semantic_layer_get_details for one paginated collection. Follow nextCursor only when you need more results.
  4. astrato_semantic_layer_validate when you need to check correctness and before treating an edit as complete.

Do not fetch the entire model for a focused change. The prepare tools use the same ranked field matching as astrato_semantic_layer_search_fields, so there is no need to search first when prepare already resolves the requested fields.

Deterministic integrations that need the whole model use the snapshot returned by astrato_semantic_layer_get_summary instead of paginating every detail collection. The snapshot is the complete current draft as Semantic Layer V3 JSON, including measure definitions that compact detail pages intentionally omit.

After initializing the MCP connection, send this JSON-RPC request:

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "astrato_semantic_layer_get_summary",
"arguments": {
"semanticLayerId": "11111111-1111-4111-8111-111111111111",
"includeSnapshot": true
}
}
}

Find the item in result.content whose type is resource_link, then pass its uri to resources/read:

{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": {
"uri": "nash-mcp://resources/<resource-id>"
}
}

Read the resource in the same MCP session that created the link. Parse result.contents[0].text as JSON; it is the complete fileVersion: 3 document. The text result also contains these artifact fields:

  • filename, mimeType and UTF-8 size;
  • snapshotRevision, the exact draft revision in the resource;
  • publishedRevision, or null when the layer has never been published;
  • publicationState: published, unpublished_changes, never_published, or superseded.

The text result’s data.resourceUri and data.snapshotResourceUri contain the same URI when the attachment is requested. Without includeSnapshot: true, the server does not serialize or store a snapshot and neither URI is present. Omit the flag for ordinary summaries and bounded detail reads.

superseded means a newer revision was published while this point-in-time snapshot was being prepared. Read the summary again before claiming that snapshot is current. The resource is JSON; serialize it to YAML locally when a YAML artifact is required.

To create a semantic layer that does not exist yet, call astrato_data_list_sources, choose the exact datasourceId, then call astrato_semantic_layer_create. The new layer is empty and unpublished. Astrato may rename it to avoid a collision, so use the returned name in the response and pass the returned semanticLayerId to every later authoring call.

Requested change Workflow
One measure astrato_semantic_layer_prepare_measure_change → astrato_semantic_layer_apply_measure_change
One field, bucket, or drilldown dimension astrato_semantic_layer_prepare_dimension_change → astrato_semantic_layer_apply_dimension_change
Several independent measures or dimensions astrato_semantic_layer_plan_authoring_batch → astrato_semantic_layer_commit_authoring_batch
One or more view and field changes Optional source search → astrato_semantic_layer_prepare_view_change → astrato_semantic_layer_apply_view_change
A supported join, function, or description edit astrato_semantic_layer_begin_edit_session → astrato_semantic_layer_apply_edit → astrato_semantic_layer_preview_edit_session → astrato_semantic_layer_commit_edit_session
Upstream datatype or conservative physical-field rename astrato_semantic_layer_check_source_schema → confirmation → astrato_semantic_layer_apply_source_schema_fix
Snowflake semantic-view import astrato_semantic_layer_list_snowflake_views → astrato_semantic_layer_preview_snowflake_view_import → confirmation → astrato_semantic_layer_import_snowflake_view

Do not use a generic edit session for structural measure, dimension, view, or field changes. The typed tools accept structured business requests and are the supported MCP workflows. A description-only update, including a dimension description, uses the edit session’s batched update_descriptions operation.

Prepare once with the complete request and every concept needed by the definition. The result contains field matches, available aggregations, an existing target when applicable, and the current revision. If several matches are plausible, resolve the ambiguity instead of choosing one by guess.

This example prepares a new aggregate measure:

{
"semanticLayerId": "11111111-1111-4111-8111-111111111111",
"request": "Create Total Order Amount as the sum of Order Amount",
"queries": [
{
"id": "orderAmount",
"text": "Order Amount",
"kinds": ["field"],
"dataTypes": ["numeric"]
}
]
}

Pass that object to astrato_semantic_layer_prepare_measure_change. When it finds an exact match and confirms that SUM is available, apply the returned names and revision in one call:

Set includeSuggestions: true when you also want create-ready measure suggestions from the semantic engine. Set includeCompiledSql: true to receive statement-only SQL for the prepared measure; on a create request it returns and compiles the suggested measures. Dimension preparation supports the same includeSuggestions option. These optional operations use the current draft and never publish it.

For repeated deterministic measure preparation, set compact: true. The compact response preserves the revision, exact field and measure references, aggregation capabilities, target definition and relevant joins. It omits repeated locale, relative-period and authoring-rule metadata, plus candidate descriptions and duplicate confidence fields. Omit compact when a person or model needs that display context; the default response is unchanged.

{
"semanticLayerId": "11111111-1111-4111-8111-111111111111",
"preparedTarget": null,
"expectedRevision": 42,
"change": {
"operation": "create",
"name": "Total Order Amount",
"definition": {
"description": "Total value of orders.",
"inputs": [
{
"id": "orderAmount",
"kind": "aggregate",
"field": { "view": "Orders", "field": "Order Amount" },
"aggregation": "SUM"
}
]
}
}
}

The UUID and revision above are examples. Use the selected semantic-layer ID, keep the input IDs consistent between prepare and apply, and copy the exact view, field, aggregation, revision, and preparedTarget returned by prepare. The apply call validates the change and commits at most one draft revision. It never publishes the layer.

To aggregate arithmetic over each row, give an aggregate input row instead of field: an expression over names in braces, and fields mapping each name to its view and field. {"expression": "{price} * {quantity}", "fields": {"price": {...}, "quantity": {...}}} stores SUM(Price * Quantity). A row cannot contain a number yet, so write SUM(Price * (1 - Discount)) as two inputs, SUM(Price) and SUM(Price * Discount), with the expression {m1} - {m2}.

A conditional measure is an IF written as the whole expression over the input IDs, for example IF(sales > 1000000, 'Large', IF(sales > 100000, 'Medium', 'Small')) with one aggregate input sales. The rules it follows are listed under Stored measure expression rules.

A parameter input takes dataType int or float; it is float when omitted, and an int default must be a whole number. To change the default of a parameter a measure already declares, patch it by name without resending the calculation:

{ "operation": "patch", "name": "Rolling Sales", "patch": { "parameters": { "periods": 6 } } }

Each parameter keeps its stored type. A name the measure does not declare is refused with the names it does.

A field dimension can group a date or time field by dateLevel (such as month) or extract a datePart (such as quarter). Use a name from the dateLevels or dateParts that dimension preparation lists for the datasource; the names vary by database. Names match without regard to case. A query’s local dimension is checked against the same lists, and so is a date transform in a query’s or business question’s filter or in the filter of a measure the query defines. A name the datasource does not offer is refused before the query runs, with the names it does. A stored measure’s, view’s or dataset’s filter is not checked. dayOfWeek, weekday and dayOfMonth are not names: use dayofweekiso, which counts Monday as 1 and Sunday as 7, and day for the day of the month.

A periodAxis dimension (a Time Axis) reads one date field, or several sources when fact tables that do not join share one axis, such as order date and return date. Each source is { "field": { "view": ..., "field": ... } } or { "dimension": ... }, a field dimension over a date or timestamp field with no date level or part. An axis takes at most one source per view.

A replace keeps what its definition leaves out: the label, synonyms, categories and import record, and on a measure the aggregate settings the authoring fields do not carry. Send label or categories to change them. To change one bucket bound or the Time Axis window, patch bucketing or timeAxis instead of replacing the dimension. A delete takes references: remove deletes the item with what depends on it, as the editor’s Delete references does, and keep, the default, leaves those references broken and lists them under publishBlockers. Rename takes references: keep to rename only the item. duplicate copies a measure or dimension as Copy of its name unless newName is given.

Use the same expression forms as the visual measure editor:

  • Arithmetic supports +, -, *, / and parentheses, with numeric values, numeric parameters, and compatible aggregates or stored measures. Negative numbers such as -0.1 are supported; negating a reference or group, such as -(Revenue + Cost), is not.
  • One row aggregation can wrap arithmetic over fields, for example SUM(Price * Quantity). Values, parameters and nested aggregations are not supported inside that row expression. Choose an aggregation from the datasource capabilities; use the aggregation’s distinct setting rather than inventing a function name such as COUNT_DISTINCT. When the connection’s COUNT supports distinct values, a request for COUNT_DISTINCT, countDistinct, COUNTD or DISTINCT_COUNT is refused with a message that names COUNT with distinct: true.
  • The fields of one row expression must sit on one row: one of their views reaches every other through many-to-one or one-to-one joins, followed in their declared direction. Quantity × a part’s retail price, over an order line joined many-to-one to its part, passes. Supply cost × quantity, across a part-supplier view and order lines with no join from one to the other, is refused as cross-grain-row-expression, because the engine would read one side as empty and return a wrong total. Add a many-to-one join on every field that identifies the other view’s row (a composite join), or a custom SQL view that computes the value; for separate totals, aggregate each view in its own operand. An aggregation with fanout: "allow" or fanout: "reduceToRelation" also reaches across many-to-many joins. Both enable fanout; reduceToRelation additionally reduces to the relation’s dimensionality. Neither bridges two views that only share an attribute, such as both joining to a part.
  • IF requires a condition, THEN value and ELSE value. Conditions use measure comparisons or null checks, joined with AND/OR and parentheses. Branches can contain compatible measure references, literal values or another IF. Arithmetic and parameters inside IF are not supported, and IF cannot be embedded in arithmetic.
  • Numeric values must be finite and filled in. Comparison and branch types must be compatible; a null branch needs a non-null alternative. Text, Boolean and null literal comparisons support equality/inequality or null checks.
  • General scalar functions such as DATEDIFF, CURRENT_DATE, ABS, ROUND and COALESCE are not stored-measure expression functions. SQL fields and aggregate filters have separate rules.

The examples above describe the calculation; typed authoring expressions refer to the input IDs provided in definition.inputs. Authoring tools reject unsupported expressions with a measure and expression-path diagnostic. Manually edited YAML can remain saved as a repairable draft with errors; publication requires a data connection with available capabilities and those errors to be resolved.

Dimensions use the same pattern. Prepare all field or reusable-dimension concepts together, then apply one typed field, bucket, or drillthrough definition. The server derives result types and can resolve omitted numeric bucket ranges from data.

A filter on an aggregate input, and each rule of a custom-group (mapping) dimension, is a list of conditions, each naming a field, an operator and a value. They are stored exactly as the editor’s filter builder stores them. Values are typed from the field, so no data type needs to be supplied.

Operator Value
equals, not_equals, greater_than, greater_than_or_equal, less_than, less_than_or_equal one value, another field, or a single date (below)
contains, not_contains, like, not_like one text value
between, not_between two values, [from, to]; between also a date range
in, not_in a list of one or more values
is_set, is_not_set, hide_not_set none; hide_not_set drops null and zero or empty values
geo_intersects a polygon, on a geography field

A single-value operator given a list is refused; use in or not_in for a list.

like and not_like ignore case and use Astrato’s search syntax, not SQL: * matches any run of characters, ? matches one character, and \ escapes either. % and _ match themselves, so LG* keeps text starting with LG and *BRASS text ending with BRASS. A value with no * or ? matches anywhere in the text, as contains does.

A value written with a SQL % wildcard, such as LG%, would match only text containing “LG%”: like would keep no rows and not_like every row. astrato_data_execute_query refuses one, and so does an MCP authoring tool when its edit writes a value the layer does not already hold; each names the pattern to send instead. An edit that keeps a stored value, such as renaming the measure or a field it filters on, is not refused. To match a literal %, use contains or write \%. Layer validation shows the same problem as a warning on filters already stored.

A custom group rule is the exception: it still compiles like as SQL LIKE, so there % and _ are the wildcards and * and ? match themselves.

To compare two fields of the same row, give {"kind": "field", "field": {"view": ..., "field": ...}} with equals, not_equals or one of the four comparisons, for example orders shipped after they were due. Both fields must be of the same kind (number, date, text or Boolean).

An aggregate filter on another view’s field works only when each row the measure aggregates joins at most one row of that view, directly or through other views. When a row joins many, the filter cannot be evaluated: the measure still saves, but a question that uses it is refused when it executes; see Filters that cannot reach a measure’s rows.

A date or timestamp field also takes a generated date, resolved when the query runs:

  • {"kind": "relative_period", "period": ...} with between: year_to_date, month_to_date, quarter_to_date, week_to_date, their previous_…_to_date forms, extract_year_to_date and extract_month_to_date (the same to-date span in every year or month), last_12_months, last_90_days, last_30_days, last_7_days, current_year, current_quarter, current_month, current_week, previous_year, previous_quarter, previous_month, previous_week, today, yesterday, and minimum_date and maximum_date (the earliest or latest date in the data).
  • {"kind": "predefined_date", "date": ...} with equals, not_equals or a comparison: beginning_of_year, beginning_of_quarter, beginning_of_month, beginning_of_week, their beginning_of_previous_… forms, end_of_year, end_of_quarter, end_of_month, end_of_week, today and yesterday.
  • {"kind": "relative_date", "unit": "month", "offset": -1}, compared like a predefined date.
  • {"kind": "relative_range", "unit": "day", "from": -7, "to": 0} with between; from must not be after to.

Units are year, quarter, month, week, day, hour, minute and second. An operator a date form cannot take is refused with the one to use. A fixed date is written YYYY-MM-DD. Custom-group rules are compiled into a column once, so they accept fixed dates only.

geo_intersects takes {"kind": "polygon", "points": [[longitude, latitude], ...]}: at least four points, the last repeating the first, with longitudes from -180 to 180 and latitudes from -90 to 90.

The user attributes $userEmail and $userGroupNames are refused in both places. A stored measure would compare the parameter’s stored default rather than the requesting user, and a custom group would compile that default into its column. To restrict rows per viewer, add a security rule to the table in the editor, where the attribute resolves to the requesting user.

The editor’s measure SQL view and the optional prepare-tool suggestions use the current draft, so unpublished edits appear immediately, and they never publish it. A draft with blocking validation errors cannot run these operations; correct the reported document issues first.

Compiled SQL is a statement preview only. It does not execute data, perform a new authorization or datasource lookup, or return query warnings and resolved items.

Plan independent measure and dimension requests together. For a proposed measure creation, include a measure query for the intended use case so planning can find an exact existing measure instead of creating a duplicate.

{
"semanticLayerId": "11111111-1111-4111-8111-111111111111",
"items": [
{
"id": "totalOrderAmount",
"entity": "measure",
"operation": "create",
"request": "Create Total Order Amount as the sum of Order Amount",
"queries": [
{ "id": "existingTotal", "text": "Total Order Amount", "kinds": ["measure"] },
{ "id": "orderAmount", "text": "Order Amount", "kinds": ["field"], "dataTypes": ["numeric"] }
]
},
{
"id": "orderDate",
"entity": "dimension",
"operation": "create",
"request": "Create an Order Date field dimension",
"queries": [{ "id": "orderDateField", "text": "Order Date", "kinds": ["field"], "dataTypes": ["date"] }]
}
]
}

Call astrato_semantic_layer_plan_authoring_batch with that object and review the complete plan. Commit only when every item is ready and the plan has not found an existing item that should be reused. Copy the returned revision, optional planId, and each preparedTarget into astrato_semantic_layer_commit_authoring_batch with the complete typed changes:

{
"semanticLayerId": "11111111-1111-4111-8111-111111111111",
"planId": "22222222-2222-4222-8222-222222222222",
"expectedRevision": 42,
"items": [
{
"id": "totalOrderAmount",
"entity": "measure",
"preparedTarget": null,
"change": {
"operation": "create",
"name": "Total Order Amount",
"definition": {
"inputs": [
{
"id": "orderAmount",
"kind": "aggregate",
"field": { "view": "Orders", "field": "Order Amount" },
"aggregation": "SUM"
}
]
}
}
},
{
"id": "orderDate",
"entity": "dimension",
"preparedTarget": null,
"change": {
"operation": "create",
"name": "Order Date",
"definition": {
"kind": "field",
"field": { "view": "Orders", "field": "Order Date" }
}
}
}
]
}

Copy planId, the revision, every preparedTarget, and all resolved views, fields, and aggregations from the plan. Keep the item and input IDs consistent. If the plan finds an exact existing use case, reuse it instead of creating a duplicate. The batch is all-or-nothing: if one item fails compilation or validation, nothing is written. A successful commit creates at most one draft revision and returns a receipt for every item.

Author views and fields from physical sources

Section titled “Author views and fields from physical sources”

Use astrato_semantic_layer_prepare_view_change and astrato_semantic_layer_apply_view_change for table, SQL, and inline view creation; view copy, rename, metadata/source update, and deletion; and field creation, update, rename, and deletion. One ordered apply call can change several views and fields and commits at most one draft revision without publishing.

When physical-source discovery is required, call astrato_semantic_layer_search_source_catalog with ordinary business terms, or pass all source concepts to prepare so it can resolve them together. Keep the exact schema, table, column identifiers, and casing returned by the source catalog. Never invent a source identifier when discovery is ambiguous or empty.

Source discovery returns a progressive informationSchemaUri. Reuse that URI in later searches and in view prepare/apply calls so previously loaded branches are not fetched again. Broad searches are bounded; continue with nextSchemaOffset when it is returned. The resource expires after ten minutes without use, after which omitting the URI starts a fresh context.

Call astrato_semantic_layer_refresh_source_catalog only when you need current source metadata. With no branch it refreshes everything already loaded. A schema refreshes its table list and loaded columns; a table refreshes that table and its columns. Refresh keeps the same URI and does not discover unrelated branches.

Catalog-backed table creation can select all fields, an explicit subset, or all except an explicit list. Deletes that would break a join, dimension, measure, or filter are rejected instead of cascading silently. Copy every resolved source and revision from prepare into apply.

Some datasource-native values need a lossless semantic representation. View preparation returns nativeDataTypeOptions for the active datasource. Each option declares the exact nativeDataType, semantic dataType, and sourceType combination accepted by the engine. Use only those returned combinations and only on physical column fields; inline columns and expression fields cannot use datasource-native types.

For Google BigQuery, the currently supported option is:

{
"nativeDataType": "INT64",
"dataType": "string",
"sourceType": "column"
}

This keeps values larger than JavaScript’s lossless number range as strings while queries continue to use BigQuery INT64. Setting another semantic datatype removes the native override. Source checks compare the stored native datatype with the raw datatype currently reported by the database; an actual upstream change is reported rather than hidden by the override.

Use an edit session for focused join, function, and description changes supported by astrato_semantic_layer_apply_edit. Description-only changes use the batched update_descriptions operation.

  1. Begin a session. Its base revision pins the work to a known draft state.
  2. Apply one or more structured edits. They remain session-local.
  3. Preview the complete diff and validation result.
  4. Commit the session when the preview is valid. Roll it back only if you intend to discard every pending change.

Sessions expire after 30 minutes of inactivity by default and belong to the current tenant and identity. A commit may rebase non-overlapping concurrent changes. If the same entity changed elsewhere, re-read the layer and begin a fresh session.

astrato_semantic_layer_validate checks the semantic document. To check for database drift, call astrato_semantic_layer_check_source_schema with fresh: true. A fresh check requires write access and may create one draft revision when the database confirms that a referenced object is missing or has been restored. Datatype changes and likely renames based on casing or separators remain suggestions.

Review every suggested fix and apply it only after explicit confirmation. Pass the exact runId, entityKey, and revision returned by the check to astrato_semantic_layer_apply_source_schema_fix. See Semantic layer issues for the meaning of each issue origin.

List the views available on the datasource, then preview the selected import. Review the conversion counts and diagnostics before continuing. Import only after the preview is confirmed, and copy its revision and source hash exactly. Import creates a draft revision and a named version, but does not publish the layer.

New imports preserve case-distinct physical columns and their references. For example, Snowflake expressions "YEAR" and "Year" import as separate fields. Unquoted column identifiers use Snowflake’s uppercase spelling. Semantic layers imported earlier are not updated automatically.

Average metrics written as AVG(...) retain the AVG aggregation in the imported measure. The data connection’s normal aggregation restrictions still apply.

Snowflake filters (a fact or dimension labelled FILTER, and the standalone filters of a table) import as Boolean dimensions in the Filters category. A filter fact is never turned into a measure. A metric that conditions on a filter, such as SUM(IFF(discounted_line, revenue, 0)), imports as a measure with that filter. Labels other than FILTER are still not imported and are reported in the preview diagnostics.

The imported layer records which semantic view, and which element of it, each table, field, join, dimension, and measure came from. The Semantic Layer Editor uses this to report later changes to the view; see When an imported Snowflake semantic view changes.

  • astrato_semantic_layer_create_version creates a named, durable version of the draft head at call time. It does not publish. The tool does not accept expectedRevision, and its result does not identify the revision that was captured.
  • astrato_semantic_layer_export_ossie exports the exact current validated V3 draft as Apache Ossie YAML. It does not publish. Advanced measures are retained as clearly labelled comment-only engine SQL; see Apache Ossie export.
  • astrato_semantic_layer_export_snowflake_semantic_view writes the SQL that creates a Snowflake-backed layer as a Snowflake semantic view, for you to run in Snowflake. It does not publish or run anything; see Create a Snowflake semantic view.
  • astrato_semantic_layer_publish is a separate write. Call it only when publishing was explicitly requested, and pass the exact revision that was confirmed. After publishing, it rates the joins that publish added or changed with the engine’s join-quality check and returns them under joinQuality. Tell the user about every join listed under needsAttention, with its recommendation. The check never blocks or fails the publish. When it cannot run, notMeasured says why.

To rate joins before publishing, call astrato_semantic_layer_check_join_quality. It measures the draft’s joins. Queries keep using the published joins until you publish, so the result lists under publishedJoinsNotInDraft the published joins that the draft removes or changes. A join on a view that is new or changed in the draft is measured on the draft when your workspace routes draft analysis through an exit node. Otherwise it is listed under needsAttention with the status awaitingPublish until you publish. The same applies to every join when the draft uses another data connection.

When you create a version and publish in the same workflow, validate revision R, create the version, then immediately re-read the summary. Publish with expectedRevision: R only if the summary still reports R. If the revision changed, stop: the version may have captured a different draft. The publish call’s expected-revision check protects against another edit after the re-read.

After any write, re-read the summary before relying on the current revision. Another editor may have committed after the tool response.

  • Ambiguous match: review the alternatives and resolve the ambiguity before continuing. Do not guess.
  • Stale revision: re-read the layer, then prepare or plan the change again.
  • Validation failure: report the affected entity and diagnostics; do not publish or retry with invented data.
  • Typed-workflow no-op: report that the layer was already in the requested state. Atomic and batch tools do not create a revision for a no-op.
  • Missing scope or unavailable MCP endpoint: report the access needed. Do not fall back to undocumented HTTP calls or raw V3 edits.