Defining Features
Defining features in the UI
On the store's page:
Entities tab, New Entity:
- Name (for example
driver) and a Description; - Join keys: the key column, or several separated by commas for a composite key (for example
driver_id); - Value type:
int64,string,float64,boolortimestamp.
Feature Views tab, New Feature View (a batch view over Postgres):
- Name: lowercase letters, digits and underscores, starting with a letter;
- Entities: the entities it describes (Create an entity if there is none yet);
- TTL (optional): how long a feature value stays valid, a number and a unit (
s,m,h,dorw, for example2d). Older values are served as missing; blank means unbounded; - Schema (features): each feature's name and type:
int64,float64,bool,string,timestamp,float32_vector(with its dimension, such as 768),array(of a scalar type) orstruct(of named scalar fields); - Source (Postgres): the add-on or data source, the Table (
tableorschema.table), the Event-time column (when each value was observed; point-in-time joins run on it), an optional Ingestion-time column (when each row was written; with it a row that arrives late is still taken), and for each entity key its source column (Entity key → source column).
Feature Services tab, New Feature Service: a Name and its Features, each a Feature view, its View version and the Feature.
Streaming and on-demand views carry transform code, so they are defined with the SDK (see On-demand views).
Defining features with the SDK
You define entities, feature views, and feature services with the Python SDK or the REST API using a single apply call, which registers them in dependency order (entities, then views, then services). The result reports each view's change: created for a new view, identical, additive or breaking. Applying the same definition again is safe: an unchanged view is a no-op, an additive change updates in place, and a breaking change creates a new version.
from strongly import Strongly
client = Strongly()
store_id = client.feature_store.list_stores()[0]["_id"]
client.feature_store.apply(
store_id,
entities=[
{"name": "driver", "joinKeys": ["driver_id"], "valueType": "int64"},
],
views=[
{
"name": "driver_stats",
"entities": ["driver"],
"mode": "batch",
"ttl": "2d",
"schema": [
{"name": "conv_rate", "dtype": "float64"},
{"name": "acc_rate", "dtype": "float64"},
],
"source": {
"serviceKind": "addon",
"serviceId": "<your-postgres-addon-id>",
"type": "postgres",
"table": "driver_stats_raw",
"timestampField": "event_timestamp",
"entityKeyColumns": {"driver_id": "driver_id"},
},
},
],
services=[
{
"name": "driver_model_v1",
"features": [
{"view": "driver_stats", "feature": "conv_rate"},
{"view": "driver_stats", "feature": "acc_rate"},
],
},
],
)
Feature views and services appear on the store details page under their tabs, each showing versions, feature counts, and freshness.
Streaming views with windowed aggregations
A streaming view aggregates events as they arrive: each aggregation is a column, a function (count, sum, min, max, avg, var, std, first, last, or the approximate approx_distinct, approx_quantile and top_k; the last two take a param, the quantile or the number of top values) and the windows to serve it over, each producing a feature named <column>_<function>_<window> (for example fare_sum_1h). Events are grouped into tiles of tileSize; allowedLateness is how late an event may arrive and still count online.
client.feature_store.apply(store_id, views=[{
"name": "driver_trips",
"entities": ["driver"],
"mode": "stream",
"schema": [{"name": "fare", "dtype": "float64"}],
"source": {"type": "rabbitmq", "exchange": "fs.driver_trips", "timestampField": "event_timestamp",
"entityKeyColumns": {"driver_id": "driver_id"}},
"aggregations": [{"column": "fare", "function": "sum", "windows": ["1h", "1d"]}],
"tileSize": "5m",
}])
client.feature_store.push(store_id, "driver_trips", events=[
{"driver_id": 1001, "event_timestamp": "2026-10-08T07:00:00Z", "fare": 12.5},
])
Online, a window is served as of now: an event older than a window no longer counts in it, also for an entity that sends no new events, so online values match training data computed for the same moment. Events pushed right after the view is applied are kept until the stream worker starts on it.
On-demand views
An on-demand view computes its features when they are requested, from the features of other views and from values the request supplies, with your Python. Its transformCode defines transform(df): it receives a pandas DataFrame with one row per entity, holding every feature of its source views (None where the entity has no value) and its request fields, and returns a DataFrame with the same number of rows and a column for each of the view's features. Import what the code uses.
client.feature_store.apply(store_id, views=[{
"name": "conv_x_dist",
"entities": ["driver"],
"mode": "on_demand",
"schema": [{"name": "conv_x_dist", "dtype": "float64"}],
"onDemandSources": ["driver_stats"],
"requestDataSources": [{"name": "trip", "fields": [{"name": "trip_distance", "dtype": "float64"}]}],
"transformCode": (
"import pandas as pd\n"
"def transform(df):\n"
" return pd.DataFrame({'conv_x_dist': df['conv_rate'] * df['trip_distance']})\n"
),
}])
Applying it pins the newest active version of each source view, so a model serving it keeps the inputs it was trained on, and deploys its transform: a workflow in your organization, owned by you, in which your code runs in a sandbox, kept running to serve requests. The view's page shows the transform's state (provisioning, ready, or failed with the reason; applying the view again retries a failed one) and links to its runs. Any change to an on-demand view creates a new version with its own transform, and deleting a version removes its transform. A view version that an on-demand view reads cannot be deleted while it does.
Each request supplies the request fields with the entity: online, on each entity row ({"driver_id": 1001, "trip_distance": 3.0}); in training data, as columns of the entity rows.