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.

The 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.

Illustrative 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.

1

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),
;
2

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
;
3

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.

1

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
;
2

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.

1

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
;
2

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
);
3

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;
4

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.

1

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
    )
;
2

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),
;
3

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
    )
;
4

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;
5

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.