Configure the semantic layer
Generate a database connection's semantic layer, then describe its tables and add business definitions and KPIs.
Agents that use the connection get the layer in their prompt: business names, relationships, definitions and KPIs instead of raw names (Orders for public.tbl_ord). /query refuses tables with is_included: false.
Endpoints
Base URL: /api/v1/data-sources/connections/{connection_id}/semantic-layer.
| Method | Path | Body | Does | Auth |
|---|---|---|---|---|
GET | / | Read the layer and its status | Key or bearer | |
POST | /initialize | none | Regenerate the layer from the database; returns status: "generating" | Key or bearer |
PATCH | /tables/{table_name} | display_name, purpose, domain, is_included | Describe or exclude one table | Key or bearer |
GET · POST | /definitions | name, description, domain, conditions | List · add business terms | Key or bearer |
PATCH · DELETE | /definitions/{definition_id} | name, description, domain, conditions | Edit · delete one | Key or bearer |
GET · POST | /kpis | name, description | List · add KPIs | Key or bearer |
PATCH · DELETE | /kpis/{kpi_id} | name, description | Edit · delete one | Key or bearer |
- Reads need Read connections; writes need Edit connections (
connection:edit). - On an
oauth2connection only its owner can write; others get403 Only the connection owner can modify its semantic layer. - Use the paths exactly as listed; only
GET /ends with a slash. An added or missing slash returns a307redirect. DELETEreturns204even when the ID does not exist.
Layer fields
| Field | Notes |
|---|---|
status | generating, ready or failed. A generating older than 15 minutes reads as failed |
config.tables[] | name (as /metadata names it, e.g. public.tbl_ord), display_name, purpose, domain, is_included, column_samples |
config.relationships[] | from_table, to_table, from_columns, to_columns, cardinality, join_type, confidence, when_to_use |
config.definitions[] | id, name, description, domain, conditions[], source |
config.kpis[] | id, name, description, formula, sql, tables, source |
source is ai-generated, manual (the default for what you add) or a label you send.
Generate the layer
A new connection with connector_type database and category data_source generates its layer automatically.
-
To regenerate it, send
POST …/semantic-layer/initialize. Optional?sample_columns_per_table=(0–200, default 20; 0 turns off value sampling):curl -X POST "$CENTRAL_HUB_URL/api/v1/data-sources/connections/123/semantic-layer/initialize" \ -H "X-API-Key: $ALO_API_KEY" \ -H "X-Space-Slug: $WORKSPACE_SLUG"The response returns at once with
"status": "generating"and the currentconfig. -
Poll
GET …/semantic-layer/untilstatusisreadyorfailed:curl "$CENTRAL_HUB_URL/api/v1/data-sources/connections/123/semantic-layer/" \ -H "X-API-Key: $ALO_API_KEY" \ -H "X-Space-Slug: $WORKSPACE_SLUG"{ "connection_id": 123, "status": "ready", "config": { "tables": [ { "name": "public.tbl_ord", "display_name": "Orders", "purpose": "Records order transactions, including the purchasing customer, transaction amount, and order creation date.", "domain": "Sales", "is_included": true, "column_samples": {"amount": ["80.00", "120.50", "42.00"]} } ], "relationships": [ {"from_table": "public.customers", "to_table": "public.tbl_ord", "from_columns": ["id"], "to_columns": ["cust_id"], "cardinality": "1:N", "join_type": "LEFT", "confidence": "high", "when_to_use": "To join order transactions with customer profile and demographic information."} ], "definitions": [], "kpis": [], "status": "ready", "generated_at": "2026-10-13T19:38:46.075462+00:00" } }
Regenerating rebuilds tables: your display_name, purpose, domain and is_included edits are lost, so excluded tables are included again. Definitions, KPIs and relationships stay; new AI-generated ones are added when their name, or table pair, is not taken. If no table is readable, status becomes failed.
Describe or exclude a table
-
Find the table's exact
nameinconfig.tables[]; for PostgreSQL it is schema-qualified. -
Send only the fields you change.
is_included: falsehides the table from agents and from/query:curl -X PATCH "$CENTRAL_HUB_URL/api/v1/data-sources/connections/123/semantic-layer/tables/public.tbl_ord" \ -H "X-API-Key: $ALO_API_KEY" \ -H "X-Space-Slug: $WORKSPACE_SLUG" \ -H "Content-Type: application/json" \ -d '{"display_name": "Orders", "purpose": "Customer purchase transactions", "domain": "Sales", "is_included": true}'The response is the whole layer.
Add a business definition
-
Send
nameanddescription, with optionaldomainandconditions. A condition'soperatorisequals,not_equals,contains,greater_than,less_than,inornot_in:curl -X POST "$CENTRAL_HUB_URL/api/v1/data-sources/connections/123/semantic-layer/definitions" \ -H "X-API-Key: $ALO_API_KEY" \ -H "X-Space-Slug: $WORKSPACE_SLUG" \ -H "Content-Type: application/json" \ -d '{ "name": "Active Customer", "description": "A customer with an order in the last 90 days", "domain": "Sales", "conditions": [{"field": "tbl_ord.created_at", "operator": "greater_than", "value": "NOW() - INTERVAL '"'"'90 days'"'"'"}] }' -
Keep the
idfrom the201response to edit or delete it later:{ "id": "06a5349b-05bf-49c3-aaf4-ec8aad1aac03", "name": "Active Customer", "description": "A customer with an order in the last 90 days", "domain": "Sales", "conditions": [{"field": "tbl_ord.created_at", "operator": "greater_than", "value": "NOW() - INTERVAL '90 days'"}], "source": "manual" }
Add a KPI
-
Send
nameanddescription. The SQL is always AI-generated from them and the layer: anyformula,sqlortablesyou send is replaced:curl -X POST "$CENTRAL_HUB_URL/api/v1/data-sources/connections/123/semantic-layer/kpis" \ -H "X-API-Key: $ALO_API_KEY" \ -H "X-Space-Slug: $WORKSPACE_SLUG" \ -H "Content-Type: application/json" \ -d '{"name": "Total Revenue", "description": "Sum of all order amounts"}' -
Read
formula,sqlandtablesfrom the201response. When generation fails they arenull,nulland[]:{ "id": "7e7e537c-6fc8-41f0-afcf-77c72c1c7d4b", "name": "Total Revenue", "description": "Sum of all order amounts", "formula": "SUM(tbl_ord.amount)", "sql": "SELECT SUM(amount) AS total_revenue FROM public.tbl_ord", "tables": ["public.tbl_ord"], "source": "manual" }
PATCH …/kpis/{kpi_id}accepts onlynameanddescription; changing either generates the SQL again.- KPI names are not unique:
POST …/kpisadds a KPI even when one with that name exists. List them first withGET …/kpis.
Troubleshooting
| Response | Cause | Fix |
|---|---|---|
404 Table 'tbl_ord' not found in semantic layer | table_name is not the exact config.tables[].name | Use the schema-qualified name, e.g. public.tbl_ord |
422 Input should be 'equals', 'not_equals', 'contains', 'greater_than', 'less_than', 'in' or 'not_in' | Unknown condition operator | Use one of those values |
403 Only the connection owner can modify its semantic layer. | A write on another user's oauth2 connection | Ask the owner |
status stays failed | No readable table, or generation stopped | Check the credentials can read the tables, then POST …/initialize again |
404 Definition '<id>' not found · KPI '<id>' not found | Wrong ID on PATCH | List them with GET …/definitions or GET …/kpis |