SLayer — Semantic Layer for AI Agents

SLayer — AI agent operating a semantic layer

A lightweight, open-source semantic layer by MotleyAI. Agents describe what data they want — measures, dimensions, filters — and SLayer generates the SQL.

GitHub | PyPI | Discord

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 same revenue measure works with sum, 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_shift runs 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. last is an aggregation that picks the latest record's value — changed_at tells it which column defines "latest."
  • customers.regions.name — a multi-hop dimension: SLayer traces orders → customers → regions and 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 transformscumsum, 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.

Full concept docs: Models | Queries | Formulas