Skip to content

Look up online features before prediction

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.

Generate, train and set up an online Bundle

From a Skyulf checkout, save these selections as online-init.json, completing the ordinary project, source and target answers when prompted:

{
  "project_name": "customer_online",
  "engine": "pandas",
  "training_layout": "single_model",
  "task": "regression",
  "input_columns": "total_spend, feature_updated_at",
  "record_key_columns": "customer_id",
  "include_online_serving": "true",
  "include_online_publication": "false",
  "include_daily_rollout": "false",
  "score_handoff": "disabled",
  "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". include_online_serving adds an unscheduled online_serving_setup job and its notebook. It installs the native Feature Engineering dependency for training even when another project owns publication. Initialization creates files only. Selecting include_online_publication: "true" asks for online_publication_mode: 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 config/training.yml defaults, retaining the source, label, split and model settings for your project. For example:

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 published Core release pinned in deployment/requirements.txt (skyulf-core[mlflow]==0.9.2) 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. Provision and publish the feature source as described below, or use an existing publication maintained externally. Then validate, deploy and train:

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

Take the concrete registered version from the training result and complete online_serving in config/serving.yml:

online_serving:
  enabled: true
  exclusive_writer: true
  tracking_uri: databricks
  registry_uri: databricks-uc
  endpoint:
    endpoint_name: customer-online-v3
    model_name: models.risk.customer_online
    model_version: "3"
    logging_catalog: ops
    logging_schema: serving
    logging_table_prefix: customer_online_v3
    logging_mode: telemetry
  publications:
    - source_table: features.customer.customer_features
      source_table_id: "REPLACE_WITH_DESCRIBE_DETAIL_ID"
      online_table: features.online.customer_features
      online_store: customer-features
      publish_mode: TRIGGERED # Match the existing publication; use CONTINUOUS for a stream.

Use actual resolved UC names here; the source table must be the model's saved lookup source with the same Delta identity. Configure exclusive endpoint writer permissions before setting exclusive_writer: true. The setup identity must be able to inspect the source, publication and store; the serving identity also needs access to the online features. Grant visibility of all publications for these sources and prevent concurrent publication changes during setup. UC enumeration covers the setup identity's visible inventory, not hidden resources. Setup rejects missing, mismatched or ambiguous accessible publications and requires evidence of a completed first sync before admitting creation. It reports a validation error while the first sync is incomplete; it does not poll or republish.

databricks bundle deploy -t test
databricks bundle run online_serving_setup -t test

The setup result reports SUBMITTED, model_uri, input_schema and a typed request_example derived from the actual native signature. Replace its example values with a real entity and all other required request values. Wait for native endpoint readiness, then send that request body to POST /serving-endpoints/customer-online-v3/invocations or use query_named_records below. A setup submission does not establish feature availability. Rerunning setup never overwrites an existing endpoint; inspect an ambiguous creation result before taking another action.

Save the returned request_example object as request.json, replace its sample values, and query after the endpoint reports readiness:

databricks serving-endpoints get customer-online-v3
databricks serving-endpoints query customer-online-v3 --json @request.json

Required source metadata can survive in the native signature, even when excluded from estimator inputs. Inspect input_schema; do not assume every model accepts keys alone. Direct REST callers can override fetched values. Freshness validation checks the received values and does not attest their source.

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.

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.current_values"
detail = spark.sql(f"DESCRIBE DETAIL {source}").first()
publication = OnlinePublicationSpec(
    source_table=source,
    online_table="features.online.customer_values",
    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.current_values
  source_table_id: "REPLACE_WITH_DESCRIBE_DETAIL_ID"
  online_table: features.online.customer_values
  online_store: customer-features

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

databricks bundle validate -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_serving.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

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.

Admit and query the registered package

Register the native outer package, then inspect a concrete version:

from skyulf.integrations.databricks.serving import (
    PinnedEndpointSpec,
    create_pinned_endpoint,
    prepare_online_endpoint,
    query_named_records,
    require_pinned_endpoint_ready,
)

spec = PinnedEndpointSpec(
    endpoint_name="customer-risk-online",
    model_name="models.risk.customer_risk_online",
    model_version="3",
    logging_catalog="ops",
    logging_schema="serving",
    logging_table_prefix="customer_risk_online",
)
plan = prepare_online_endpoint(
    spec, tracking_uri="databricks", registry_uri="databricks-uc"
)
print(plan.input_schema)
create_pinned_endpoint(workspace, plan)  # Once, using a new endpoint name.

After deployment is ready:

require_pinned_endpoint_ready(workspace, plan)
response = query_named_records(workspace, plan, [{"customer_id": 42}])
print(response)

The example request is valid only if the inspected input schema requires exactly customer_id. Native Feature Engineering can retain excluded training-source columns in its outer signature. Supply every field reported by plan.input_schema, or train from a narrower source. Temporal/date and binary source inputs are rejected by this bridge. The endpoint identity must have permission to read the required online features.

The helper's exact schema excludes feature overrides. Direct native REST calls can still provide feature values, including a freshness value. The saved guard checks the values it receives; it cannot prove that those values came from the online store after enrichment. Restrict direct endpoint access or use an application boundary if your application must prohibit caller overrides.

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.

The native Feature Engineering wrapper can return a generic model-evaluation error to the REST caller. Inspect the served model's logs for the underlying Online required feature or Online freshness feature exception. 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 weaken historical batch training_snapshot checks. Those checks still reject changed feature bindings. Reconstructing original heldout data for a later automatic champion promotion can therefore stop when the original feature source has advanced; current features are not silently substituted for the saved training evidence.