SLayer — Semantic Layer for AI Agents¶
A lightweight, open-source semantic layer by MotleyAI. Agents describe what data they want — measures, dimensions, filters — and SLayer generates the SQL.
Why?¶
When AI agents write raw SQL, they can get joins wrong, and produce metrics that drift between queries.
Existing semantic layers (Cube, dbt semantic layer) were built for dashboards — heavy infrastructure, slow model refresh cycles, and not enough flexibility for ad-hoc agent queries.
SLayer is different: models are editable at runtime, aggregation is chosen at query time, and there's no build step.
What it looks like¶
Given an orders model with a revenue measure and joins to customers and regions:
{
"source_model": "orders",
"measures": [
"revenue:sum",
{"formula": "change_pct(revenue:sum)", "name": "mom_growth"},
{"formula": "revenue:sum / time_shift(revenue:sum, -1, 'year') - 1", "name": "yoy_growth"},
"customers.score:last(changed_at)"
],
"dimensions": ["customers.regions.name"],
"time_dimensions": [{
"dimension": "created_at",
"granularity": "month",
"date_range": ["2025-01-01", "2025-12-31"]
}],
"filters": ["status = 'completed'", "change(revenue:sum) > 0"],
"order": [{"column": "revenue_sum", "direction": "desc"}]
}
One query, and SLayer handles:
revenue:sum— aggregation is chosen at query time, not baked into the measure definition. The samerevenuemeasure works withsum,avg,median,weighted_avg, or any custom aggregation.change_pct(revenue:sum)— month-over-month growth as a transform. SLayer generates the necessary window query. Other built-in transforms:cumsum,change,time_shift,rank/percent_rank/dense_rank/ntile,lag,lead— all nestable ("change(cumsum(revenue:sum))"works).revenue:sum / time_shift(revenue:sum, -1, 'year') - 1— arithmetic on aggregated measures.time_shiftruns a separate time-shifted sub-query and joins it back by all dimensions; dividing by it gives year-over-year growth. Standard operator precedence applies.customers.score:last(changed_at)— a measure from a joined model, resolved by walking the join graph.lastis an aggregation that picks the latest record's value —changed_attells it which column defines "latest."customers.regions.name— a multi-hop dimension: SLayer tracesorders → customers → regionsand builds the joins automatically.change(revenue:sum) > 0— filtering on a computed transform. SLayer computes the transform first as a hidden field, then applies the filter on the outer query.
What SLayer does¶
- Auto-ingestion — Point it at a database, it introspects the schema, detects foreign keys, and generates models with joins. No manual YAML needed to get started (tutorial). Re-run the same idempotent pass on every server boot with
slayer serve --ingest-on-startup/slayer mcp --ingest-on-startup. - Aggregation at query time — Measures are expressions, not pre-baked aggregates.
"revenue:sum","revenue:median","price:weighted_avg(weight=quantity)". Built-in and custom aggregations with parameters. - Composable transforms —
cumsum,change,change_pct,time_shift,rank/percent_rank/dense_rank/ntile,lag,lead— all nestable:"change(cumsum(revenue:sum))"just works (tutorial). - Cross-model measures — Query measures from joined models with dot syntax:
"customers.score:avg". Joins are auto-resolved by walking the model graph (tutorial). - Multistage queries — Use one query as the source for another, or save any query as a permanent model.
- Runtime model editing — Add measures, dimensions, and joins through any interface. No rebuild, no restart.
- Broad database support — Integration-tested against Postgres, MySQL, ClickHouse, DuckDB, and SQLite. Others via sqlglot.
Get started¶
- MCP — for AI agents (Claude Code, Cursor, etc.)
- CLI — query from the terminal, manage models and datasources
- REST API — build apps in any language
- Python SDK — embed SLayer directly, no server needed
Under the hood¶
Agent --> MCP / REST API / Python SDK
|
SlayerQuery (source_model, measures, dimensions, filters)
|
SlayerQueryEngine (resolves model definitions from storage)
|
EnrichedQuery (resolved SQL expressions, model metadata)
|
SQLGenerator (sqlglot AST --> dialect-aware SQL)
|
SlayerSQLClient (SQLAlchemy --> database)
|
SlayerResponse (data, columns, sql)
SlayerQuery is what the user sends — names and references, no SQL. EnrichedQuery is the engine-internal form where every measure and dimension carries its resolved SQL, aggregation, and model context. New datasource adapters only need to translate EnrichedQuery.