FeatureMesh:
Foundations for happy data teams
Express your business logic as FeatureQL formulas, and some rather nice consequences follow.
FeatureMesh turns them into foundations your data team can build on:
enjoyable analytics today, and the conditions for AI to optimize the business tomorrow.
Or use locally with no account: pip install featuremesh
The immediate consequences
Enjoyable self-service analytics
Named, composable, pure: properties that are easy to overlook at first.
They make all the difference once you build on top of them.
Give your AI assistant FeatureQL instead of SQL and .md notes
Text-to-SQL answers you can trust
The assistant composes named, typed definitions. A wrong join fails to compile instead of returning a wrong number.
Read moreWrite the gold layer in FeatureQL, with automatic caching
New metrics without new pipelines
Analysts change definitions the same day. Pipelines become caches, decoupled from what users query.
Read moreKeep every KPI and business rule in one registry
Clear audits and risk management
Each rule is one named object, with its dependencies and its history. Reviewers read one place, not several codebases.
Read moreRun the same definitions on DuckDB, Trino or DataFusion
Lower cloud compute costs
Move caching and repeated queries to the cheapest engine that fits, without rewriting SQL across dialects.
Read moreThe longer-term consequences
AI can optimize your business
Decoupled from execution: your rules run anywhere, even in your backend at request time.
Deterministic and versioned: agents can test and change them safely.
Serve business rules as APIs, straight from the registry
Rule changes without an engineering release
Ops adjust eligibility, pricing or promotions. The application keeps calling the same endpoint.
Read moreLet AI agents test and roll out rule changes, within limits you set
Continuous optimization, without a human bottleneck
Agents propose, backtest and roll out variants. People decide how results are judged.
Read moreIllustrative business use case
One promo rule,
from definition to production
Should this customer see a promo code?
Steps 1 and 2 are the analytics work, useful on their own.
Steps 3 and 4 serve the same rule in production and improve it by experiment.
1. Build foundations
Data engineers do this once, with an assistant if they like. Nothing here is specific to the promo.
Define entities and keys
Business objects first. Typed keys make invalid joins fail at compile time.
CREATE FEATURES IN fm.home AS PERSIST customers := ENTITY(), orders := ENTITY(), customer_id := INPUT(BIGINT#customers), order_id := INPUT(BIGINT#orders), ;
Map the tables
Warehouse columns become source features: business names, not table schemas.
CREATE FEATURES IN fm.home AS PERSIST tables.fct_orders := EXTERNAL_COLUMNS( order_id BIGINT#orders BIND TO order_id, order_customer_id BIGINT#customers, price DECIMAL, created_at TIMESTAMP FROM TABLE(home.fct_orders) ), order_price := tables.fct_orders[price], -- …other columns, customers, exposure events ;
Define the shared metrics
Lifetime value and recency get one definition each. Every team builds on them.
CREATE FEATURES IN fm.home AS PERSIST reference_date := TIMESTAMP '2026-02-01', customer_ltv := customer_id.RELATED( SUM(order_price) FILTER (WHERE order_created_at < reference_date) GROUP BY (order_customer_id) ), last_order_at := customer_id.RELATED( MAX(order_created_at) FILTER (WHERE order_created_at < reference_date) GROUP BY (order_customer_id) ), recency := DATE_DIFF(reference_date, last_order_at, 'day'), -- …customer_ltv_cents: the same value as integer cents ;
2. Add a segment
Anyone can do this, or an agent. It takes the names of two metrics.
Write the rule
Three lines on top of the foundations: no tables, no joins. The thresholds are named, so an experiment can replace them.
CREATE FEATURES IN fm.home AS PERSIST promo_min_recency_days := 30, promo_min_ltv_cents := 100000, show_promocode_offline := recency > promo_min_recency_days AND customer_ltv_cents > promo_min_ltv_cents ;
Analyze in batch
FeatureQL owns the definition. The outer SQL aggregates as usual, so a BI tool can send it as is.
/* SQL */ SELECT show_promocode_offline, COUNT(1) AS num_customers FROM FEATUREQL( RETURN customer_id, show_promocode_offline := fm.home.show_promocode_offline FROM fm.home FOR customer_id := @BIND_KEYSET(dim_customers_keyset), order_id := @BIND_KEYSET(fct_orders_keyset) ) GROUP BY show_promocode_offline
3. Run in production
The rule is not rewritten. Only the path to its inputs changes.
Map the online sources
Same types as in the warehouse: recency and lifetime value, read from Redis.
CREATE FEATURES IN fm.home AS PERSIST redis_source := SOURCE_REDIS('redis://…' WITH (timeout='500ms')), redis_key := 'tutorial:featuremesh:' || CAST(KEY_TO_SCALAR(customer_id) AS VARCHAR), recency_online := CAST( EXTERNAL_REDIS(KEY redis_key FIELD 'days_since_order' FROM redis_source) AS BIGINT ), -- …customer_ltv_cents_online: same pattern ;
Reuse the rule
Swap the warehouse inputs for the Redis ones. The promo rule stays as written.
CREATE FEATURE fm.home.show_promocode_online AS VARIANT( fm.home.show_promocode_offline REPLACING fm.home.recency, fm.home.customer_ltv_cents WITH fm.home.recency_online, fm.home.customer_ltv_cents_online );
Compile and deploy
A prepared statement is an endpoint: customer id in, decision out. REFRESH deploys it.
CREATE FEATURE fm.home.show_promocode_online_ps AS PREPARED_STATEMENT( fm.home.show_promocode_online USING fm.home.customer_id ); REFRESH FEATURES FM.HOME.SHOW_PROMOCODE_ONLINE_PS;
Call it from anywhere
Your application depends on one thing: the id of the endpoint.
POST /api/evaluate
Content-Type: application/json
{
"id": "fm.home.show_promocode_online_ps",
"inputs": [
["100"]
]
}4. Experiment
Would 40 days of inactivity work better than 30? Test it without touching the rule or the application.
Propose a variant
One threshold changes. The rule is not copied, so it keeps following the original.
CREATE FEATURES IN fm.home AS PERSIST promo_min_recency_days_test := 40, show_promocode_offline_test := VARIANT( show_promocode_offline REPLACING promo_min_recency_days WITH promo_min_recency_days_test ) ;
Backtest on history
Both rules on every customer, before anyone sees the change.
RETURN customer_id, recency, control := show_promocode_offline, test := show_promocode_offline_test, FROM fm.home FOR customer_id := @BIND_KEYSET(dim_customers_keyset), order_id := @BIND_KEYSET(fct_orders_keyset), ;
Split the traffic
Half of the customers get the new threshold. The same customer always lands in the same arm.
CREATE FEATURES IN fm.home AS PERSIST promo_in_test := SAMPLE_BOOL( CAST(KEY_TO_SCALAR(customer_id) AS VARCHAR) || 'promo_40d', 0.5e0 ), promo_min_recency_days_assigned := IF( promo_in_test, promo_min_recency_days_test, promo_min_recency_days ) ;
Deploy behind the same endpoint
The endpoint keeps its id, so the application is not redeployed. Production switches when you REFRESH, not before.
CREATE FEATURE fm.home.show_promocode_online_experiment AS VARIANT( fm.home.show_promocode_offline REPLACING fm.home.recency, fm.home.customer_ltv_cents, fm.home.promo_min_recency_days WITH fm.home.recency_online, fm.home.customer_ltv_cents_online, fm.home.promo_min_recency_days_assigned ); CREATE OR REPLACE FEATURE fm.home.show_promocode_online_ps AS PREPARED_STATEMENT( fm.home.show_promocode_online_experiment USING fm.home.customer_id ); REFRESH FEATURES FM.HOME.SHOW_PROMOCODE_ONLINE_PS;
Measure after first exposure
Only exposed customers, and only what they ordered after their first exposure. Both conditions are features, defined once and reused by the next experiment.
RETURN arm := IF(promo_in_test, 'test', 'control'), exposed_customers := COUNT() FILTER (WHERE is_exposed) GROUP BY (arm), buyers := COUNT() FILTER (WHERE is_exposed AND orders_after_exposure > 0) GROUP BY (arm), revenue := SUM(revenue_after_exposure) FILTER (WHERE is_exposed) GROUP BY (arm), FROM fm.home FOR customer_id := @BIND_KEYSET(dim_customers_keyset), order_id := @BIND_KEYSET(fct_orders_keyset), event_id := @BIND_KEYSET(fct_events_keyset), ;
An agent can drive this loop. It may change what is decided, not how it is judged.
All of it without complexity
FeatureMesh is a light layer on top of what you already run.
Keep your infrastructure
Your warehouse, your tables, your BI tools. FeatureMesh maps the tables you have.
Adopt at your pace
FeatureQL and SQL mix in one query. Start with one rule and leave the rest as it is.
No data transfer
FeatureMesh holds definitions, not rows. The SQL it compiles runs on your side.
Leave when you want
Every definition compiles to standard SQL that you can read and keep.
Simpler data work.
More business impact.
One person, a pip install and an assistant are enough for the first step.
From there, FeatureQL makes your data work simpler and your business more agile.