FeatureQL for the Impatient

If you know SQL, this tutorial shows how FeatureQL turns business logic into named features that you can compose, persist, and reuse. FOR binds the data universe (every key the graph may read), not a SQL WHERE. Grain is declared, not a primary-key check.

You will build one customer segment, persist its source features, and use it in a report.

Build a customer segment, step by step

The goal: define IS_LONG_TENURED — a feature that takes a customer ID and returns whether the customer signed up before 2024.

Step 1: Declare the input

This query starts with an input: the customer ID for which you want to compute features.

FeatureQL
DEFINE
    customer_id := INPUT(BIGINT) -- Define the input
RETURN
    customer_id -- The feature you want to return
FOR
    customer_id := BIND_VALUES(ARRAY(100, 101)) -- Bind concrete values to the input
;
Result
CUSTOMER_ID BIGINT
100
101

No tables, no FROM clause. You declared an input (CUSTOMER_ID), selected it, and bound two values to evaluate. That's the core pattern: define features, bind inputs, get results.

Step 2: Connect to real data

Create some mock customer data. FeatureQL can embed raw SQL when setup or an outer query needs it:

FeatureQL
CREATE SCHEMA IF NOT exists impatient;
--
DROP TABLE IF EXISTS impatient.dim_customers;
--
CREATE TABLE impatient.dim_customers (
    customer_id BIGINT,
    preferred_city_id VARCHAR,
    name VARCHAR,
    time_create TIMESTAMP
);
--
INSERT INTO impatient.dim_customers
SELECT *
FROM (
    VALUES
        (CAST(100 AS BIGINT), 'BCN', 'John Doe', TIMESTAMP '2023-09-18 14:30:00'),
        (CAST(101 AS BIGINT), 'BCN', 'Jane Doe', TIMESTAMP '2024-12-08 09:45:00'),
        (CAST(102 AS BIGINT), 'BCN', 'Jack Doe', TIMESTAMP '2025-02-03 12:00:02')
) AS t(customer_id, preferred_city_id, name, time_create);
--
SELECT CAST(COUNT() AS INTEGER) AS Count FROM impatient.dim_customers;
Result
Count BIGINT
3

Now map table columns to features with EXTERNAL_COLUMNS(). The BIND TO clause tells FeatureQL which column is the lookup key:

FeatureQL
DEFINE
    customer_id := INPUT(BIGINT),
    data.customers := EXTERNAL_COLUMNS(
        customer_id BIGINT BIND TO customer_id,
        name VARCHAR,
        time_create TIMESTAMP
        FROM TABLE(impatient.dim_customers)
    ),
    customer_name := data.customers[name],
    customer_time_create := data.customers[time_create]
RETURN
    customer_id,
    customer_name,
    customer_time_create
FOR
    customer_id := BIND_VALUES(ARRAY(100, 101)) -- We need to choose the values we want to bind to the input
;
Result
CUSTOMER_ID BIGINTCUSTOMER_NAME VARCHARCUSTOMER_TIME_CREATE TIMESTAMP(3)
100John Doe2023-09-18T14:30:00
101Jane Doe2024-12-08T09:45:00

Notice: you didn't write a JOIN. You declared which columns you need and which key to look them up by. FeatureQL generates the join for you.

Step 3: Add business logic

One line of logic on top of the data mapping. A customer is long-tenured if they signed up before 2024:

FeatureQL
DEFINE
    customer_id := INPUT(BIGINT),
    data.customers := EXTERNAL_COLUMNS(
        customer_id BIGINT BIND TO customer_id,
        name VARCHAR,
        time_create TIMESTAMP
        FROM TABLE(impatient.dim_customers)
    ),
    customer_name := data.customers[name],
    customer_time_create := data.customers[time_create],
    is_long_tenured := customer_time_create < '2024-01-01'::TIMESTAMP
RETURN
    customer_id,
    customer_name,
    customer_time_create,
    is_long_tenured
FOR
    customer_id := BIND_COLUMNS(
        customer_id
        FROM TABLE(impatient.dim_customers)
    )
;
Result
CUSTOMER_ID BIGINTCUSTOMER_NAME VARCHARCUSTOMER_TIME_CREATE TIMESTAMP(3)IS_LONG_TENURED BOOLEAN
100John Doe2023-09-18T14:30:00true
101Jane Doe2024-12-08T09:45:00false
102Jack Doe2025-02-03T12:00:02false

IS_LONG_TENURED is a pure function: it depends on CUSTOMER_TIME_CREATE, which depends on DATA.CUSTOMERS, which depends on CUSTOMER_ID. FeatureQL resolves this dependency chain automatically — you never specify evaluation order.

Also notice: the FOR clause switched from BIND_VALUES() (an explicit list) to BIND_COLUMNS() (an entire table column). The features stay the same; only the binding strategy changes.

Persist and reuse

The features above were local — defined in the query. In practice, core features like CUSTOMER_ID and CUSTOMER_NAME would be persisted in the registry so the entire organization can reuse them:

FeatureQL
CREATE OR REPLACE FEATURES IN FM.TUTORIALS.IMPATIENT AS
PERSIST
    customer_id := INPUT(BIGINT),
    data.customers := EXTERNAL_COLUMNS(
        customer_id BIGINT BIND TO customer_id,
        name VARCHAR,
        time_create TIMESTAMP
        FROM TABLE(impatient.dim_customers)
    ),
    customer_name := data.customers[name],
    customer_time_create := data.customers[time_create]
;
Result
feature_name VARCHARstatus VARCHARmessage VARCHAR
FM.TUTORIALS.IMPATIENT.CUSTOMER_IDCREATEDFeature created as not exists
FM.TUTORIALS.IMPATIENT.DATA.CUSTOMERSCREATEDFeature created as not exists
FM.TUTORIALS.IMPATIENT.CUSTOMER_NAMECREATEDFeature created as not exists
FM.TUTORIALS.IMPATIENT.CUSTOMER_TIME_CREATECREATEDFeature created as not exists

Now anyone can build on top without redefining the data mapping. An analyst just writes their business logic:

FeatureQL
DEFINE
    is_long_tenured := customer_time_create < '2024-01-01'::TIMESTAMP
RETURN
    customer_id,
    is_long_tenured
FROM FM.TUTORIALS.IMPATIENT
FOR
    customer_id := BIND_COLUMNS(
        customer_id
        FROM TABLE(impatient.dim_customers)
    )
;
Result
FM.TUTORIALS.IMPATIENT.CUSTOMER_ID BIGINTIS_LONG_TENURED BOOLEAN
100true
101false
102false

The FROM FM.TUTORIALS.IMPATIENT clause loads the persisted features, DEFINE adds a local feature, and FOR binds the input. FeatureQL resolves the data mapping and dependencies.

From features to dashboards

Features produce fine-grained, per-entity results. For dashboards and reports, you typically need aggregations. FeatureQL handles this with GROUP BY as part of the feature definition:

FeatureQL
DEFINE
    is_long_tenured := data.customers[time_create] < '2024-01-01'::TIMESTAMP
RETURN
    is_long_tenured,
    num_customers := COUNT() GROUP BY (is_long_tenured), -- Note that group by (is) part of the feature definition
FROM FM.TUTORIALS.IMPATIENT
FOR
    customer_id := BIND_COLUMNS(
        customer_id
        FROM TABLE(impatient.dim_customers)
    )
;
Result
IS_LONG_TENURED BOOLEANNUM_CUSTOMERS BIGINT
false2
true1

For existing BI tools that generate their own SQL, you can embed FeatureQL inside SQL. The BI tool writes the outer GROUP BY and WHERE clauses; FeatureQL handles the feature computation:

FeatureQL
WITH
    CUSTOMERS_DATA AS FEATUREQL(
        DEFINE
            IS_LONG_TENURED := CUSTOMER_TIME_CREATE < '2024-01-01'::TIMESTAMP,
        RETURN
            CUSTOMER_ID,
            IS_LONG_TENURED,
        FROM FM.TUTORIALS.IMPATIENT
        FOR
            CUSTOMER_ID := BIND_COLUMNS(customer_id FROM TABLE(impatient.dim_customers))
        ;
    )
SELECT
    IS_LONG_TENURED,
    COUNT() AS NUM_CUSTOMERS
FROM CUSTOMERS_DATA
GROUP BY (IS_LONG_TENURED);
Result
IS_LONG_TENURED BOOLEANNUM_CUSTOMERS BIGINT
false2
true1

This means you can adopt FeatureQL incrementally — your existing SQL tools keep working, they just get better data.

Where to go next