Skip to content

Look up online features before prediction

When you need it

An online feature model accepts entity keys, lets Databricks fetch current feature values, and then runs the saved Skyulf preprocessing and predictor. An explicit saved policy rejects missing or stale feature values before preprocessing can impute them. This workflow uses the native Databricks Feature Engineering envelope and Online Feature Store.

The saved guard supports raw pandas and Polars model execution. Endpoint admission retains the existing certificate requirement: the fitted artifact must use pandas and supported independent-row preprocessing. The initial online request contract supports non-temporal scalar source inputs. It does not omit required source fields to make an incompatible model appear to accept only keys.

Follow the data through the system

flowchart LR
    D["Delta feature table"] --> P["TRIGGERED or CONTINUOUS publication"]
    P --> O["Existing online feature store"]
    K["Request keys and other required inputs"] --> L["Native feature lookup"]
    O --> L
    L --> F["Saved freshness and missing-value checks"]
    F --> M["Saved preprocessing and model"]
    M --> R["Prediction response"]

Training binds the offline feature source to the model. Publication copies the source's values into an online store. Endpoint setup verifies that the model's saved source has an accessible matching publication. At request time Databricks performs the lookup; Skyulf applies the saved validation and prediction logic.

Bundle choice What it adds What you must supply
include_online_feature_lookup=true An unscheduled endpoint setup job and training lookup placeholders Lookup fields, freshness policy, registered version and existing publication
include_online_publication=true A feature synchronization job Existing source table, source ID, destination and store name
online_publication_mode=TRIGGERED Manual or scheduled synchronization; hourly schedule starts paused Run a sync and observe completion before relying on the new values
online_publication_mode=CONTINUOUS Start/status/stop/restart controls for native synchronization Start once and inspect propagation and native pipeline health
include_daily_rollout=true A separate gradual traffic controller Two serving versions, saved comparison and initialized rollout state

You can use online serving without this project's publication job when another owner publishes the features. Conversely, publishing features does not create a model endpoint. Continuous publication updates features; it is not a continuous prediction job. Rollout is optional for either publication mode.

What to edit

Prepare identities and resources

Use the template's matching Core and Feature Engineering dependencies. On a new workspace, arrange these before running the examples:

Resource or identity Required role in this flow
Source and destination UC schemas Existing namespaces with access for the intended owners
Offline feature table One row per key, declared non-null primary key, CDF and a feature timestamp
Online store Explicitly provisioned and available; copy its exact name into the configuration
Training identity Read observations/features and log/register the fitted native feature model
Publication identity Read the source and manage its owned online publication
Endpoint identity Read the published features and the registered package
Endpoint caller Query permission and a request matching the saved input schema

The online store is backed by Lakebase. Provisioning chooses its name and capacity in the authenticated workspace; Bundle selection only references it. See Databricks Online Feature Stores for the current platform provisioning requirements. The examples below use the Skyulf adapter's stricter latest-value, non-temporal contract.

Generate, train and set up an online Bundle

From a Skyulf checkout, save these selections as online-init.json. This answer-file command takes missing answers from their defaults; it does not ask for missing table or target names. Replace the explicit example identities with your own before generating:

{
  "project_name": "customer_online",
  "catalog": "features",
  "schema": "customer",
  "source_table_name": "customer_observations",
  "target_column": "next_month_spend",
  "engine": "pandas",
  "training_layout": "single_model",
  "task": "regression",
  "input_columns": "total_spend, feature_updated_at",
  "record_key_columns": "customer_id",
  "include_online_feature_lookup": "true",
  "include_online_publication": "false",
  "include_daily_rollout": "false",
  "score_follow_champion": "false",
  "scoring_mode": "manual",
  "promotion_policy": "manual_approval"
}
databricks bundle init ./skyulf-core/templates/databricks --config-file online-init.json --output-dir ./generated
cd generated/customer_online

The three serving selections are independent and default to "false". Only detailed setup asks them; quick setup keeps "false", so use detailed setup or an answer file like the one above. include_online_feature_lookup adds an unscheduled online_feature_lookup_setup job and its notebook. It installs the native Feature Engineering dependency for training even when another project owns publication. Initialization creates files only. With include_online_publication: "true", detailed setup asks for online_publication_mode; an answer file can set it directly: TRIGGERED (default) keeps the paused hourly job; CONTINUOUS generates explicit start/status/stop/restart controls without a periodic schedule. The mode is independent of online serving and rollout.

Complete the generated placeholders under defaults: in config/training.yml, retaining the source, label, split and model settings for your project. Both mappings belong under defaults:; a top-level feature_lookup is rejected. For example:

defaults:
  # ... keep the generated source, keys, target and model settings ...
  feature_lookup:
    lookups:
      - table_name: "{catalog}.{input_schema}.customer_features"
        lookup_key: [customer_id]
        feature_names: [total_spend, feature_updated_at]
  online_features:
    required_features: [total_spend, feature_updated_at]
    freshness:
      - column: feature_updated_at
        max_age_seconds: 3600.0

All lookup features must be declared in input_columns; the required features must cover them exactly. Choose a maximum age for each lookup's freshness field. Empty generated placeholders fail validation. The policy is frozen into the training specification and carried through prepare, fit, log and registration. The original feature-table snapshot binding and fitted preprocessing remain enforced. This generated flow supports pandas single-model and competition training; multi-target policy transport and temporal feature lookups are rejected. No Spark scoring job or batch prediction table needs to run or exist.

Keep the generated Core release pinned in deployment/requirements.txt and complete your Bundle target and ordinary training settings. Change only this requirement's version to upgrade Core and redeploy. The optional online jobs inherit this pin through serving-requirements.txt, which also installs the pinned feature-engineering SDK. Validate saved-model runtime compatibility before upgrading production; see runtime packaging. Provision and publish the feature source as described below, or use an existing publication maintained externally. Then validate, deploy and train:

databricks bundle validate --strict -t test
databricks bundle deploy -t test
databricks bundle run train -t test

Continue with online endpoint setup using the concrete version from training. The setup verifies publication binding and first-sync evidence; it does not provision the store or publish features.

The command

Create a latest-value source and an online store

Use a Unity Catalog Delta feature table containing one current row per entity. Declare its primary key columns NOT NULL, enable Change Data Feed, and include a DOUBLE feature timestamp containing UTC Unix seconds. The timestamp records when that entity's values were produced; a recent table synchronization is not a substitute for a recent feature timestamp.

Provision an online store explicitly with the native Feature Engineering client and wait until it is AVAILABLE. Store provisioning is never a side effect of Skyulf prediction. Install deployment/serving-requirements.txt with uv pip install in a compatible Databricks environment. Packaging and serving must use the same Core release and source runtime.

Choose the store name and capacity when provisioning it in your selected workspace. Databricks manages the underlying Lakebase infrastructure. Bundle initialization does not create that infrastructure: online_store in config/serving.yml refers to this existing store. source_table identifies the Delta source, while online_table names the destination as catalog.schema.table; prepare that destination catalog and schema first.

from databricks.feature_engineering import FeatureEngineeringClient

features = FeatureEngineeringClient()
# Run provisioning once with a new name and an appropriate capacity.
features.create_online_store(name="customer-features", capacity="CU_1")

Prepare the destination catalog and schema before publication. Keep one online publication per source when exact store selection matters: native lookup can choose the oldest online copy if the same source has several publications. The model's lookup contract identifies its source features, not a chosen online table replica.

Inspect provisioning before publishing; creation is not a readiness signal:

store = features.get_online_store(name="customer-features")
print(store.name, store.state, store.capacity)

Continue when the platform reports AVAILABLE. For the following publisher example, use features.customer.customer_features with customer_id BIGINT NOT NULL, total_spend DOUBLE and feature_updated_at DOUBLE, a primary key on customer_id, and CDF enabled. Keep the training observation table separate: it contains entity keys and labels, while the lookup supplies the selected feature values. Source and online destination are different objects.

Publish and inspect a sync

from databricks.sdk import WorkspaceClient
from skyulf.integrations.databricks.data.admission import SingleWriterAdmission
from skyulf.integrations.databricks.feature_store import (
    OnlinePublicationSpec,
    online_publication_status,
    publish_online_features,
)

source = "features.customer.customer_features"
detail = spark.sql(f"DESCRIBE DETAIL {source}").first()
publication = OnlinePublicationSpec(
    source_table=source,
    online_table="features.online.customer_features",
    online_store="customer-features",
    source_table_id=detail["id"],
    publish_mode="TRIGGERED", # Default; choose CONTINUOUS to start a native stream.
)
receipt = publish_online_features(
    spark,
    publication,
    feature_client=features,
    admission=SingleWriterAdmission(),
)
print(receipt)

This validates the source table identity, CDF, key declarations and actual key rows before requesting a TRIGGERED sync. Source and publication writers must share the declared exclusive ownership; the adapter does not lock outside writers or change the source schema for you. It publishes all source columns. Temporal source tables remain unsupported. CONTINUOUS starts native feature synchronization and releases the writer admission after submission, allowing subsequent source writes. No caller process needs to keep running.

Inspect status later without submitting another sync:

workspace = WorkspaceClient()
status = online_publication_status(workspace, receipt)
print(status["status"], status.get("update_id"))

SUBMITTED and PENDING are not completion. SYNCED requires a native pipeline update that started after submission and completed; FAILED identifies a failed or canceled update. Keep the receipt and update ID for inspection. The recorded source version is the version checked during preflight, not a guarantee that native latest-value publication copied precisely that snapshot. Verify an actual endpoint lookup when testing propagation.

Operate continuous feature publication

Select online_publication_mode: "CONTINUOUS" together with include_online_publication: "true" in the Bundle initialization answers. Complete the generated publisher configuration with the actual resources:

online_publication:
  enabled: true
  exclusive_writer: true
  publish_mode: CONTINUOUS
  source_table: features.customer.customer_features
  source_table_id: "REPLACE_WITH_DESCRIBE_DETAIL_ID"
  online_table: features.online.customer_features
  online_store: customer-features

After configuring exclusive source/publication ownership and deploying the Bundle, run one explicit operation at a time:

databricks bundle validate --strict -t test
databricks bundle deploy -t test
databricks bundle run online_publication -t test --params publication_action=start
databricks bundle run online_publication -t test --params publication_action=status
databricks bundle run online_publication -t test --params publication_action=stop
databricks bundle run online_publication -t test --params publication_action=status
# Wait until pipeline_state is IDLE or FAILED, then explicitly restart:
databricks bundle run online_publication -t test --params publication_action=restart

RUNNING describes pipeline activity, including resource provisioning; it is not first-sync completion or entity freshness. Inspect pipeline_state, update_id, update_state, synchronization_state, first_sync_completed and last_sync_end_timestamp separately. A failed update or synchronization is reported by the inspected status command as FAILED even if the pipeline still reports running during recovery. STOP_REQUESTED is a submitted stop; check again for STOPPING or STOPPED. Restart requires native pipeline_state to be IDLE or FAILED. Overall status=FAILED can coexist with a RUNNING or STOPPING pipeline; stop active compute first and inspect its pipeline state. A restart receipt keeps its new update ID and reports PENDING until that update appears in native metadata. Earlier update/synchronization failures do not establish failure of that attempt. Once its acknowledged update appears, that update/pipeline activity decides the attempt status; synchronization failure labels remain separately visible because they can lag an intentional cancellation or restart. An intentionally stopped pipeline can therefore report native pipeline_state=IDLE alongside overall status=FAILED and update_state=CANCELED from the last operation. The model's saved missing/stale guard still validates every prediction request.

A repeated continuous start inspects and reuses the active pipeline without submitting another update. Restart uses the same pipeline after a confirmed stop or failure. Calls that time out or lose a response are not retried automatically; inspect native status before another action. Changing publish_mode on an existing publication is rejected. Stop and inspect the existing pipeline, then explicitly plan the new publication and model-routing migration with its resource owner; this adapter never drops or recreates an online table.

With TRIGGERED, the same start command requests one sync and the generated hourly schedule remains PAUSED until you deliberately enable it. status inspects the latest native update; SYNCED does not attest entity freshness. Continuous stop/restart controls require a continuous publication. If online serving uses an external publication, set its online_feature_lookup.publications[].publish_mode to that publication's actual mode before setup.

The SDK exposes the same operations without a Bundle:

from skyulf.integrations.databricks.feature_store import (
    publication_receipt,
    restart_online_publication,
    stop_online_publication,
)

# publication is an OnlinePublicationSpec with publish_mode="CONTINUOUS".
receipt = publication_receipt(workspace, publication)
print(online_publication_status(workspace, receipt))
print(stop_online_publication(workspace, receipt, admission=SingleWriterAdmission()))
# Inspect status in a later cell; restart only when pipeline_state is IDLE or FAILED.
receipt = restart_online_publication(
    spark, receipt, client=workspace, feature_client=features,
    admission=SingleWriterAdmission(),
)

If the source was dropped, replaced or became inaccessible, the guarded SDK control fails closed during binding checks, even for stop. The resource owner must inspect permissions, the saved receipt, online table source/store properties and native pipeline identity before stopping through the native API. For example, after that ownership review, verify the remaining publication metadata:

saved = receipt["spec"]
online = workspace.tables.get(saved["online_table"])
synced = workspace.database.get_synced_database_table(name=saved["online_table"])
pipeline_id = receipt["pipeline_id"]
assert online.pipeline_id == synced.data_synchronization_status.pipeline_id == pipeline_id
assert (online.properties or {}).get("source_table") == saved["source_table"]
assert synced.spec.source_table_full_name == saved["source_table"]
assert synced.effective_database_instance_name == saved["online_store"]
print(workspace.pipelines.get(pipeline_id=pipeline_id))
# Resource owner only: after confirming this saved pipeline is the intended resource.
workspace.pipelines.stop(pipeline_id=pipeline_id)

This recovery stops pipeline compute and does not alter the source table or publication. If metadata or ownership cannot be confirmed, resolve that with the resource owner before any mutation. Inspect native pipeline state after the stop request. Restart still requires restored, valid source identity/access and all normal source preflight checks.

Save the freshness policy with the model

The Bundle training path above saves this policy for you. The next snippet is the equivalent SDK packaging stage, continuing an existing fitted model and native training set; its named objects are prerequisites, not standalone inputs.

Build the native training set with explicit lookup fields and train from its load_df() result, retaining that same native training-set object for packaging. The freshness field must already be a fitted raw float64/double input and a selected lookup feature. Each lookup table needs its own freshness field.

from skyulf.integrations.databricks.feature_store import OnlineFeaturePolicy
from skyulf.integrations.mlflow.models.feature_model import log_feature_pipeline_model

online_policy = OnlineFeaturePolicy(
    required_features=("total_spend", "feature_updated_at"),
    freshness=(("feature_updated_at", 3600.0),),
)

# fitted_path, training_set, lookup_spec and lookup_binding come from training.
# run_id identifies the explicit MLflow run that owns this package.
model_uri = log_feature_pipeline_model(
    fitted_path,
    training_set=training_set,
    lookup_spec=lookup_spec,
    lookup_binding=lookup_binding,
    run_id=run_id,
    artifact_path="model",
    client=features,
    tracking_uri="databricks",
    online_policy=online_policy,
)

lookup_binding is the saved binding for that training set: its lookup spec and training_snapshot evidence identify the feature table IDs and Delta versions used during training. Retain that binding with the fitted artifact; do not replace it with a new snapshot when packaging an already fitted model.

Choose the maximum age for your domain. The example's one-hour limit is not a universal default. All fetched features are required and non-null. Every request uses one real UTC clock reading; nonfinite, future or over-age timestamps fail before fitted preprocessing. Callers cannot pass another clock to the saved model. The policy is checked across saved model metadata, configuration and serialized state when the package is loaded.

The timestamp is an actual fitted input: adding it only at packaging time is rejected. If it should not affect the estimator, remove it through a supported fitted preprocessing step and verify that step's serving certificate. Do not drop or transform inputs outside the saved preprocessing and expect Databricks lookup to repeat those transformations. log_feature_model_set also accepts online_policy; its raw saved guard covers the model-set request before its components run.

Next: create the endpoint

Continue with endpoint admission and requests. That page owns endpoint creation, readiness and the inspected request schema; the publication and saved freshness policy above are prerequisites.

What you should see

Schedule publication and test updates

Generate a Bundle project with include_online_publication: "true". Fill online_publication in config/serving.yml with the explicit source table ID, source and online table names, and store name. Establish exclusive source/sync writer ownership before setting exclusive_writer: true and enabled: true. The hourly job starts PAUSED; deploy, inspect a manual run and its pipeline status, then unpause it. Its result may be PENDING while the native pipeline continues. A later run should not be used to blindly retry an ambiguous native failure; inspect its pipeline and receipt first.

For acceptance, compare an endpoint prediction with the saved fitted artifact, change a source feature and its timestamp, publish, and verify that the same key-only request reflects the new value. Check that an absent entity and an expired timestamp produce the intended feature error. A successful local model load or native batch score_batch call does not establish online endpoint lookup.

Repeat the same request after a feature update

Continue the publication examples above and the endpoint SDK example with their workspace, features, publication, plan and receipt. Use an isolated demonstration source, with an existing customer_id=42, a one-hour saved freshness limit and a model whose output depends on total_spend. This sequence uses TRIGGERED publication. Supply any additional fields required by plan.input_schema; the illustrated model accepts only the entity key.

assert tuple(plan.input_columns) == ("customer_id",)
assert spark.table(publication.source_table).where("customer_id = 42").count() == 1
records = [{"customer_id": 42}]
before = query_named_records(
    workspace, plan, records, client_request_id="feature-demo-before",
)
print("Before:", before.as_dict())

spark.sql(f"""
UPDATE {publication.source_table}
SET total_spend = total_spend + 100.0,
    feature_updated_at = CAST(unix_timestamp() AS DOUBLE)
WHERE customer_id = 42
""")
receipt = publish_online_features(
    spark, publication, feature_client=features,
    admission=SingleWriterAdmission(),
)
print(receipt)

Do not query immediately and treat an unchanged response as a failure. In a later cell, inspect the submitted synchronization:

status = online_publication_status(workspace, receipt)
print(status)
assert status["status"] == "SYNCED", "Wait for this submitted sync; inspect FAILED separately"
after = query_named_records(
    workspace, plan, records, client_request_id="feature-demo-after",
)
print("After:", after.as_dict())

The request and model version are unchanged. Compare the response with a local prediction from the same fitted artifact using the new feature values. For a simple model prediction = 2 * total_spend, a change from 50 to 150 should change the prediction from 100 to 300. That arithmetic illustrates the acceptance check; it is not an expected value for an arbitrary trained model. A classifier label may remain unchanged even when its probabilities change.

For CONTINUOUS, update the source once, inspect the existing publication and observe propagation; do not resubmit publish_online_features on every write. An active pipeline alone does not prove that a particular row reached the store.

Common errors and fixes

Check missing and stale features

First use a key absent from the demonstration source. The following call should raise a native model-evaluation error, not return an imputed prediction:

assert spark.table(publication.source_table).where("customer_id = 999999").count() == 0
query_named_records(
    workspace, plan, [{"customer_id": 999999}],
    client_request_id="feature-demo-missing",
)

Then change the existing entity's freshness field to two hours ago:

spark.sql(f"""
UPDATE {publication.source_table}
SET feature_updated_at = CAST(unix_timestamp() - 7200 AS DOUBLE)
WHERE customer_id = 42
""")
receipt = publish_online_features(
    spark, publication, feature_client=features,
    admission=SingleWriterAdmission(),
)

Wait for this receipt's SYNCED status as above, then repeat records with client_request_id="feature-demo-stale". A model saved with the one-hour limit must reject it. Inspect the matched request's model log for the freshness error; an unrelated network/authentication error does not pass this check. Afterwards restore the entity's real feature values and a current production timestamp, synchronize again and verify a successful request. Do not leave a shared entity with intentionally stale test features.

The native Feature Engineering wrapper replaces every model error with a generic one, so the REST caller only sees Encountered an unexpected error while evaluating the model. ... Error ''. Read the reason from the served model's logs:

databricks serving-endpoints logs <endpoint-name> <served-model-name> --profile <profile>

The served model name is in databricks serving-endpoints get <endpoint-name> (config.served_entities[].name, for example customer_model-1). Look for one of:

Online required features have no value in 1 of 1 rows (first row 0): total_spend, feature_updated_at. The lookup key has no row in the online table, or the published value is null.
Online freshness feature 'feature_updated_at' is stale in 1 of 1 rows: oldest age 7200s exceeds 3600s (max_age_seconds). Refresh and republish the feature table.

Check that the traceback belongs to the rejected request; a generic HTTP error alone does not establish that the freshness policy ran.

The latest-value policy does not replace historical evaluation evidence. New feature-bound candidates save their enriched holdout in lifecycle/data/holdout.parquet; later champion promotion verifies that saved artifact and the original source identities. Advancing a feature table's version is allowed on this path; dropping/recreating a table or altering the saved holdout blocks it. A candidate without a saved holdout uses strict snapshot reconstruction, so a feature-source advance blocks it. Train a new candidate with the matching runtime rather than replacing historical values with today's online features.