API Endpoints¶
Complete reference for all OrionBelt REST API endpoints.
Health Check¶
GET /health¶
Returns the service status and version.
Response:
Sessions¶
POST /v1/sessions¶
Create a new session. Each session has its own model store.
Request (optional):
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:
| 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):
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:
Response (200):
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, structuredwarningswith the failure cause. include_database_explain: truebut the warehouse rejectsEXPLAIN→ OBSL plan still returned with aDATABASE_EXPLAIN_FAILEDwarning describing why;database_explainisnull.
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"] } }'
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:
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:
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:
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):
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):
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):
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→modelwins, falling back tohostthenUTC.- otherwise → cached
databasesession timezone wins (when known), thenmodel, thenhost, thenUTC.
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.