External deployments
An external deployment enables H2O MLOps to monitor models deployed outside the H2O MLOps platform. Instead of serving a model, the deployment runs a standalone mlops-proxy that captures scoring data and forwards it to the existing H2O MLOps monitoring pipeline. Use it to track a model you have already deployed elsewhere, or to monitor a model that runs in an environment H2O MLOps can't reach.
An external deployment runs in one of two modes:
- Proxy URL: The proxy forwards each scoring request to a reachable upstream model, returns the upstream response to the caller, and records the request and response pair for monitoring.
- Recorder: The proxy records pre-paired request and response records that you post to it. It makes no upstream call.
An external deployment has no model, experiment, or composition. You define its monitoring schema directly with MonitoringOptions columns, and an immutable ApiTemplate tells the proxy how to extract those columns from the request and response bodies.
You create and manage external deployments with the H2O MLOps Python client.
Prerequisites
Before you begin, complete the following steps:
- Import the necessary Python packages. For instructions, see Step 1: Import the required packages.
- Connect to H2O MLOps. For instructions, see Connect to H2O MLOps.
- Create a workspace. For instructions, see Create a workspace.
The examples on this page assume that you have assigned the workspace to the variable workspace. Because an external deployment has no model, experiment, or composition, you don't need to create any of those first.
Import the classes used throughout this page:
from h2o_mlops.options import (ApiTemplate,Column,ExternalDeploymentOptions,MonitoringOptions,RowTemplate,SecurityOptions,UpstreamAuth,)from h2o_mlops.types import (ColumnLogicalType,DeploymentModeType,ExternalDeploymentMode,SecurityType,)
Define the monitoring schema
Because an external deployment has no experiment, you define the monitored columns directly with MonitoringOptions, and you map each column to a location in the request or response body with an ApiTemplate. Both modes share this setup.
Define the monitored columns
Use MonitoringOptions to list the input and output columns to monitor:
monitoring_options = MonitoringOptions(enabled=True,input_columns=[Column(name="LIMIT_BAL", logical_type=ColumnLogicalType.NUMERICAL),Column(name="AGE", logical_type=ColumnLogicalType.NUMERICAL),Column(name="SEX", logical_type=ColumnLogicalType.CATEGORICAL),],output_columns=[Column(name="PREDICTION",logical_type=ColumnLogicalType.NUMERICAL,),],)
Map the columns with an ApiTemplate
The ApiTemplate tells the proxy how to find each monitored column in the request and response bodies. It has a request template for input columns and a response template for output columns. Each template is a RowTemplate with the following fields:
rows_path: A JMESPath expression that yields the array of row objects in the body. For example,"instances"selects theinstancesarray, and"@"selects the whole body.column_paths: A map of each monitored column name to a JMESPath expression, relative to each row object, that locates the column value.
api_template = ApiTemplate(request=RowTemplate(rows_path="instances",column_paths={"LIMIT_BAL": "LIMIT_BAL", "AGE": "AGE", "SEX": "SEX"},),response=RowTemplate(rows_path="predictions",column_paths={"PREDICTION": "score"},),)
- JMESPath is case-sensitive. The keys in
column_pathsmust match the column names inmonitoring_options, and the paths must match the keys in the request and response bodies exactly. - The
api_templateis immutable after you create the deployment. You can't change it withupdate(). - You can omit the
requestorresponsetemplate when the deployment monitors no columns of that kind.
Provide baseline data
An external deployment has no experiment or training dataset, so H2O MLOps can't compute monitoring baselines automatically. Provide filled baselines through the baseline_data field of MonitoringOptions, using BaselineData with a NumericalAggregate, CategoricalAggregate, or TextAggregate for each column. The automatic prepare_monitoring_options_from_data_frame helper doesn't apply to external deployments.
For the structure of BaselineData and its aggregates, see Manual configuration.
Proxy URL mode
Use proxy URL mode when you have already deployed a model outside H2O MLOps and want to monitor it. You point the proxy at your existing endpoint with upstream_url. The proxy forwards each scoring request to that endpoint, adds the configured upstream authentication, returns the upstream response to the caller, and records the request and response pair for monitoring.
Authenticate to the upstream
UpstreamAuth configures how the proxy authenticates to your upstream model. It requires exactly one of the following fields.
Bearer token
Store the token in H2O Secure Store and reference it by key:
secret = workspace.secrets.create(name="upstream-token", value=b"<token>")upstream_auth = UpstreamAuth(bearer_secret_ref=secret.key)
mTLS
Reference a kubernetes.io/tls Secret that contains tls.crt and tls.key in the proxy namespace:
upstream_auth = UpstreamAuth(mtls_kubernetes_secret="my-upstream-client-cert")
Create the deployment
Configure the deployment with ExternalDeploymentOptions, then create it with mode=DeploymentModeType.EXTERNAL. Proxy URL mode requires upstream_url.
proxy_options = ExternalDeploymentOptions(mode=ExternalDeploymentMode.PROXY_URL,api_template=api_template,upstream_url="https://example-deployment.external.com/score",upstream_auth=upstream_auth,readyz_url="https://example-deployment.external.com/readyz",)proxy_deployment = workspace.deployments.create(name="external-proxy-demo",composition_options=None,security_options=SecurityOptions(security_type=SecurityType.DISABLED),mode=DeploymentModeType.EXTERNAL,external_options=proxy_options,monitoring_options=monitoring_options,)
For an external deployment, composition_options must be None. The kubernetes_options, vpa_options, pdb_options, and environment_variables arguments don't apply. Set the proxy's scaling and placement through external_options instead. For more information, see Scale and place the proxy.
Score against the deployment
Post a scoring request to the scoring endpoint. The proxy forwards it to upstream_url, returns the upstream response, and records the pair.
import requestsscore_url = proxy_deployment.scorer.scoring_endpointpayload = {"instances": [{"LIMIT_BAL": 30000, "AGE": 23, "SEX": 2}]}response = requests.post(score_url, json=payload)print(response.status_code, response.json()) # 200 -> {"predictions": [{"score": 0.42}]}
The request and response bodies use the upstream model's own shape, not the standard H2O MLOps {"fields": [...], "rows": [...]} tabular format. The body keys must match the api_template column_paths exactly, because JMESPath is case-sensitive. If the deployment uses token security, pass the credential as usual, such as in an Authorization header.
Recorder mode
Use recorder mode when the model runs in an environment that can't reach H2O MLOps, such as an air-gapped factory floor. In this mode, the proxy makes no upstream call. Instead, you post pre-paired request and response records to the record endpoint, and the proxy captures them for monitoring.
Create the deployment
Recorder mode uses only mode and api_template:
recorder_options = ExternalDeploymentOptions(mode=ExternalDeploymentMode.RECORDER,api_template=api_template,)recorder_deployment = workspace.deployments.create(name="external-recorder-demo",composition_options=None,security_options=SecurityOptions(security_type=SecurityType.DISABLED),mode=DeploymentModeType.EXTERNAL,external_options=recorder_options,monitoring_options=monitoring_options,)
Recorder mode must not set upstream_url, upstream_auth, or readyz_url. Setting any of them raises a ValueError.
Record a request and response pair
Post a record that pairs a request body with its response body. The recorder returns 202 Accepted.
import requestsrecord_url = recorder_deployment.scorer.scoring_endpointrecord = {"request": {"instances": [{"LIMIT_BAL": 30000, "AGE": 23, "SEX": 2}]},"response": {"predictions": [{"score": 0.42}]},"request-timestamp": "2026-06-22T09:39:21.091631769Z", # optional"response-timestamp": "2026-06-22T09:39:21.091631769Z", # optional}response = requests.post(record_url, json=record)print(response.status_code) # 202
A record can optionally include request-timestamp and response-timestamp fields (RFC 3339 or ISO 8601, in UTC with a trailing Z) that capture when the request and response actually occurred. This is useful because the recorder can receive a record long after the producer generated the data. If you omit the timestamps, the recorder timestamps the record when it receives it. The body keys must match the api_template column_paths exactly, so that the captured columns line up with monitoring_options.
Record offline traffic on a schedule
Where the model runs with no access to H2O MLOps, separate capture from shipping:
- On the air-gapped machine, append each captured request and response pair to a file as JSON Lines, with one record per line.
- On a host that can reach H2O MLOps, run a scheduled job, such as a cron job, that reads the file and posts each record to the record endpoint.
The offline producer writes records to a file:
import jsonrecord = {"request": {"instances": [{"LIMIT_BAL": 30000, "AGE": 23, "SEX": 2}]},"response": {"predictions": [{"score": 0.42}]},# optional -- when the original request and response actually happened:"request-timestamp": "2026-06-22T09:39:21.091631769Z","response-timestamp": "2026-06-22T09:39:21.091631769Z",}with open("records.jsonl", "a") as f:f.write(json.dumps(record) + "\n")
The scheduled shipper reads the file and records each pair:
import jsonimport requestsrecord_url = "https://mlops.example.com/<deployment-id>/model/record"with open("records.jsonl") as f:for line in f:line = line.strip()if not line:continuerequests.post(record_url, json=json.loads(line)) # returns 202 Accepted
Get the scoring or record URL and the schema
Both modes expose a scorer. Use it to discover the endpoint URLs and the monitored schema:
scorer = deployment.scorerscorer.api_base_url # https://mlops.example.com/<deployment-id>scorer.scoring_endpoint # proxy_url -> .../model/score; recorder -> .../model/recordscorer.schema_endpoint # .../model/schemascorer.schema() # {"input_fields": [...], "output_fields": [...]}scorer.sample_request() # an example record, including request, response, and timestamps
For a recorder, sample_request() returns the full record shape, including the optional timestamp fields:
Input:
recorder_deployment.scorer.sample_request()
Output:
{"request": {"instances": [{"AGE": 1.0, "LIMIT_BAL": 1.0, "SEX": "SEX"}]},"response": {"predictions": [{"score": 1.0}]},"request-timestamp": "2026-06-22T09:39:21.091631769Z","response-timestamp": "2026-06-22T09:39:21.091631769Z",}
For more information about the scorer and its properties, see Deployment scorer.
Scale and place the proxy
ExternalDeploymentOptions exposes Kubernetes scaling and placement shortcuts for the proxy pod:
replicas: The number of proxy pods. Set0to scale to zero. When unset, it defaults to1.affinity: A Kubernetes affinity shortcut.toleration: A Kubernetes toleration shortcut.
proxy_options = ExternalDeploymentOptions(mode=ExternalDeploymentMode.PROXY_URL,api_template=api_template,upstream_url="https://example-deployment.external.com/score",upstream_auth=upstream_auth,readyz_url="https://example-deployment.external.com/readyz",replicas=2,affinity="<affinity-shortcut>",toleration="<toleration-shortcut>",)
To check the allowed affinity and toleration values, run mlops.configs.allowed_k8s_affinities and mlops.configs.allowed_k8s_tolerations. For more information, see Node affinity and toleration.
Update an external deployment
You can update the replicas, affinity, and toleration values, and the proxy URL fields upstream_url, upstream_auth, and readyz_url. The api_template is immutable, so pass the same one you used at creation.
proxy_deployment.update(external_options=ExternalDeploymentOptions(mode=ExternalDeploymentMode.PROXY_URL,api_template=api_template, # unchanged; immutableupstream_url="https://example-deployment.external.com/score",upstream_auth=upstream_auth,readyz_url="https://example-deployment.external.com/readyz",replicas=3,))
Security, audit, and Kafka
External deployments support the same platform features as managed deployments:
- Security:
SecurityOptionsworks the same way, including token authentication.SecurityType.DISABLEDis a valid single entry. For more information, see Secure deployments. - Audit: Audit logging applies the same way.
- Kafka: Raw scoring data export to Kafka works for external deployments. For more information, see Raw data export to Kafka.
- Submit and view feedback for this page
- Send feedback about H2O MLOps to cloud-feedback@h2o.ai