> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openlayer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Publish traditional ML predictions

> Monitor a traditional / tabular ML model by publishing its predictions to Openlayer — streamed as they happen or uploaded in batches.

For traditional ML systems (tabular classification, tabular regression, and other non-LLM models),
there is no LLM call to wrap, so the `@trace` decorator and provider wrappers used in
[Instrument your code](/monitoring/instrument) do not apply. Instead, you publish your model's
**predictions** directly to an inference pipeline.

You can do this in two ways — pick the one that matches how your system runs:

* **Stream** predictions as they happen (online serving, one request at a time).
* **Batch** upload predictions periodically (a scheduled scoring job, or a backfill).

Both paths use the same **typed configuration** that describes how your columns map to Openlayer's
semantics. The config class depends on your project's [task type](/development/openlayer-json).

<Info>
  **Prerequisites**:

  * A [project](/workspace-and-projects/creating-and-loading-projects) with monitoring mode enabled,
    and an [inference pipeline](/monitoring/overview) in it.

  * An [Openlayer API key](/workspace-and-projects/find-your-api-key).

  * The [Openlayer Python SDK](/api-reference/sdk/overview) installed.
</Info>

## Define the config for your task type

Import the config class matching your project's task type from
`openlayer.types.inference_pipelines.data_stream_params`:

<CodeGroup>
  ```python Tabular classification theme={null}
  from openlayer.types.inference_pipelines import data_stream_params

  config = data_stream_params.ConfigTabularClassificationData(
      categorical_feature_names=["Gender", "Geography"],
      class_names=["Retained", "Exited"],
      feature_names=["CreditScore", "Geography", "Gender", "Age", "Balance"],
      predictions_column_name="predictions",          # model's predicted class index
      prediction_scores_column_name="prediction_scores",  # per-class probability lists (optional)
      label_column_name="Exited",                     # ground truth, when available
      inference_id_column_name="inference_id",         # enables later ground-truth updates
  )
  ```

  ```python Tabular regression theme={null}
  from openlayer.types.inference_pipelines import data_stream_params

  config = data_stream_params.ConfigTabularRegressionData(
      feature_names=["age", "bmi", "bp", "s1", "s2", "s3"],
      predictions_column_name="predictions",   # model's predicted value
      target_column_name="target",             # ground truth, when available
      inference_id_column_name="inference_id",  # enables later ground-truth updates
  )
  ```
</CodeGroup>

The keys in your streamed rows (or the columns of your batch DataFrame) must exactly match the
`*_column_name` values declared in the config.

<Warning>
  For tabular classification, send `class_names`, `feature_names`, **and**
  `categorical_feature_names` together — the server requires all three even
  though the SDK type hints mark only `class_names` as required. Pass
  `categorical_feature_names=[]` when your model has no categorical features;
  omitting it returns a `400 ... not valid under any of the given schemas`.
</Warning>

## Option A — Stream predictions as they happen

Use `client.inference_pipelines.data.stream(...)` to publish one or a few rows per call, right after
your model scores a request:

```python Python theme={null}
from openlayer import Openlayer

client = Openlayer(api_key="YOUR_OPENLAYER_API_KEY_HERE")

row = {
    "CreditScore": 600,
    "Geography": "France",
    "Gender": "Male",
    "Age": 40,
    "Balance": 100000,
    "predictions": 1,
    "prediction_scores": [0.15, 0.85],
    "inference_id": "a1b2c3",
}

client.inference_pipelines.data.stream(
    inference_pipeline_id="YOUR_INFERENCE_PIPELINE_ID_HERE",
    rows=[row],
    config=config,
)
```

## Option B — Upload predictions in batches

For a scheduled scoring job or a backfill, score a whole DataFrame and upload it at once with
`upload_batch_inferences`:

```python Python theme={null}
from openlayer import Openlayer
from openlayer.lib import data

data.upload_batch_inferences(
    client=Openlayer(api_key="YOUR_OPENLAYER_API_KEY_HERE"),
    inference_pipeline_id="YOUR_INFERENCE_PIPELINE_ID_HERE",
    dataset_df=df,   # columns must match the config's *_column_name values
    config=config,
)
```

## Add ground truth later

When the true labels/targets arrive after you have already published predictions, patch them in by
correlating on the `inference_id` you set above:

```python Python theme={null}
from openlayer.lib import data

data.update_batch_inferences(
    client=Openlayer(api_key="YOUR_OPENLAYER_API_KEY_HERE"),
    inference_pipeline_id="YOUR_INFERENCE_PIPELINE_ID_HERE",
    dataset_df=labels_df,  # must include the inference_id column + the label/target column
    config=config,         # config.inference_id_column_name must be set
)
```

See [Update production data](/monitoring/updating-data) for more on delayed ground truth, and
[Upload a reference dataset](/monitoring/uploading-reference-dataset) to add a baseline for data-drift
tests.
