Skip to content
OBML v1.0
OrionBelt v2.34.0

API Endpoints

Complete reference for all OrionBelt REST API endpoints.

Health Check

GET /health

Returns the service status and version.

Response:

{
 "status": "ok",
 "version": "2.34.0"
}

Sessions

POST /v1/sessions

Create a new session. Each session has its own model store.

Request (optional):

{
 "metadata": {
 "user": "alice",
 "purpose": "revenue analysis"
 }
}

Response (201):

{
 "session_id": "a1b2c3d4e5f6",
 "created_at": "2025-01-15T10:30:00Z",
 "last_accessed_at": "2025-01-15T10:30:00Z",
 "model_count": 0,
 "metadata": {
 "user": "alice",
 "purpose": "revenue analysis"
 }
}

GET /v1/sessions

List all active sessions.

Response (200):

{
 "sessions": [
 {
 "session_id": "a1b2c3d4e5f6",
 "created_at": "2025-01-15T10:30:00Z",
 "last_accessed_at": "2025-01-15T10:35:00Z",
 "model_count": 2,
 "metadata": {}
 }
 ]
}

GET /v1/sessions/{session_id}

Get info for a specific session. Also refreshes the session's last-accessed time.

Response (200): Same as single session in list response.

Error (404): Session not found or expired.

DELETE /v1/sessions/{session_id}

Close a session and release its resources.

Response (204): No content.

Error (404): Session not found.


Session Models

POST /v1/sessions/{session_id}/models

Load an OBML semantic model into a session. The model is parsed, validated, and stored.

Admin-curated mode

Returns 403 Forbidden when MODEL_FILES is configured. Address the preloaded model via its named protected session instead (/v1/sessions/<model_name>/...).

Request:

{
 "model_yaml": "version: 1.0\ndataObjects:\n Orders:\n code: ORDERS\n ...",
 "dedup": true
}
Field Type Default Description
model_yaml string — OBML YAML (or use model_json).
model_json object/string — OBML as JSON (or use model_yaml).
extends array — Inline YAML fragments to merge.
inherits string — Parent model_id to inherit from.
dedup bool true When true, identical OBML content already loaded in this session reuses the existing model_id. Set false to force a fresh parse.

Response (201):

{
 "model_id": "abcd1234",
 "data_objects": 2,
 "dimensions": 3,
 "measures": 2,
 "metrics": 1,
 "warnings": [],
 "model_load": "fresh",
 "health": {
 "status": "ok",
 "data_objects": 2,
 "joins": 1,
 "orphan_data_objects": [],
 "fan_trap_risks": [],
 "unreachable_dimensions": [],
 "warnings_count": 0
 }
}

model_load is "fresh" when the OBML was parsed and loaded normally, "reused" when an identical model was already present in the session (no parsing/validation work was done; the existing model_id is returned). Dedup applies only to plain model_yaml loads — supplying extends or inherits always loads fresh.

health block

Structural health of the model's join graph, computed during load (no extra round trip required). Always present on fresh loads and on reused dedup hits. Fields:

Field Type Description
status string ok when nothing surfaced, warnings when one or more risks were detected.
data_objects int Count of dataObjects in the model.
joins int Count of (non-secondary) joins detected.
orphan_data_objects array of strings DataObjects with no incoming or outgoing joins. May be intentional in single-table models.
fan_trap_risks array of objects Pairs of facts that share a dimension via the same FK columns. Each entry has tables (qualified physical names), reason, and suggested_pattern (typically "composite_fact_layer").
unreachable_dimensions array of strings Dimensions whose dataObject is not reachable from any fact via directed joins.
warnings_count int Total warnings across orphans, fan-traps, and unreachable dims.

Structured warnings

Every warnings list in this API uses the same shape so agents can branch on stable codes without parsing message text:

{
 "code": "FAN_TRAP_RISK",
 "severity": "warning",
 "message": "Measure 'Revenue' (SUM): cross-join through 'Movie Directors' …",
 "path": "select.measures[0]",
 "hint": "Add the junction-table dimension to the GROUP BY, …",
 "context": { "measure": "Revenue", "junction": "Movie Directors" }
}

Initial warning code taxonomy: GRAIN_OVERRIDE_INCOMPATIBLE, FILTER_CONTEXT_OVERRIDE_INCOMPATIBLE, POP_CONSTRAINT_VIOLATED, CUMULATIVE_CONSTRAINT_VIOLATED, FAN_TRAP_RISK, ORPHAN_DATA_OBJECT, SHARED_TABLE_CONTRACT_DISAGREEMENT, LARGE_RESULT_SET, CACHE_TTL_FLOOR_HIT, INCOMPATIBLE_COMBINATION, SQL_VALIDATION, MERGE_WARNING, DECLARED_TYPE_NOT_APPLIED. Codes are extended over time, never repurposed.

Error (403): Single-model mode: model upload is disabled.

Error (422): Model has validation errors.

Error (404): Session not found.

GET /v1/sessions/{session_id}/models

List all models loaded in a session.

Response (200):

[
 {
 "model_id": "abcd1234",
 "data_objects": 2,
 "dimensions": 3,
 "measures": 2,
 "metrics": 1
 }
]

GET /v1/sessions/{session_id}/models/{model_id}

Describe a model's contents — data objects (with fields and joins), dimensions, measures, and metrics.

Response (200):

{
 "model_id": "abcd1234",
 "data_objects": [
 {
 "label": "Orders",
 "code": "WAREHOUSE.PUBLIC.ORDERS",
 "columns": ["Order ID", "Price", "Quantity"],
 "join_targets": ["Customers"]
 }
 ],
 "dimensions": [
 {
 "name": "Country",
 "result_type": "string",
 "data_object": "Customers",
 "column": "Country",
 "time_grain": null
 }
 ],
 "measures": [...],
 "metrics": [...]
}

Error (404): Model or session not found.

DELETE /v1/sessions/{session_id}/models/{model_id}

Remove a model from a session.

Admin-curated mode

Returns 403 Forbidden when MODEL_FILES is configured.

Response (204): No content.

Error (403): Single-model mode: model removal is disabled.

Error (404): Model or session not found.


Session Validation

POST /v1/sessions/{session_id}/validate

Validate OBML YAML within a session context. Does not store the model.

Request:

{
 "model_yaml": "version: 1.0\ndataObjects:\n ..."
}

Response (200):

{
 "valid": true,
 "errors": [],
 "warnings": []
}

Validation failure:

{
 "valid": false,
 "errors": [
 {
 "code": "UNKNOWN_DATA_OBJECT",
 "message": "Data object 'Unknown' not found",
 "path": "dimensions.Bad.dataObject"
 }
 ],
 "warnings": []
}

Checking the model against the datasource

Validation is offline by default: the model is checked against itself and the OBML schema, and no connection is opened. That leaves one thing unchecked — the physical binding. A data object's database / schema / code, and each column's code, are opaque strings to the validator, so a model can be valid in every structural sense and still name a table that was dropped or a column that was renamed. The first thing to say so is the warehouse, at query time.

?online=true closes that gap:

Query param Default Meaning
online false Probe the datasource for every data object's table and declared columns
dialect DB_VENDOR Which configured datasource to probe

dialect here selects a connection to open, which is why it falls back to DB_VENDOR rather than to the model's settings.defaultDialect — a model declaring defaultDialect: snowflake on a DuckDB deployment would otherwise be probed by opening a Snowflake connection that does not exist.

Each data object costs one round trip: a SELECT <declared columns> FROM <table> LIMIT 0, quoted exactly as a compiled query would quote it. That plans without scanning, and proves the columns are addressable through the same connection and the same spelling real queries use — which also settles identifier case without OBSL having to know which engines fold it. Only when that projection fails does a second SELECT * ... LIMIT 0 run, to tell a missing table from a missing column and to name which one.

Findings are errors like any other, so valid goes to false:

{
 "valid": false,
 "errors": [
 {
 "code": "DATASOURCE_COLUMN_MISSING",
 "message": "Column 'order_id' does not exist on main.orders (data object 'Orders').",
 "path": "dataObjects.Orders.columns.Order ID.code",
 "hint": "Correct the column's 'code', or drop the column from the model."
 },
 {
 "code": "DATASOURCE_TYPE_MISMATCH",
 "message": "Column 'amount' on data object 'Orders' is declared 'float' but the datasource returns a string column.",
 "path": "dataObjects.Orders.columns.Amount.abstractType"
 }
 ],
 "warnings": []
}
Code Meaning
DATASOURCE_TABLE_MISSING The table a data object maps to could not be read. Carries the driver's own error.
DATASOURCE_COLUMN_MISSING A declared code names no column on an existing table.
DATASOURCE_COLUMN_CASE The column exists under a different letter case, and this engine rejected the model's spelling. The hint carries the real one.
DATASOURCE_TYPE_MISMATCH The column's type is not in the family its abstractType declares.
DATASOURCE_UNAVAILABLE The check could not run at all — no driver, no credentials, no connection. Reported once, not per data object.
DATASOURCE_UNSUPPORTED_DIALECT dialect names an engine OBSL has no dialect for.
DATASOURCE_PROBE_FAILED The table and columns are all there but the read was still refused (a column-level grant, most likely). Carries the driver's error.

Since the flag is opt-in, it fails closed: asking for a check that cannot run returns DATASOURCE_UNAVAILABLE and valid: false rather than a green light.

Two things are deliberately out of scope. Type comparison is by family — number, datetime, string, boolean, binary — so a float backed by DECIMAL(38,9) or a timestamp backed by DATE is not drift; only a float backed by text is. And a column that comes back null-typed, which a zero-row fetch is exactly where you find, is skipped rather than reported. Data objects sourced through nestedIn are not probed: their rows come from unnesting a parent's array column, so their columns belong to that array's element type rather than to any table of their own.

The same options are on POST /v1/validate (stateless) and on the CLI as obsl validate --online.


Session Query Compilation & Execution

POST /v1/sessions/{session_id}/query/sql

Compile a semantic query against a model loaded in the session.

Request:

{
 "model_id": "abcd1234",
 "query": {
 "select": {
 "dimensions": ["Customer Country"],
 "measures": ["Revenue"]
 },
 "where": [
 {
 "field": "Customer Segment",
 "op": "in",
 "value": ["SMB", "MidMarket"]
 }
 ],
 "orderBy": [
 { "field": "Revenue", "direction": "desc" }
 ],
 "limit": 1000
 },
 "dialect": "postgres"
}

Response (200):

{
 "sql": "SELECT ...",
 "dialect": "postgres",
 "sql_valid": true,
 "explain": {
 "planner": "Star Schema",
 "planner_reason": "All measures come from a single fact table",
 "base_object": "Orders",
 "base_object_reason": "Orders has the most joins and contains all requested measures",
 "joins": [
 {
 "from_object": "Orders",
 "to_object": "Customers",
 "join_columns": ["CUSTOMER_ID"],
 "reason": "Required for dimension 'Customer Country'"
 }
 ],
 "where_filter_count": 1,
 "having_filter_count": 0,
 "has_totals": false,
 "cfl_legs": 0
 },
 "warnings": []
}

Error responses:

Status Cause
400 Unsupported dialect
404 Model or session not found
422 Resolution error

POST /v1/sessions/{session_id}/query/plan

Return the planner's understanding of a query without compiling SQL or executing. Cheap by default (no warehouse round trip) — agents use it as a "would this work?" probe in their planning loop.

Request:

{
 "model_id": "abcd1234",
 "query": {
 "select": {
 "dimensions": ["Customer Country"],
 "measures": ["Revenue"]
 }
 },
 "dialect": "postgres",
 "include_database_explain": false
}
Field Type Default Description
model_id string — Loaded model id.
query object — QueryObject (same shape as /query/sql).
dialect string model/env default SQL dialect.
include_database_explain bool false When true, also runs EXPLAIN <sql> against the configured warehouse and includes the raw output. Costs one round trip; some warehouses bill compute for EXPLAIN.

Response (200, OBSL-only plan):

{
 "status": "ok",
 "planner": "Star Schema",
 "planner_reason": "All requested objects are reachable from a single base via directed joins",
 "physical_tables": ["WAREHOUSE.PUBLIC.ORDERS", "WAREHOUSE.PUBLIC.CUSTOMERS"],
 "join_path": [
 {
 "from_object": "Orders",
 "to_object": "Customers",
 "cardinality": "many-to-one",
 "fk": "CUSTOMER_ID = CUSTOMER_ID"
 }
 ],
 "filters_applied": 0,
 "warnings": [],
 "would_compile": true,
 "compiled_sql_length_estimate": 312,
 "database_explain": null
}

Response (200, with include_database_explain: true):

Adds a database_explain block. The explain_output is opaque text in the dialect's native EXPLAIN format — OBSL does not normalize across dialects.

{
 "...": "...",
 "database_explain": {
 "dialect": "postgres",
 "compiled_sql": "SELECT ...",
 "explain_output": "Hash Join (cost=128.50..1247.30 rows=1042 width=48) ...",
 "explain_format": "text"
 }
}

Failure modes:

  • Resolution / fanout / unsupported-aggregation errors → status: "error", would_compile: false, structured warnings with the failure cause.
  • include_database_explain: true but the warehouse rejects EXPLAIN → OBSL plan still returned with a DATABASE_EXPLAIN_FAILED warning describing why; database_explain is null.

The plan endpoint never executes the actual query, even with include_database_explain: true.

POST /v1/sessions/{session_id}/query/execute

Compile and execute a semantic query against the configured database. Requires FLIGHT_ENABLED=true with DB_VENDOR and vendor credentials configured.

If the query has no explicit limit, a default of 10,000 rows is enforced.

Request:

{
 "model_id": "abcd1234",
 "query": {
 "select": {
 "dimensions": ["Customer Country"],
 "measures": ["Revenue"]
 },
 "limit": 100
 },
 "dialect": "postgres"
}

Response (200):

{
 "sql": "SELECT ...",
 "dialect": "postgres",
 "columns": [
 {"name": "Customer Country", "type": "string"},
 {"name": "Revenue", "type": "decimal(18, 2)", "format": "#,##0.00"}
 ],
 "rows": [
 ["US", 15230.50],
 ["UK", 9870.00]
 ],
 "row_count": 2,
 "execution_time_ms": 42.5,
 "resolved": {
 "fact_tables": ["Orders"],
 "dimensions": ["Customer Country"],
 "measures": ["Revenue"]
 },
 "sql_valid": true,
 "warnings": [],
 "explain": { "..." : "..." }
}

Column types follow the model, not the engine

A result is reconciled against the type the model declares before it is returned, so a column's type is a property of the model rather than of whichever engine is behind it. The same query answers the same way on every surface - REST, pgwire and Flight - and on a cache hit as on a miss.

It matters where an engine cannot express a declared type at all:

Engine Driver returns You receive
MySQL (no boolean type) int64 1 true — and BOOL on pgwire
Dremio date64[ms] date32[day]

This is an allowlist of exactly those cases, not a general cast. A resultType names a family, so an engine answering more precisely than the declaration - a decimal(38, 2) behind a measure declared float - is left alone rather than narrowed. See Arrow type fidelity for the per-engine measurements.

When it is refused. A declared boolean holding something other than 0, 1 or NULL is not cast: Arrow maps every nonzero to true, and asserting that a column holding 7 is true invents content the model never claimed. The column keeps the engine's type, columns[].type reports that type rather than the declared one, and the declaration comes back as a warning:

{
 "code": "DECLARED_TYPE_NOT_APPLIED",
 "severity": "warning",
 "message": "Column 'Tier' was not reconciled to its declared type: declared boolean but the column holds values other than 0 and 1; left as int64",
 "context": { "column": "Tier", "reason": "declared boolean but the column holds values other than 0 and 1" }
}

So columns[].type always describes the cells beside it, and warnings tells you where the model wanted otherwise.

Query parameters (apply to both the session and shortcut form):

Param Type Default Description
format json | tsv | arrow json When tsv, returns text/tab-separated-values; cells with tab/newline/CR/double-quote are RFC 4180-quoted (implies format_values=true). When arrow, returns a length-prefixed result frame (application/vnd.orionbelt.result+arrow): [u32 big-endian json_len][JSON envelope][gzip'd Arrow IPC data]. The JSON envelope (row count, execution time, cache status, timezone, column types/formats) is assembled fresh per request; the Arrow sub-part carries the row data (its own gzip). Content negotiation via the Accept header is also supported.
format_values bool false When true, numeric cells in the JSON response are rendered as locale-aware display strings using each column's format pattern (matches the Gradio UI).
locale string model settings.defaultLocale, then DEFAULT_LOCALE env BCP-47 tag (e.g. de, en-US). Drives thousand/decimal separators. When omitted, falls back to the model's settings.defaultLocale, then the DEFAULT_LOCALE env.
timezone string model default_timezone IANA TZ name (e.g. Europe/Berlin). Overrides the model's default for naive timestamp coercion.

Example (TSV with German locale):

curl -X POST 'http://localhost:8080/v1/query/execute?format=tsv&locale=de' \
 -H 'Content-Type: application/json' \
 -d '{ "select": { "dimensions": ["Customer Country"], "measures": ["Revenue"] } }'
Customer Country    Revenue
US  15.230,50
UK  9.870,00

Error responses:

Status Cause
400 Unsupported dialect
404 Model or session not found
422 Resolution error
502 Database execution failed
503 Query execution not available (FLIGHT_ENABLED not set)

Top-level shortcut: POST /v1/query/execute — auto-resolves session/model, auto-detects dialect from DB_VENDOR.


One-shot Batch

POST /v1/oneshot/batch

Load (or reference) a model and run multiple independent queries against it in a single round trip. Designed for agent workflows: one model, N sub-questions, parallel execution under a server-capped semaphore. See design/PLAN_oneshot_batch.md for the full design.

Request:

{
 "session_id": null,
 "model_yaml": "version: 1.0\ndataObjects: ...",
 "model_id": null,
 "queries": [
 {
 "id": "revenue_by_country",
 "query": {
 "select": {"dimensions": ["Customer Country"], "measures": ["Total Revenue"]},
 "limit": 100
 }
 },
 {
 "id": "revenue_by_product",
 "query": {"select": {"dimensions": ["Product"], "measures": ["Total Revenue"]}},
 "execute": false
 }
 ],
 "dialect": "postgres",
 "execute": true,
 "max_parallelism": 4,
 "fail_fast": false,
 "persist_model": false,
 "dedup": true
}
Field Type Default Description
session_id string auto-create Existing session, otherwise OBSL creates one.
model_yaml string — OBML YAML. Mutually exclusive with model_id.
model_id string — ID of an already-loaded model in the session. Mutually exclusive with model_yaml.
queries array required List of queries (1..max_queries).
queries[].id string auto (q0,q1,...) Optional caller ID. Must be unique within the batch when supplied. Omit to let the server assign positional IDs.
queries[].execute bool inherits batch Per-query override for compile-only vs. execute.
queries[].dialect string inherits batch Per-query dialect override.
dialect string model/env Default dialect for the batch.
execute bool false Default execute flag for the batch.
max_parallelism int server cap Concurrency cap (silently lowered to the server max).
fail_fast bool false Cancel remaining queries on first failure.
persist_model bool false Keep the model loaded after the batch (only for model_yaml loads).
dedup bool true Reuse an existing identical model loaded in this session.

Response (200):

{
 "session_id": "a1b2c3d4...",
 "model_id": "abcd1234",
 "model_persisted": false,
 "model_load": "fresh",
 "results": [
 {
 "id": "revenue_by_country",
 "status": "ok",
 "sql": "SELECT ...",
 "dialect": "postgres",
 "sql_valid": true,
 "executed": true,
 "columns": [{"name": "Customer Country", "type": "string"}],
 "rows": [["US", 15230.5]],
 "row_count": 42,
 "execution_time_ms": 38.2,
 "warnings": []
 },
 {
 "id": "revenue_by_product",
 "status": "ok",
 "sql": "SELECT ...",
 "dialect": "postgres",
 "sql_valid": true,
 "executed": false,
 "warnings": []
 }
 ],
 "batch_warnings": []
}

model_load is "fresh" (parsed and loaded), "reused" (matched dedup index), or "referenced" (caller supplied model_id).

Per-query status is "ok", "error" (with an error envelope: {code, message, path, hint}), or "cancelled" (only when fail_fast: true triggers and remaining queries are short-circuited).

Error (422): Validation failure (duplicate query IDs, both/neither of model_yaml/model_id, batch over ONESHOT_BATCH_MAX_QUERIES, or model load failure).

Error (404): Session or model_id not found.

Error (410): Session expired.

Limits (configurable, see GET /v1/settings.oneshot_batch):

Setting Default
ONESHOT_BATCH_MAX_QUERIES 50
ONESHOT_BATCH_MAX_PARALLELISM 8
ONESHOT_BATCH_DEFAULT_TIMEOUT_MS 30000 (per-query)
ONESHOT_BATCH_BATCH_TIMEOUT_MS 120000 (whole batch)

OSI ↔ OBML Conversion

Stateless endpoints for converting between OSI (Open Semantic Interchange) and OBML formats. No session required.

POST /v1/convert/osi-to-obml

Convert an OSI YAML model to OBML format. OBSL emits and validates against OSI v0.2.0.dev0; v0.1.x inputs still load via a legacy reader shim.

Request:

{
 "input_yaml": "version: \"0.2.0.dev0\"\nsemantic_model:\n - name: my_model\n ..."
}

Response (200):

{
 "output_yaml": "version: 1.0\ndataObjects:\n ...",
 "warnings": [
 "Relationship 'sales_to_date': no type specified, defaulting to many-to-one."
 ],
 "validation": {
 "schema_valid": true,
 "semantic_valid": true,
 "schema_errors": [],
 "semantic_errors": [],
 "semantic_warnings": []
 },
 "input_validation": {
 "schema_valid": true,
 "semantic_valid": true,
 "schema_errors": [],
 "semantic_errors": [],
 "semantic_warnings": []
 }
}

the new input_validation field carries the result of running the OSI input against the vendored OSI v0.2 schema (Draft 2020-12). Advisory by default: the endpoint still returns 200 with the converted output when input fails strict v0.2 validation, because the legacy reader shim still produces correct OBML for v0.1.x documents. Inspect input_validation.schema_errors if you need a hard gate on the source format.

The validation field (unchanged from v2.5) reports the OBML output.

Error (400): Invalid YAML input.

Error (422): Conversion failed (e.g. unsupported OSI structure).

POST /v1/convert/obml-to-osi

Convert an OBML YAML model to OSI format. OBSL emits OSI v0.2.0.dev0 (version: "0.2.0.dev0" at the top level, primary_key / unique_keys first-class, informational dialects / vendors arrays).

Request:

{
 "input_yaml": "version: 1.0\ndataObjects:\n ...",
 "model_name": "my_model",
 "model_description": "Sales analytics model",
 "ai_instructions": ""
}

The model_name, model_description, and ai_instructions fields are optional (defaults: "semantic_model", "", "").

Response (200): Same structure as POST /v1/convert/osi-to-obml. The input_validation field is always null here — input-side OBML validation isn't wired on this endpoint yet (output-side validation still runs against the OSI v0.2 schema).

Error (400): Invalid YAML input.

Error (422): Conversion failed.


Model Discovery

These endpoints provide structured access to model metadata. All fields include an optional owner property when set in the OBML model.

GET /v1/sessions/{session_id}/models/{model_id}/schema

Full model structure as JSON, including all data objects, dimensions, measures, and metrics.

Response (200):

{
 "model_id": "abcd1234",
 "version": 1.0,
 "owner": "team-data",
 "data_objects": [
 {
 "name": "Orders",
 "code": "ORDERS",
 "database": "WAREHOUSE",
 "schema": "PUBLIC",
 "columns": [
 { "name": "Price", "code": "PRICE", "abstract_type": "float" }
 ],
 "join_targets": ["Customers"],
 "owner": "team-sales"
 }
 ],
 "dimensions": [
 { "name": "Country", "data_object": "Customers", "column": "Country", "result_type": "string" }
 ],
 "measures": [
 { "name": "Revenue", "aggregation": "sum", "result_type": "float", "columns": [...] }
 ],
 "metrics": [
 { "name": "Revenue per Order", "expression": "...", "component_measures": ["Revenue", "Order Count"] }
 ]
}

GET /v1/sessions/{session_id}/models/{model_id}/dimensions

List all dimensions.

Response (200): Array of dimension objects.

GET /v1/sessions/{session_id}/models/{model_id}/dimensions/{name}

Get a single dimension by name.

Response (200):

{
 "name": "Country",
 "data_object": "Customers",
 "column": "Country",
 "result_type": "string",
 "time_grain": null,
 "via": null,
 "path_name": null,
 "owner": null
}

Error (404): Dimension not found.

GET /v1/sessions/{session_id}/models/{model_id}/measures

List all measures.

Response (200): Array of measure objects.

GET /v1/sessions/{session_id}/models/{model_id}/measures/{name}

Get a single measure by name.

Response (200):

{
 "name": "Revenue",
 "aggregation": "sum",
 "result_type": "float",
 "columns": [
 { "data_object": "Orders", "column": "Price" }
 ],
 "expression": "{[Orders].[Price]} * {[Orders].[Quantity]}",
 "total": false,
 "owner": null
}

Error (404): Measure not found.

GET /v1/sessions/{session_id}/models/{model_id}/metrics

List all metrics.

Response (200): Array of metric objects.

GET /v1/sessions/{session_id}/models/{model_id}/metrics/{name}

Get a single metric by name. Returns the expression formula and its component measures.

Error (404): Metric not found.

GET /v1/sessions/{session_id}/models/{model_id}/explain/{name}

Explain the lineage of a dimension, measure, or metric — traces back through the dependency chain to the underlying data objects and columns.

Response (200):

{
 "name": "Revenue",
 "type": "measure",
 "lineage": [
 { "type": "data_object", "name": "Orders" },
 { "type": "column", "name": "Price", "detail": "referenced in expression" },
 { "type": "column", "name": "Quantity", "detail": "referenced in expression" }
 ]
}

Error (404): Name not found in model.

POST /v1/sessions/{session_id}/models/{model_id}/find

Search across model artefacts by name or synonym. When the query produces zero exact and zero synonym matches, deterministic fuzzy fallback (Levenshtein + trigram-Jaccard) returns the closest near-miss candidates. Threshold is 0.5; up to 10 results are returned.

Request:

{
 "query": "Revenue",
 "types": ["measure", "metric"]
}

The types filter is optional. Valid types: dimension, measure, metric, data_object.

Response (200, exact / synonym hits):

{
 "query": "Revenue",
 "results": [
 { "name": "Revenue", "type": "measure", "match_field": "name", "score": 1.0 },
 { "name": "Revenue per Order", "type": "metric", "match_field": "name", "score": 1.0 }
 ],
 "exact_matches": [...],
 "synonym_matches": [],
 "fuzzy_matches": []
}

Response (200, no exact/synonym hit — fuzzy fallback fires):

{
 "query": "Custmr Cuntry",
 "results": [],
 "exact_matches": [],
 "synonym_matches": [],
 "fuzzy_matches": [
 {
 "name": "Customer Country",
 "kind": "dimension",
 "score": 0.78,
 "reason": "trigram overlap"
 }
 ]
}

fuzzy_matches is empty when the query produced exact or synonym hits, and also when nothing scored above the 0.5 threshold (truly no match).

GET /v1/sessions/{session_id}/models/{model_id}/join-graph

Return the join graph as nodes and edges.

Response (200):

{
 "nodes": ["Orders", "Customers", "Products"],
 "edges": [
 {
 "from_object": "Orders",
 "to_object": "Customers",
 "cardinality": "many-to-one",
 "secondary": false
 }
 ]
}

External Concept Mappings

Links from model artefacts to concepts in external ontologies, authored in OBML as ontology.prefixes plus externalConceptMappings on the model, a data object, a dimension, a measure, a metric or a rule. Descriptive metadata only: nothing here changes SQL. The describe endpoints above (schema, dimensions/{name}, measures/{name}, metrics/{name}) carry each artefact's mappings under external_concept_mappings; these endpoints answer the cross-cutting questions.

GET /v1/sessions/{session_id}/models/{model_id}/concept-mappings

Every mapping in the model, with the artefact it sits on and the concept expanded to an absolute IRI. Filters combine.

Param Description
concept A compact IRI using the model's prefixes (corp:NetRevenue) or a full IRI. Returns the artefacts mapped to that concept; the response echoes the expanded IRI in concept. An undeclared prefix or a malformed IRI is 422.
namespace A declared or built-in prefix name (corp) or an absolute namespace IRI the target must start with. Anything else is 422.
relation One of exact, close, broader, narrower, related.
types Comma-separated subset of model, dataObject, dimension, measure, metric, rule.

Response (200):

{
 "mappings": [
 {
 "object": {"type": "measure", "name": "Revenue"},
 "concept": "corp:NetRevenue",
 "expanded_iri": "https://ontology.example.com/business/NetRevenue",
 "relation": "exact",
 "justification": "curated",
 "source": "enterprise-finance-ontology",
 "ontology_version": "2026.1",
 "confidence": 1.0,
 "comment": "Approved by Finance Data Governance"
 }
 ],
 "total": 1,
 "concept": "https://ontology.example.com/business/NetRevenue"
}

relation reads artefact first, in the SKOS direction: broader means the external concept is the broader one (the artefact is a narrower notion of it), narrower means the external concept is the narrower one.

GET /v1/sessions/{session_id}/models/{model_id}/concept-mappings/namespaces

The external namespaces the model links into, most used first, plus the declared ontology.prefixes. A target is attributed to the longest declared or built-in prefix that covers it; a full-IRI mapping outside every prefix is grouped by the IRI up to its last # or / (prefix is then null).

Response (200):

{
 "prefixes": {"corp": "https://ontology.example.com/business/"},
 "namespaces": [
 {"prefix": "corp", "namespace": "https://ontology.example.com/business/", "mapping_count": 5, "object_count": 4},
 {"prefix": null, "namespace": "https://schema.org/", "mapping_count": 1, "object_count": 1}
 ]
}

GET /v1/sessions/{session_id}/models/{model_id}/concept-mappings/unmapped

Artefacts in the mappable scope that carry no mapping yet: the model itself, data objects, dimensions, declared measures, metrics and rules. Synthesized count measures cannot carry mappings and are not listed. types (comma-separated) narrows the report.

Response (200):

{
 "objects": [
 {"type": "dataObject", "name": "Customers"},
 {"type": "measure", "name": "Order Total"}
 ],
 "total": 2,
 "types": ["model", "dataObject", "dimension", "measure", "metric", "rule"]
}

Business Rules

Declarative rules from the OBML rules block (see the Business Rules guide). A rule compiles to an ordinary query whose rows are its findings: members for classification and eligibility, violations for validation and constraint.

GET /v1/sessions/{session_id}/models/{model_id}/rules

Every rule with its derived facts and whether it compiles, plus statistics. ?dialect= overrides the dialect (defaults like query/sql: model default, then DB_VENDOR).

Response (200):

{
 "dialect": "duckdb",
 "rules": [
 {
 "name": "High Return Rate",
 "type": "classification",
 "level": "aggregate",
 "findings": "matches",
 "severity": null,
 "grain": ["Product Category"],
 "description": "Product categories returning more than a tenth of what they sell",
 "owner": null,
 "dimensions": ["Product Category"],
 "measures": ["Return Rate"],
 "depends_on": [],
 "executable": true,
 "error": null
 }
 ],
 "statistics": {
 "total": 6,
 "by_type": {"classification": 3, "eligibility": 1, "validation": 2},
 "by_level": {"row": 1, "aggregate": 5},
 "by_severity": {"error": 1, "warning": 1},
 "executable": 6,
 "not_executable": 0
 }
}

GET /v1/sessions/{session_id}/models/{model_id}/rules/{name}

One rule: everything the list carries plus condition (as authored), synonyms, external_concept_mappings and query (the QueryObject whose rows are the findings). 404 for an unknown rule.

POST /v1/sessions/{session_id}/models/{model_id}/rules/{name}/compile

Compile one rule. Body: {"dialect": "postgres"}, optional.

Response (200):

{
 "name": "High Return Rate",
 "level": "aggregate",
 "findings": "matches",
 "dialect": "postgres",
 "sql": "SELECT ... GROUP BY ... HAVING ...",
 "query": {"select": {"dimensions": ["Product Category"], "measures": ["Return Rate"]}, "having": [{"field": "Return Rate", "op": ">", "value": 0.1}]},
 "warnings": []
}

Compile failures are reported the way query/sql reports them: 422 with structured errors, 400 for an unsupported dialect.

POST /v1/sessions/{session_id}/models/{model_id}/rules/{name}/evaluate

Run one rule and return its findings: members for classification / eligibility, violations for validation / constraint. Goes through the same cache-aware pipeline as query/execute; requires QUERY_EXECUTE=true (503 otherwise). Body, all optional: dialect, limit (default: the API's query limit), format_values.

{
 "name": "High Return Rate", "type": "classification", "level": "aggregate", "findings": "matches",
 "severity": null, "dialect": "duckdb", "sql": "SELECT ...",
 "columns": [{"name": "Product Category", "type": "string"}, {"name": "Return Rate", "type": "float"}],
 "rows": [["Toys", 0.125]], "row_count": 1, "limit": 100,
 "execution_time_ms": 12.4, "cached": false, "warnings": []
}

POST /v1/sessions/{session_id}/models/{model_id}/rules/evaluate

Run every rule, or a filtered subset, into a report. A report, not one result: each rule is a row with its status and nothing is hidden. Requires QUERY_EXECUTE=true unless dry_run.

Body field Default Description
dialect model default target dialect
types, severities all only rules of these types / severities
executable_only false skip rules whose query does not compile instead of failing them
max_rules all stop after this many
limit 20 sample findings per rule
dry_run false compile only; status compiled
stop_on_first_failure false
include_sql, include_rows false, true what each row carries
format_values false render numeric cells with their display format
{
 "model_id": "…", "dialect": "duckdb", "generated_at": "2026-09-13T15:04:05+00:00",
 "filters": {"limit": 20, "dry_run": false, "stop_on_first_failure": false, "executable_only": false, "format_values": false},
 "summary": {"total": 6, "executed": 6, "compiled": 0, "skipped": 0, "failed": 0, "with_findings": 4},
 "results": [
 {"name": "High Return Rate", "type": "classification", "level": "aggregate", "findings": "matches", "severity": null,
 "status": "executed", "finding_count": 2, "columns": ["Product Category", "Return Rate"], "rows": [["Toys", 0.125]],
 "sql": null, "error": null, "cached": false, "elapsed_ms": 12.4}
 ],
 "elapsed_ms": 80.3
}

Statuses: executed, compiled (dry run), skipped (executable_only and the query does not compile), failed (compile or execution error, with error).

POST /v1/sessions/{session_id}/models/{model_id}/rules/compile

Compile every rule. A rule that fails is a row with status: "failed" and its error; the response is always 200.

{
 "dialect": "duckdb",
 "results": [{"name": "Electronics Sale", "status": "compiled", "level": "row", "findings": "matches", "sql": "SELECT ...", "error": null}],
 "compiled": 6,
 "failed": 0
}

Model Examples

Canonical example queries authored alongside the model in OBML's optional examples: block. Surfaced through these endpoints so agents can discover what kinds of questions a model is designed to answer in one round trip.

See docs/guide/model-format.md for the OBML examples: syntax.

GET /v1/sessions/{session_id}/models/{model_id}/examples

List every example summary in the loaded model.

Query parameters (optional):

Param Description
intent Filter by intent tag (case-insensitive). Resolution: exact tag match → substring match → fuzzy match against the tag corpus.

Response (200):

{
 "examples": [
 {
 "name": "revenue_by_country",
 "description": "Total completed-order revenue, broken down by customer country.",
 "intent_tags": ["revenue", "geography"]
 }
 ],
 "suggestion": null
}

Response (200, ?intent= did not match anything):

{
 "examples": [],
 "suggestion": "no examples for 'foo'; available tags: revenue, orders, geography"
}

GET /v1/sessions/{session_id}/models/{model_id}/examples/{example_name}

Return a single example by name, with the full query payload and a best-effort compiled SQL preview.

Response (200):

{
 "name": "revenue_by_country",
 "description": "Total completed-order revenue, broken down by customer country.",
 "intent_tags": ["revenue", "geography"],
 "query": {
 "select": {
 "dimensions": ["Customer Country"],
 "measures": ["Total Revenue"]
 }
 },
 "compiled_sql_preview": "SELECT ..."
}

compiled_sql_preview is null when the example fails to compile against the current model (e.g. the model has drifted since the example was authored).

Error (404): Example name not found in model.


Lineage

What a dimension, measure, metric, rule or query is built from, followed down to the tables it reads. Names are looked up per artefact type, because a rule may share its name with a measure, so each type has its own route.

Lineage of Endpoint
Dimension GET /v1/sessions/{session_id}/models/{model_id}/dimensions/{name}/lineage
Measure GET /v1/sessions/{session_id}/models/{model_id}/measures/{name}/lineage
Metric GET /v1/sessions/{session_id}/models/{model_id}/metrics/{name}/lineage
Rule GET /v1/sessions/{session_id}/models/{model_id}/rules/{name}/lineage
Query POST /v1/sessions/{session_id}/query/lineage, body like query/sql

A query is compiled first, so its lineage includes the joins the planner chose, across every leg of a multi-fact query. An unknown name is a 404; a query that does not resolve is a 422, as at query/sql.

What each artefact depends on:

Artefact Sources
Dimension Its column; the via data object
Measure Its columns or expression columns; filter columns (filter); withinGroup column (order); grain-override dimensions (grain); anchor data object; a synthesized count reads its data object's rows (rows)
Metric The measures and metrics its expression names; for cumulative and window metrics the measure plus the time (time) and partition (partition) dimensions; for reaggregate metrics the measure plus its per dimensions (per) and the measures its having reads (having)
Rule The fields its condition reads (condition), the rules it references (rule), its grain dimensions (grain)
Query Its dimensions, measures and metrics; raw select.fields columns (field); where, having and order fields; an exists / nonexists subquery's data object and filter fields (labelled with the operator); the planner's joins between data objects (join on ..., drawn as the model declares it, with the join's pathName as path_name; a role-playing dimension's join is named by its declared objects, not its alias). Fields resolve as the compiler reads them: a raw field is always its column, and inside a subquery a column of the subquery's data object shadows a same-named dimension. A multi-fact query adds a union node: each leg's measures feed it (leg <fact>), and the metrics and the query read those measures through it, as the UNION ALL that combines the legs does

A computed column depends on the columns its expression reads (expression).

format query parameter:

Value Response
json (default) root, nodes (id, kind, name, detail), edges (source, target, label, path_name) and mermaid. Node kinds: data_object, column, dimension, measure, metric, rule, union, query
mermaid The Mermaid flowchart as text/vnd.mermaid
turtle text/turtle over the same IRIs as the model's OBSL graph, so the two merge; each edge is target prov:wasDerivedFrom source (W3C PROV), and a query is derived from the IRI of each declared join it used, secondary joins included, and a query or union is a prov:Entity; a query is derived from the joins it used
curl "http://127.0.0.1:8000/v1/metrics/Gross%20Margin/lineage?format=mermaid"
curl -X POST "http://127.0.0.1:8000/v1/query/lineage?format=turtle" \
  -H "Content-Type: application/json" \
  -d '{"select": {"dimensions": ["Country Name"], "measures": ["Total Sales"]}}'

OBSL Graph & SPARQL

GET /v1/sessions/{session_id}/models/{model_id}/graph

Return the OBSL-Core RDF graph as Turtle. The graph is generated at model load time.

Response (200): text/turtle

@prefix obsl: <https://ralforion.com/ns/obsl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://ralforion.com/ns/model/abc123> a obsl:SemanticModel ;
 obsl:hasDataObject <.../data-object/orders> ;
 obsl:hasMeasure <.../measure/revenue> .

Error (404): Session or model not found.

POST /v1/sessions/{session_id}/models/{model_id}/sparql

Execute a read-only SPARQL query against the model's OBSL graph.

Request:

{
 "query": "PREFIX obsl: <https://ralforion.com/ns/obsl#> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT ?label WHERE { ?m a obsl:Measure ; rdfs:label ?label . }"
}

Only SELECT and ASK queries are allowed. The query field has a maximum length of 100,000 characters.

Response (200):

{
 "type": "select",
 "variables": ["label"],
 "results": [
 {"label": "Revenue"},
 {"label": "Order Count"}
 ],
 "boolean": null,
 "warnings": []
}

For ASK queries:

{
 "type": "ask",
 "variables": [],
 "results": [],
 "boolean": true,
 "warnings": []
}

warnings lists non-fatal findings. Today that is a variable that is ordered by or projected but never bound in a triple pattern, BIND or VALUES: valid SPARQL (an unbound variable compares equal everywhere, so it orders nothing and its column is empty) and nearly always a typo, e.g. "ORDER BY ?lable: the variable is never bound, so it orders nothing".

Error (400): Update query rejected or invalid SPARQL syntax.

Error (404): Session or model not found.


Result cache

See docs/guide/result-cache.md for the full design and operational notes. Cache is off by default (CACHE_BACKEND=noop). Enable with CACHE_BACKEND=file and a writable CACHE_DIR.

GET /v1/cache/stats

Always responds — when CACHE_BACKEND=noop the response shows backend: "noop" with zero counters.

Response (200):

{
 "backend": "file",
 "entry_count": 1247,
 "total_size_bytes": 234567890,
 "max_size_bytes": 5368709120,
 "hit_count_total": 9821,
 "miss_count_total": 4203,
 "hit_rate": 0.700,
 "oldest_entry": "2026-04-15T12:30:00Z",
 "next_sweep_at": "2026-04-15T12:45:00Z",
 "tracked_physical_tables": 8,
 "heartbeat_invalidations_total": 142
}

POST /v1/cache/sweep

Triggers a single TTL + capacity eviction pass on demand — equivalent to one tick of the periodic sweeper. Safe to call at any time. With CACHE_BACKEND=noop returns zero counts.

Response (200):

{
 "backend": "file",
 "ttl_evicted": 17,
 "capacity_evicted": 0
}

POST /v1/cache/clear

Drops every cache entry regardless of TTL or freshness contract. Useful for manual resets and debugging. Counters (hit_count_total, miss_count_total, heartbeat_invalidations_total) are preserved as historical telemetry. With CACHE_BACKEND=noop returns zero.

Response (200):

{
 "backend": "file",
 "entries_cleared": 1247
}

Compilation cache

A separate, in-memory cache of compiled SQL (not rows): the SQL, warnings, validation status, explain plan and physical-table dependencies of a query, keyed by the loaded model, the dialect and the full query. A repeated query skips the compiler on every surface (REST, pgwire, Flight); result-cache freshness is still decided on every execution. It is on by default with COMPILE_CACHE_MAX_ENTRIES=2048 (about 7.5 MB on the TPC-DS example); 0 disables it. COMPILE_CACHE_MAX_BYTES (default 64 MB) caps its estimated memory and COMPILE_CACHE_MAX_ENTRY_BYTES (default 1 MB) skips larger entries. Entries of a model go when the last session holding it releases it.

GET /v1/cache/compilation

Always responds; enabled is false when COMPILE_CACHE_MAX_ENTRIES=0.

Response (200):

{
 "enabled": true,
 "entries": 312,
 "bytes": 1843200,
 "max_entries": 2048,
 "max_bytes": 67108864,
 "hits": 9120,
 "misses": 312,
 "bypasses": 0,
 "evictions": 0
}

bypasses counts queries whose filter values have no exact typed key; they are compiled every time.

POST /v1/cache/compilation/clear

Drops every compiled query. The result cache is not touched, and the counters are kept.

Response (200):

{
 "entries_cleared": 312
}

POST /v1/heartbeat

ETL pings this endpoint after refreshing a physical table. The cache invalidates every entry whose dependency set includes that table — across every dataObject and every session.

Authentication: Authorization: Bearer <HEARTBEAT_AUTH_TOKEN>. When the env var is unset, the route returns 404.

Request:

{
 "database": "WAREHOUSE",
 "schema": "PUBLIC",
 "table": "ORDERS",
 "timestamp": "2026-04-29T14:32:15Z"
}

timestamp is optional; defaults to server now(). Future timestamps are clamped to now().

Response (200):

{
 "table_ref": "WAREHOUSE.PUBLIC.ORDERS",
 "recorded_at": "2026-04-29T14:32:15Z",
 "invalidated_cache_entries": 47,
 "affected_data_objects": ["Orders", "OrderReturns", "OrdersPivoted"]
}

affected_data_objects lists every OBML name tied to this physical table at the moment of the heartbeat — useful for verifying your model maps the way you expect.

Status Cause
401 Missing or invalid bearer token
404 Heartbeat endpoint disabled (no HEARTBEAT_AUTH_TOKEN configured)
422 Invalid timestamp format

Per-query response fields

Every query/execute JSON response gains a cache observability block:

Field Description
cached Whether this result came from the cache.
cached_at ISO 8601 timestamp the cached result was first computed (null when fresh).
ttl_seconds Effective TTL applied to this entry.
ttl_source freshness_derived, caller_capped, default_unknown, no_cache:<reason>.
ttl_limiting_table Physical table whose contract drove the effective TTL.
physical_tables Deduplicated database.schema.code strings the query touched.

physical_tables is also surfaced on query/sql responses for clients that want to inspect plan reach without executing.

execution_time_ms on cache hits: when cached: true, this field reports the wall-clock time spent reading and decoding the cached entry — not the original database run time. The original DB timing is preserved on disk in the Parquet sidecar for forensic inspection but not surfaced on the wire. Combine with the cached flag to distinguish "fresh from warehouse" vs "served from cache" durations.


Top-level Shortcuts

These endpoints auto-resolve the session and model when only one exists. They mirror the session-scoped model discovery endpoints without requiring session/model IDs.

Returns 404 if no sessions exist, 409 Conflict if multiple sessions or models exist.

Shortcut Equivalent
GET /v1/schema GET /v1/sessions/{id}/models/{mid}/schema
GET /v1/dimensions GET /v1/sessions/{id}/models/{mid}/dimensions
GET /v1/dimensions/{name} GET /v1/sessions/{id}/models/{mid}/dimensions/{name}
GET /v1/measures GET /v1/sessions/{id}/models/{mid}/measures
GET /v1/measures/{name} GET /v1/sessions/{id}/models/{mid}/measures/{name}
GET /v1/metrics GET /v1/sessions/{id}/models/{mid}/metrics
GET /v1/metrics/{name} GET /v1/sessions/{id}/models/{mid}/metrics/{name}
GET /v1/explain/{name} GET /v1/sessions/{id}/models/{mid}/explain/{name}
POST /v1/find POST /v1/sessions/{id}/models/{mid}/find
GET /v1/join-graph GET /v1/sessions/{id}/models/{mid}/join-graph
GET /v1/concept-mappings GET /v1/sessions/{id}/models/{mid}/concept-mappings
GET /v1/concept-mappings/namespaces GET /v1/sessions/{id}/models/{mid}/concept-mappings/namespaces
GET /v1/concept-mappings/unmapped GET /v1/sessions/{id}/models/{mid}/concept-mappings/unmapped
GET /v1/rules GET /v1/sessions/{id}/models/{mid}/rules
GET /v1/rules/{name} GET /v1/sessions/{id}/models/{mid}/rules/{name}
POST /v1/rules/{name}/compile POST /v1/sessions/{id}/models/{mid}/rules/{name}/compile
POST /v1/rules/compile POST /v1/sessions/{id}/models/{mid}/rules/compile
POST /v1/rules/{name}/evaluate POST /v1/sessions/{id}/models/{mid}/rules/{name}/evaluate
GET /v1/{dimensions,measures,metrics,rules}/{name}/lineage GET /v1/sessions/{id}/models/{mid}/{type}/{name}/lineage
POST /v1/query/lineage POST /v1/sessions/{id}/query/lineage
POST /v1/rules/evaluate POST /v1/sessions/{id}/models/{mid}/rules/evaluate
GET /v1/graph GET /v1/sessions/{id}/models/{mid}/graph
POST /v1/sparql POST /v1/sessions/{id}/models/{mid}/sparql
POST /v1/query/sql POST /v1/sessions/{id}/query/sql (auto-resolves model_id)

Settings

GET /v1/settings

Return public configuration for API clients (UI, MCP, etc.).

Query parameters (optional) — both default to null:

Param Description
session_id Scope model_settings, timezone, and dialect.model to this session. If the session holds exactly one model, that model is used; otherwise the model-specific blocks are omitted (request model_id to disambiguate).
model_id Pin to a specific model in session_id. Returns 400 without session_id, 404 if the session or model is unknown.

Resolution rules without query parameters:

  • single-model mode → uses the preloaded model
  • multi-model mode → uses the unique model across all sessions if exactly one is loaded; otherwise the model-specific blocks are omitted

Response (200) — multi-model mode:

{
 "version": "2.34.0",
 "api_version": "v1",
 "single_model_mode": false,
 "session_ttl_seconds": 1800,
 "session_max_age_seconds": 86400,
 "max_sessions": 500,
 "max_models_per_session": 10,
 "query_execute": false,
 "dialect": {
 "env": "duckdb",
 "effective": "duckdb"
 }
}

Response (200) — admin-curated mode (MODEL_FILES is configured):

{
 "version": "2.34.0",
 "api_version": "v1",
 "single_model_mode": true,
 "model_yaml": null,
 "session_ttl_seconds": 1800,
 "session_max_age_seconds": 86400,
 "max_sessions": 500,
 "max_models_per_session": 10,
 "query_execute": true,
 "model_settings": {
 "defaultTimezone": "Europe/Berlin",
 "defaultDialect": "snowflake",
 "overrideDatabaseTimezone": false,
 "defaultNumericDataType": "decimal(38, 4)"
 },
 "timezone": {
 "model": "Europe/Berlin",
 "host": "Europe/Berlin",
 "database": null,
 "effective": "Europe/Berlin",
 "override_database_timezone": false,
 "now": "2026-04-29T15:30:00+02:00",
 "utc": "2026-04-29T13:30:00Z"
 },
 "dialect": {
 "model": "snowflake",
 "env": "duckdb",
 "effective": "snowflake"
 }
}
Field Type Description
version string OrionBelt Semantic Layer release version
api_version string REST API version prefix (v1)
single_model_mode bool Whether model upload/removal is disabled
model_yaml string | null Pre-loaded OBML YAML (single-model mode only)
session_ttl_seconds int Session inactivity timeout
session_max_age_seconds int Absolute max session lifetime
max_sessions int Global concurrent session cap
max_models_per_session int Maximum models per session
query_execute bool Whether POST /query/execute is available
flight object | null Arrow Flight SQL info (when Flight is enabled)
model_settings object | null Loaded model's settings: block (single-model mode)
timezone object | null Timezone resolution chain (single-model mode)
dialect object SQL dialect resolution chain (always present)

model_settings mirrors the OBML settings: block in camelCase — defaultNumericDataType, defaultTimezone, overrideDatabaseTimezone, defaultDialect, defaultLocale, queryTimezone, weekStart, expressionMode. It reports the settings in force, not the keys the YAML wrote: a setting with a default (overrideDatabaseTimezone, weekStart, expressionMode) reports that default whether the model stated it or not, because a client asking which calendar or expression mode applies wants the answer, and an absent key cannot distinguish "unset" from "unsupported by this server". Settings with no default (defaultTimezone, defaultDialect, defaultLocale, defaultNumericDataType, queryTimezone) are absent when unset. A test asserts this mirror carries every field ModelSettings declares, with the same defaults, so a setting cannot be added to the model and go missing or disagree here.

timezone is the chain db_executor.resolve_timezone() walks at execute time. Always present so clients can show the wall clock even without a loaded model:

  • override_database_timezone: true → model wins, falling back to host then UTC.
  • otherwise → cached database session timezone wins (when known), then model, then host, then UTC.

The endpoint never probes the database — database is null until a query has run for that dialect. effective is the timezone that will be applied right now. now is the current wall-clock time in the effective TZ (ISO 8601 with offset suffix); utc is the same instant in UTC.

dialect mirrors how the planner resolves the dialect when the request body omits dialect: model.defaultDialect → DB_VENDOR env → postgres. effective is what would be used for a dialect-less request.


Dialects

GET /v1/dialects

List all available SQL dialects, their capability flags, and the vocabulary each one supports.

capabilities are structural SQL feature flags. supported_aggregations lists the OBML aggregation: values the dialect can compute, and supported_functions the entries of the portable function catalog it can render — both stated positively, so a client needs no second call to learn the full vocabulary and subtract from it. Signatures, arity and pinned semantics for the functions are at GET /v1/reference/functions.

Response (200):

{
  "dialects": [
    {
      "name": "bigquery",
      "capabilities": {
        "supports_cte": true,
        "supports_qualify": true,
        "supports_arrays": true,
        "supports_window_filters": true,
        "supports_ilike": false,
        "supports_time_travel": false,
        "supports_semi_structured": true,
        "supports_union_all_by_name": false,
        "supports_group_by_all": true
      },
      "supported_aggregations": [
        "any_value", "avg", "corr", "count", "count_distinct", "covar_pop",
        "covar_samp", "first", "last", "listagg", "max", "median", "min", "mode",
        "percentile_cont", "percentile_disc", "stddev", "stddev_pop", "sum",
        "var_pop", "variance"
      ],
      "supported_functions": [
        "abs", "ceil", "coalesce", "concat", "div", "ends_with", "exp", "floor",
        "greatest", "least", "length", "ln", "log", "lower", "lpad", "ltrim",
        "mod", "nullif", "position", "power", "replace", "round", "rpad",
        "rtrim", "sign", "split_part", "sqrt", "starts_with", "substring",
        "trim", "trunc", "upper"
      ]
    },
    { "name": "clickhouse", "capabilities": { "...": true }, "supported_aggregations": ["..."], "supported_functions": ["..."] },
    { "name": "databricks", "capabilities": { "...": true }, "supported_aggregations": ["..."], "supported_functions": ["..."] },
    { "name": "dremio", "capabilities": { "...": true }, "supported_aggregations": ["..."], "supported_functions": ["..."] },
    { "name": "duckdb", "capabilities": { "...": true }, "supported_aggregations": ["..."], "supported_functions": ["..."] },
    { "name": "mysql", "capabilities": { "...": true }, "supported_aggregations": ["..."], "supported_functions": ["..."] },
    { "name": "postgres", "capabilities": { "...": true }, "supported_aggregations": ["..."], "supported_functions": ["..."] },
    { "name": "snowflake", "capabilities": { "...": true }, "supported_aggregations": ["..."], "supported_functions": ["..."] }
  ]
}

An aggregation absent from supported_aggregations is refused at compile time with a 422 rather than emitted and failed at the warehouse: MySQL has no mode, corr, covar_pop, covar_samp, regr_slope or regr_intercept, and measure is Databricks-only. Every dialect renders the whole function catalog today, since an entry is admitted only once all eight can answer it.