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
cli
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.

/api/v1/query request
{
  "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, their not_ forms, and missing (true or false). not keeps rows whose value is unknown. There is no between: a range is two conditions, gte and lt.
  • time.start is inclusive and time.end exclusive: the window is [start, end), so the example covers September through November 2016. "last": {"unit", "value"} and "this": "<unit>" are relative windows that resolve against now.
  • With time.grain the answer has one row per period in a period column. 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_filters becomes having or an entity filter, time.range becomes time.last or start/end, and "DESC" becomes "desc". version is 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_id
  • physical_plan.nodes[].details.selected_relation_type
  • physical_plan.nodes[].details.aggregate_relation_id
  • performance_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
/api/v1/build-options request
{
  "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 on INVALID_EXPRESSION_AST when a rolling/conversion window or prior_period offset is passed as an int instead of a {unit, value} object.
  • drop_time_start then widen_time_window — emitted on WINDOWED_TIME_FILTER_UNSUPPORTED in that order. drop_time_start is the always-safe first hint; widen_time_window ships a computed suggested_start.
  • FIX_QUERY_CALL — emitted on INVALID_QUERY when 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 as metric_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 after window.data_through and before now's day.
  • UNKNOWN_ROWS_KEPT — a not condition kept rows whose value is unknown; the message gives the missing: false condition that leaves them out.
  • QUERY_SHORTHAND_NORMALIZED — a dimension. id in select was moved to group_by.
  • EXPRESSION_NORMALIZED_AWAY — an input expression kind did not survive normalization, or the as: alias is missing from the compiled output. details.dropped_expression and details.compiled_kind help 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.
New callers should follow 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.

Cloud query
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.