Governed SQL API and CLI reference
Use the Semantic Rails Python runtime to discover metrics, build and validate
semantic queries, compile governed SQL, and execute against your warehouse.
This reference covers the open-source engine you run yourself: its CLI and the HTTP
routes served under /api/v1/*. Semantic Rails Cloud serves the same
calls at workspace URLs with its own API keys; see
Semantic Rails Cloud runtime API.
Connecting an AI assistant? Start with the MCP client setup guide. For the order of tool calls, see the agent query workflow. Define the models, metrics, and policies these queries use with the semantic layer YAML reference.
CLI entrypoints
uv run semantic-rails packages uv run semantic-rails catalog --package jaffle_shop uv run semantic-rails discover --package jaffle_shop --terms store_name uv run semantic-rails inspect --package jaffle_shop --object-id measure.jaffle.order_count uv run semantic-rails build-options --package jaffle_shop --query-json '@query.json' --step group_by uv run semantic-rails valid-values --package jaffle_shop --dimension dimension.jaffle_store_name uv run semantic-rails plan --package jaffle_shop --intent "new customer orders over time" uv run semantic-rails validate --package jaffle_shop --query-json '@query.json' uv run semantic-rails compile --package jaffle_shop --query-json '@query.json' uv run semantic-rails query --package jaffle_shop --query-json '@query.json' --verbosity minimal --sql-profile off uv run semantic-rails segment-preview --package jaffle_shop --segment-id segment.jaffle.high_value_customers uv run semantic-rails serve --package jaffle_shop --port 8081 PACKAGE_PATH="$(pwd)/my_package" semantic-rails profile init --package-path "$PACKAGE_PATH" semantic-rails mcp setup --path "$PACKAGE_PATH" semantic-rails mcp setup --path "$PACKAGE_PATH" --client both --mcp both --install --yes uv run semantic-rails doctor --package jaffle_shop semantic-rails serve --path "$PACKAGE_PATH" --host 127.0.0.1 --port 8091
serve runs in the foreground; stop it with Ctrl-C. It serves the HTTP
API and stateless Streamable HTTP at /mcp. See the
HTTP API and MCP setup for request headers,
API-key authentication, and trusted caller context.
HTTP routes
Use the /api/v1 prefix for integrations. Root paths and unversioned
/api/* paths are not public routes.
This table mirrors docs/QUERY_API.md, which is the canonical contract;
request/response shapes and error codes live there.
| Route | Purpose |
|---|---|
GET /api/v1/health |
Return service, package, schema, and warehouse metadata without warehouse I/O |
GET /api/v1/ready |
Return package readiness; protected deep checks can verify warehouse connectivity |
GET /api/v1/capabilities |
Return route index, supported API versions, aliases, warehouse capabilities, and package capabilities |
GET /api/v1/catalog or POST /api/v1/catalog |
Browse catalog metadata, public objects, value domains, and capability flags |
POST /api/v1/discover |
Rank semantic candidates from business terms; surfaces out_of_scope and low_relevance blocks inline |
POST /api/v1/inspect |
Inspect an object card with usage and compatible next moves |
POST /api/v1/build-options |
Get recommended, available, blocked, and patch-based builder choices |
POST /api/v1/valid-values |
Get valid categorical values for a dimension |
POST /api/v1/plan |
Produce validated best-query drafts and alternatives |
POST /api/v1/validate |
Validate a query without executing it |
POST /api/v1/compile |
Compile Query IR to rendered SQL and plan metadata without execution; the explain payload ships inside this response |
POST /api/v1/query |
Execute a validated query against the configured warehouse |
POST /api/v1/segment-validate |
Validate a package-authored segment membership query |
POST /api/v1/segment-explain |
Return planner and SQL details for a segment membership query |
POST /api/v1/segment-preview |
Preview membership rows for a package-authored segment |
Response envelope
Most routes return ok, status, api_version,
request_id, package_id and timing_ms
alongside route-specific payloads, plus warnings, errors
and recovery_hints, each only when it has entries. Runtime calls may
also include request_context when trusted semantic context headers are
supplied.
Request context and API key shim
Integrations can pass X-Request-ID, X-Semantic-Actor,
X-Semantic-Tenant, X-Semantic-Project,
X-Semantic-Roles, X-Semantic-Environment, and
X-Semantic-Audience. The default resolver reads who is asking from these
headers only; a policy_context in the request body is refused with
IDENTITY_CLAIM_REFUSED.
Authentication is off by default. Set SEMANTIC_RAILS_API_KEYS or
SEMANTIC_RAILS_API_KEY_FILE to require a key on protected routes, sent as
Authorization: Bearer <key>, X-API-Key or
X-Semantic-API-Key; GET /api/v1/health stays public. This is
the self-hosted engine's runtime shim. Semantic Rails Cloud uses its own API keys and
the ApiKey scheme; see Semantic Rails Cloud runtime API.
The query call
validate, compile and query take a query call:
the question, naming the package's ids. Over HTTP the body is the call under
query, with request options beside it in options:
now (the instant relative windows resolve against),
verbosity and sql_profile. Any other key beside
query and options is refused. This is the call shape of
engine 0.3.2; the full reference is
QUERY_CALL.md
and the JSON Schema is
schemas/query.v2.json.
{
"query": {
"select": ["measure.jaffle.order_count", {"expression": "metric.sales.aov_usd", "as": "aov_usd"}],
"group_by": ["dimension.jaffle_store_name"],
"where": [{"field": "dimension.jaffle_store_name", "op": "not", "value": "Brooklyn"}],
"having": [{"field": "order_count", "op": "gt", "value": 0}],
"time": {
"temporal_role": "temporal_role.jaffle_order_time",
"grain": "month",
"start": "2016-09-01",
"end": "2016-12-01"
},
"order_by": [{"field": "order_count", "direction": "desc"}],
"limit": 25
},
"options": {"now": "2017-09-04T12:00:00Z", "verbosity": "compact"}
}
| Field | Shape | Meaning |
|---|---|---|
select |
list of measure or metric ids, or {expression, as} |
What to measure. A reference is the id string itself. |
group_by |
list of dimension ids | How to split. Each becomes a column named by its id. |
where |
list of {field, op, value} conditions and entity filters |
Which rows and entities count. Items are ANDed; a list value is an OR. |
having |
list of {field, op, value} on results |
Which result rows to keep, after aggregation. field is a select alias or a measure or metric id. |
time |
{temporal_role, grain, last | this | start/end, fill} |
The clock, the window and the grain. |
order_by |
list of {field, direction} |
A select alias, a group_by id or period; asc or desc. |
limit |
integer ≥ 1 | The question's top N. |
routes |
list of {from, to, via} |
The join route this question means. |
observation_scope |
dataset | query |
Which rows count as "has data". |
-
Filter words (
op):is,not,gt,gte,lt,lte,contains,starts_with,ends_with, theirnot_forms, andmissing(trueorfalse).notkeeps rows whose value is unknown. There is nobetween: a range is two conditions,gteandlt. -
time.startis inclusive andtime.endexclusive: the window is[start, end), so the example covers September through November 2016."last": {"unit", "value"}and"this": "<unit>"are relative windows that resolve againstnow. -
With
time.grainthe answer has one row per period in aperiodcolumn. A window without a grain is one total per group, with no time column. -
The previous call shape is refused at its first retired key, with where it went:
{"measure": …}and{"metric": …}objects become the id string,metric_filtersbecomeshavingor an entity filter,time.rangebecomestime.lastorstart/end, and"DESC"becomes"desc".versionis ignored.
Compile and explain metadata
POST /api/v1/compile returns rendered SQL plus semantic plan metadata.
At compact verbosity, detailed plans live under explain;
at full verbosity, physical_plan and
performance_plan are also top-level response fields.
When a package declares exact physical rollups through model.variants:
or explicit aggregate relations, the compile response shows whether a query routed to
a rollup:
logical_plan.measure_plans[].aggregate_relation_idphysical_plan.nodes[].details.selected_relation_typephysical_plan.nodes[].details.aggregate_relation_idperformance_plan.aggregate_routing.selected
Missing dimensions, unsupported time grains, non-exact equivalence, non-default sources, or unsupported rollups fall back to the raw relation.
Discovery-first request example
{
"query": {
"select": [{"expression": "measure.jaffle.order_count", "as": "orders"}]
},
"focus_terms": "orders by store",
"focus_object_id": "measure.jaffle.order_count",
"step": "group_by",
"verbosity": "compact",
"limit": 10
}
Error model
For the authoring-side root cause of each error and the field that typically triggers it, see the error code → root cause table in the Authoring Reference.
| Code | Meaning | Typical fix |
|---|---|---|
AMBIGUOUS_ALIAS |
Human token maps to multiple semantic objects | Use the returned candidates and pick a stable ID |
AMBIGUOUS_PATH |
Multiple non-equivalent join paths remain valid | Change the query shape or root so the planner has one safe path |
FANOUT_UNSAFE |
Requested expansion would smear the result | Choose a different grouping shape or supported rewrite |
DUPLICATE_OUTPUT_ALIAS |
Projected output names collide | Rename one of the projected aliases |
CONVERSION_NOT_SUPPORTED |
Conversion shape falls outside the supported event-count model | Use a supported conversion metric or split the ask into explicit legal queries |
PREDICATE_SCOPE_UNSAFE |
Predicate context would smear values across the outer query | Change the grouping, time grain, or predicate scope so the entity context is safe |
Recovery hint contracts
Errors ship structured recovery_hints so callers can repair the query without re-reading docs.
The contracts below are stable.
-
USE_OBJECT_SHAPE— emitted onINVALID_EXPRESSION_ASTwhen a rolling/conversionwindowor prior_periodoffsetis passed as an int instead of a{unit, value}object. -
drop_time_startthenwiden_time_window— emitted onWINDOWED_TIME_FILTER_UNSUPPORTEDin that order.drop_time_startis the always-safe first hint;widen_time_windowships a computedsuggested_start. -
FIX_QUERY_CALL— emitted onINVALID_QUERYwhen the call has an unknown or retired key. An unknown key is named with the key it most likely meant (filters→where,sort→order_by,measures→select); a retired key, such asmetric_filters, with where it went.
Warning codes
Non-fatal signals returned in the response warnings[] array.
-
PARTIAL_DATA— a window total, or a row of a day-or-coarser series, spans a whole day afterwindow.data_throughand beforenow's day. -
UNKNOWN_ROWS_KEPT— anotcondition kept rows whose value is unknown; the message gives themissing: falsecondition that leaves them out. -
QUERY_SHORTHAND_NORMALIZED— adimension.id inselectwas moved togroup_by. -
EXPRESSION_NORMALIZED_AWAY— an input expressionkinddid not survive normalization, or theas:alias is missing from the compiled output.details.dropped_expressionanddetails.compiled_kindhelp decide whether to retry with a different shape. -
SEMANTIC_CAVEAT_APPLIED— package-authored advisory context matched the compiled query. Caveats do not change SQL, rows, access, discovery, or policy behavior; agents should use them only when interpreting affected results.
discover -> plan -> execute. /api/v1/query
validates and compiles before it runs, so validate and compile are optional dry runs.
Compare plan's best.query with the question before you run it.
Continue to Getting Started if you want the exact serve-and-query steps, or Guardrails for runtime limits.
Semantic Rails Cloud runtime API
Semantic Rails Cloud serves the same calls for each workspace environment at
https://cloud.semantic-rails.com/v1/tenants/<workspace id>/runtime/<env>/<call>,
where <env> is dev, staging or
prod and <call> is discover,
inspect, build-options, valid-values,
plan, validate, compile or query,
each a POST. Bodies are the engine's, plus package: the project
name, required when the environment serves more than one project.
Send a Cloud API key as Authorization: ApiKey <your key>. Cloud reads
Bearer as an OAuth sign-in token, so an API key sent as Bearer
is refused with 401, and Cloud ignores X-API-Key. A key bound to an
environment is refused in any other. Every call needs query:execute.
Without query:sql, answers come back at minimal verbosity with
no SQL, and compile or a request for SQL (another
verbosity, or an sql_profile) is refused with 403.
export WORKSPACE_ID="your-workspace-id"
# Set CLOUD_API_KEY privately to a prod API key with query:execute.
curl --fail-with-body --request POST \
"https://cloud.semantic-rails.com/v1/tenants/$WORKSPACE_ID/runtime/prod/query" \
--header "Authorization: ApiKey $CLOUD_API_KEY" \
--header "Content-Type: application/json" \
--data '{"package": "my-project", "query": {"select": ["measure.jaffle.order_count"], "group_by": ["dimension.jaffle_store_name"], "time": {"grain": "month", "start": "2017-01-01", "end": "2017-04-01"}}}'
Replace my-project and the ids with your project's; discover
lists them.
Limits
Runtime calls are limited per workspace, per minute, by plan, and REST and
MCP calls share the same limits. On the trial and Builder
plans, a workspace can make 6 validate,
compile or query calls a minute (over MCP, every
execute call counts here), and 60 discover,
inspect, build-options, valid-values and
plan calls a minute. Higher plans allow more.
Over a limit, Cloud answers HTTP 429 with code rate.limited;
retry after the number of seconds in details.retry_after_seconds. A 429 can
also mean too many of the workspace's calls are running at once; retry the same way.