Plenty of developers build the same thing two or three times before they settle on a semantic layer.
You are wiring an AI agent to some structured data, yours or your customers’. First you let the agent write raw SQL. It works, but it gives the model too much room, and you are never sure about the correctness of the outputs. So you try to constrain it by building an MCP server exposing a predefined set of metrics, which is either too constrained or too hard to maintain. Then you come across Cube and give it a try. It runs, but now you have to deploy another microservice and build custom wiring around it to actually integrate into your app.
A semantic layer is usually sold as software for data teams. But as a developer, you use it not as a standalone tool but as a component of a larger system, a feature. So what really matters is ease of integration and the cost of running and maintaining the solution.
Cube and SLayer are both open-source semantic layers, so let’s compare.
What a semantic layer does, and why it changed
A semantic layer sits between your data and whatever queries it. You define what can be queried, and every consumer asks for revenue by region last quarter
instead of writing the SQL by hand, so everyone uses the same definition.
The semantic layer was usually part of a BI stack driven by an analyst. Now it’s increasingly used for giving AI agents a smaller, governed surface to query when SQL generation doesn’t work well.
Cube has been around since 2019 and has the largest community in the category, with an Apache 2.0 license. SLayer is newer, MIT-licensed, and built from the start with the agent as the primary consumer. The differences below follow from that difference in starting point.
A service or a tool
Cube runs as a standalone service. In practice that is a Node.js API server you deploy in its own container, scale, monitor, and secure. For many teams that is acceptable, but for others this overhead can make the project sit in staging.
SLayer can run in-process. If your app is written in Python, you just pip install motley-slayer
, import it as a library, and it runs in the same event loop as everything else. You create a SlayerClient
, give it the models and the query, and it returns the data, giving you control over the entire process.
You can still run it as a CLI, or as a standalone server exposing a REST API and an MCP server.
Why not just let the agent write SQL
This is the question every developer building this hits first, and the answer separates SLayer from the newer wave of AI-on-data tools.
Letting an agent write raw SQL gives it too much room. It is hard to audit, hard to reproduce, and there are many ways for a query to be subtly wrong. Some people solve it by providing context to the agent that explains which queries it should make, but it’s hard to enforce and the final SQL is still up to the agent.
SLayer deliberately avoids that, instead exposing an expressive query language that channels the complexity under the hood. A simple 5-line query can translate deterministically into 30 or 50 lines of SQL, with the time shifts and transforms handled carefully and compiled to whatever SQL dialect you are on. The agent gets flexibility at query time without ever touching raw SQL.
{ "source_model": "orders", "measures": ["*:count", "revenue:sum", "change_pct(revenue:sum)"], "time_dimensions": [{"dimension": "ordered_at", "granularity": "week"}] }
The layer is expressive enough to answer real questions, but in an accurate, governed way.
Does the constrained approach cost accuracy?
A fair worry: if the agent cannot write SQL, does it answer worse?
We ran SLayer against a large dynamic-SQL benchmark called BIRD-Interact. The setup gives an agent the table information, then runs multiple rounds where the agent can ask clarifying questions, with fully automated schema ingestion (we do not hand-tune the context). We compared an agent querying through SLayer to the same agent (Claude via the SDK) writing raw SQL. The tool-driven agent matched or beat the raw-SQL agent.
Our interpretation of this result is that a governed query interface is a smaller, cleaner surface, so the agent has less to get wrong. (Full methodology and numbers in a separate, upcoming post.)
Measures: defined upfront vs computed at query time
In Cube, measures must be predefined in the model, each with a fixed aggregation. Revenue summed is a revenue_sum
measure. Average revenue per period is a second measure. Window functions, period-over-period changes, running totals: each needs its own definition. This is great for dashboards, when you know exactly which metrics you will need and want to pre-aggregate them to speed up queries. We found it suboptimal for agents though, unless you deliberately want to constrain them tightly – because they can’t get the average revenue
if you only have the sum in the model.
SLayer allows composing measures on the fly. In the models, you describe fields that can serve as a dimension or as the basis for an aggregation, and you request the aggregation at query time: revenue:sum
, revenue:avg
, or with a transform like cumsum(revenue:sum)
.
name: orders
sql_table: public.orders
data_source: my_postgres
columns:
- name: status
sql: status
type: string
- name: created_at
sql: created_at
type: time
- name: revenue
sql: amount
type: number
That single model backs count
, revenue:sum
, revenue:avg
, running totals, and period shifts, without a separate measure for each. You can still store a named measure in the model when a metric has a fixed complex formula, and have the agent call it by name. The effect is much smaller models for the same coverage, which matters when an agent is composing the query. The expense is the absence of a pre-aggregation engine, but we found sub-second latency matters less in LLM-based workflows.
Data discovery
As the semantic layer becomes also a data exploration tool, we need a way to see what is there. Dumping every table and measure into the prompt is usually a bad way to go because it pollutes context and has very low signal-to-noise ratio when looking to answer a question.
SLayer has built-in MCP tools that enable precise data discovery:
- a
search tool that searches across the models (fields, measures, descriptions) and an optional natural-language memory store the agent can update with business context; - an
explore tool that returns a model’s structure and a few sample rows; - a
help tool that works like a command-line
-help
, so the agent can look up advanced functions on demand instead of carrying the full manual in context.
The agent searches, explores the one or two models that matter, and composes its query.
Cube only exposes a global /meta API endpoint that returns all available models and their members, so you have to build your own data discovery mechanism if this is not enough.
Schema upkeep
When your database schema changes, your semantic models must also change. A common workaround on Cube is to generate the YAML models dynamically from your backend so they keep up, or write a custom repository_factory
that reads the models from a database or some other place.
SLayer helps in these ways, depending on your setup:
- If you supply models in code, this becomes a non-issue because they are always up-to-date;
- If your models are static and you must review the changes manually, having smaller models helps – e.g. you need to only update
revenue
instead of every possible measure referencing it; - SLayer automatically detects when something references a column that does not exist, warns the caller, and has a helper for automatically updating the models
Connectors
Both tools support most popular databases and warehouses.
SLayer generates dialect-aware SQL through sqlglot, with datasources in two tiers. Postgres, MySQL, ClickHouse, DuckDB, BigQuery, Snowflake and SQLite are first-class: verified by integration tests against live instances and runnable Docker examples, with regressions caught in CI. Redshift, Databricks, Trino, Presto, Athena, Oracle, and SQL Server (2022 and up) are supported at the SQL-generation tier, covered by unit tests. If your database is supported by sqlglot it may already work, since SLayer falls back to Postgres-style SQL by default.
Cube’s connectors are more uniformly battle-tested which can be a real advantage, especially if you’re using a datasource in production which is second-tier in SLayer.
Multi-tenancy and row-level security
Cube’s main mechanism for multi-tenancy is the query_rewrite
configuration function that allows modifying queries according to the supplied security context. Cube also allows filtering model members depending on that security context. This works well if your models are static and you code the rewrite logic carefully.
If you’re using SLayer as a Python library, both of those methods are achievable through your own code. SLayer’s built-in tenant isolation is on the near-term roadmap; the difference from Cube here will be that SLayer’s security policies will work on SQL level as well as on model level, ensuring no row is ever accessed by the wrong tenant even if the models are modified on the fly.
Cube API also supports auth via JWT which SLayer does not — this matters if you want a standalone service that clients talk to directly.
Connecting to your users’ data
This is a fairly specific use case, but exactly the one we had at Motley: we needed a solution for connecting to a variety of databases our users had and querying them through a unified surface. Obviously, each user had their own data schema, so we stored the models in our DB, alongside all other user data.
With Cube, we had to build a custom wiring to read the models from our DB and keep them in sync (they could change in runtime). We built SLayer when we outgrew that setup, and the models now don’t need to travel anywhere, since they are just supplied to the engine on instantiation, and the semantic layer is just a feature of the app.
Another specific, but important aspect arises when your users have their own agents connecting to the semantic layer. Often you want them to maintain the semantic layer for themselves: add new measures and models, record business context. Cube APIs only allow querying the data, while SLayer also exposes tools for managing the semantic layer itself.
When to pick which
Choose Cube if your models are static, if sub-second latency and pre-aggregations matter to you, if you need a standalone service with auth and row-level security, or if you want the most battle-tested connector for a specific data warehouse.
Choose SLayer if you want a semantic layer embedded in your Python app, if you would rather compose aggregations at query time than predefine them, and if your primary consumer is an AI agent, or if you want consumers to manage the semantic layer. It is especially strong when you serve many differently-shaped datasets, because spinning up a governed layer per context is cheap when it is simply a library.
Both tools are open source, easy to play around with and compare.
SLayer is MIT-licensed and on PyPI:
pip install motley-slayer
Repo and docs: https://github.com/MotleyAI/slayer