# Define and deploy features

```json metadata
{
  "title": "Define and deploy features",
  "description": "Author a Python definitions file and apply it to a Redis Feature Form workspace.",
  "categories": null,
  "tableOfContents": {"sections":[{"children":[{"id":"typical-file-structure","title":"Typical file structure"},{"id":"the-file-should-reference","title":"The file should reference"},{"id":"the-file-should-not-do","title":"The file should not do"}],"id":"author-a-definitions-file","title":"Author a definitions file"},{"children":[{"id":"preview-with-plan","title":"Preview with --plan"},{"id":"standard-apply","title":"Standard apply"},{"id":"apply-modes","title":"Apply modes"}],"id":"apply-a-definitions-file","title":"Apply a definitions file"},{"id":"verify-the-apply","title":"Verify the apply"},{"id":"if-apply-fails","title":"If apply fails"}]}

,
  "codeExamples": []
}
```
A feature workflow starts with a Python [definitions file](https://redis.io/docs/latest/develop/ai/featureform/concepts#definitions-files-and-ff-apply). This file declares the [entities, datasets, transformations, features, labels, training sets, and feature views](https://redis.io/docs/latest/develop/ai/featureform/concepts#resource-types) you want in a [workspace](https://redis.io/docs/latest/develop/ai/featureform/concepts#workspaces). Run `ff apply` to submit that file as the workspace's [desired state](https://redis.io/docs/latest/develop/ai/featureform/concepts#the-resource-graph). Feature Form compares the file with the workspace's current resource graph and applies only the differences. If Feature Form accepts the change, it commits a new graph version. The model is declarative — the file describes the end state, not the steps to get there. Re-applying the same file leaves the workspace unchanged.

## Author a definitions file

Redis Feature Form treats a Python definitions file as the source of a desired resource graph. The example below declares one complete workflow end to end: a Postgres dataset, a SQL rollup transformation, a feature with a 7-day aggregation, and a Redis-backed feature view.

```python
import featureform as ff
from datetime import timedelta

postgres = ff.get_postgres("demo_postgres")      # Look up a provider already registered in the workspace

customer = ff.Entity(name="customer")            # Entity: the real-world object features describe

transactions = postgres.dataset(                 # Dataset: registers an existing table with the graph
    name="transactions_raw",
    table="transactions",
    timestamp_column="timestamp",
)

# Transformation: derives a new dataset from existing ones via SQL
@postgres.sql_transformation(name="customer_daily_rollups", inputs=[transactions])
def customer_daily_rollups() -> str:
    return """
        SELECT customer_id,
               date_trunc('day', timestamp) AS event_day,
               SUM(transaction_amount) AS total_amount
        FROM {{transactions_raw}}
        GROUP BY 1, 2
    """

# Feature: an aggregated value served to models at inference time
customer_total_amount_7d = (
    ff.Feature(name="customer_total_amount_7d")
    .from_dataset(customer_daily_rollups, entity="customer",
                  entity_column="customer_id", value="total_amount",
                  timestamp="event_day")
    .aggregate(function=ff.AggregateFunction.SUM, window=timedelta(days=7))
)

customer_risk_view = ff.FeatureView(             # Feature view: groups features behind a serving contract
    name="customer_risk_feature_view",
    entity="customer",
    features=[customer_total_amount_7d],
    inference_store="demo_redis",
)

# Export every resource so ff apply registers them as the desired graph
resources = [
    customer,
    transactions,
    customer_daily_rollups,
    customer_total_amount_7d,
    customer_risk_view,
]
```

### Typical file structure

A definitions file typically declares resources in this order:

1. **Import the module** with `import featureform as ff`, which exposes the resource builders and provider helpers.
2. **[Entities](https://redis.io/docs/latest/develop/ai/featureform/concepts#resource-types)** — identify the real-world objects features describe, such as `customer` or `order`. Other resources join on the entity's key column.
3. **[Datasets](https://redis.io/docs/latest/develop/ai/featureform/concepts#resource-types)** — point at an existing table, view, or file on an offline store. The data remains in its original location.
4. **[Transformations](https://redis.io/docs/latest/develop/ai/featureform/concepts#resource-types)** — produce new datasets from existing ones, expressed as SQL or as a Spark job.
5. **[Features](https://redis.io/docs/latest/develop/ai/featureform/concepts#resource-types) and [labels](https://redis.io/docs/latest/develop/ai/featureform/concepts#resource-types)** — entity-keyed values served at inference time (features) and used as the prediction target offline (labels).
6. **[Training sets](https://redis.io/docs/latest/develop/ai/featureform/concepts#resource-types) and [feature views](https://redis.io/docs/latest/develop/ai/featureform/concepts#feature-views-and-serving)** — join features with a label on the entity key (training set) and expose features for online serving (feature view).
7. **Export a `resources = [...]` list** that names every resource above. See [Definitions files and `ff apply`](https://redis.io/docs/latest/develop/ai/featureform/concepts#definitions-files-and-ff-apply) for how the loader uses it.


If your file doesn't export `resources = [...]`, `ff apply` falls back to its auto-registration registry. Prefer the explicit list during onboarding; it's easier to reason about and is what the [Quickstart](https://redis.io/docs/latest/develop/ai/featureform/quickstart) uses.


### The file should reference

- Registered [provider names](https://redis.io/docs/latest/develop/ai/featureform/register-providers) such as `demo_postgres` and `demo_redis`. Providers must already exist in the workspace before they're referenced.
- [Secret references](https://redis.io/docs/latest/develop/ai/featureform/concepts#secrets-and-secret-references) such as `env:PG_PASSWORD`, resolved at runtime by a [secret provider](https://redis.io/docs/latest/develop/ai/featureform/register-providers#configure-secret-providers) registered in the workspace.
- Stable resource names that make sense across re-apply cycles, since the graph compares the file to current state by name.

### The file should not do

- Don't replace provider registration.
- Don't assume providers exist before the workspace registers them.
- Don't mix infrastructure provisioning into the definitions entrypoint.

## Apply a definitions file

`ff apply` reads the file you pass with `--file` and submits its resources to a workspace. `--file` accepts a Python file, a package `__init__.py`, or a package directory containing one.

Get the workspace ID with `ff workspace list`. See [Manage workspaces](https://redis.io/docs/latest/develop/ai/featureform/manage-workspace) for the full workspace lifecycle.

### Preview with `--plan`

Preview the change without applying it:

```bash
ff apply \
  --workspace <workspace-id> \
  --file examples/featureform/docs/resources.py \
  --plan
```

Use this before large changes or whenever the file might be incomplete relative to the workspace's full desired state.

### Standard apply

Apply the file and wait for it to finish:

```bash
ff apply \
  --workspace <workspace-id> \
  --file examples/featureform/docs/resources.py \
  --wait \
  --wait-for finished
```

Without `--wait`, `ff apply` returns as soon as the server accepts the request and runs the job asynchronously. `--wait` blocks until the job reaches a target state: `--wait-for finished` waits for terminal success; `--wait-for running` returns as soon as the job is actively running.

If you skip `--wait`, the response includes a job ID. Check the job's status and per-task progress with:

```bash
ff scheduler job get <job-id>
```

### Apply modes

- **Default apply** replaces the workspace's current resource graph with the file.
- **`--merge`** applies a partial definition file without treating omitted resources as deletions.
- **`--update`** is an advanced scheduler-backed mode that re-runs supported resources' normal update or incremental path, even when the graph definition is unchanged.
- **`--full-rematerialize`** is an advanced scheduler-backed mode that forces full-refresh behavior on supported materialized resources.

Use only one of `--merge`, `--update`, or `--full-rematerialize` at a time. Support for `--update` and `--full-rematerialize` is resource-family dependent — run `--plan` first to inspect the planned job, and pair them with `--wait` to see the outcome.

## Verify the apply

After apply finishes, confirm the change with:

```bash
ff graph workspace stats --workspace <workspace-id>
ff graph feature list --workspace <workspace-id>
ff catalog list --workspace <workspace-id>
```

The graph commands show the resources Feature Form recognizes; `ff catalog` shows where each materialized resource physically landed. See [Query data](https://redis.io/docs/latest/develop/ai/featureform/query-data) for more inspection options.

## If apply fails

Common reasons:

- **Provider not registered.** A resource references a provider name the workspace doesn't know. Confirm with `ff provider list --workspace <workspace-id>` and register the missing provider per [Register providers](https://redis.io/docs/latest/develop/ai/featureform/register-providers).
- **Secret can't be resolved.** A provider config uses a reference such as `env:PG_PASSWORD`, but the Feature Form server's environment doesn't expose that variable. Check the secret provider with `ff secret-provider get env --workspace <workspace-id>`.
- **No resources to apply.** The entrypoint produced no resources. Make sure your file exports a `resources = [...]` list, or that auto-registration finds the resources you declared.
- **Validation error.** The CLI prints the specific resource and field that failed; fix the file and re-run with `--plan`.

For more detail, re-run with `--verbose` to enable debug logging to stderr. For most failures, though, the apply job itself surfaces clearer errors than the debug log — let `--wait` finish, or run `ff scheduler job get <job-id>`, before reaching for `--verbose`.

