Tutorial: Deploy a production-ready container¶
Premium feature
The Workload API is a premium feature. Contact your DataRobot representative or administrator for information on enabling the feature.
Deploy a containerized AI service with full governance: a locked artifact, importance, sharing, and monitoring. Unlike a draft Workload (see Hello, Workload), this one is long-lived and production-grade.
This tutorial deploys a FastAPI-based agent service (otkachnlp/fastapi-server-example, a publicly available third-party image, not published by DataRobot) that exposes:
- OpenAI-compatible
/chat/completions, connected to the DataRobot LLM Gateway—no separate LLM deployment required. - A LangGraph
/agentendpoint: a ReAct agent with ArXiv search. /healthz,/readyz, and/healthfor liveness, readiness, and detailed status.
Pick the tab that matches how you want to work: cURL calls the REST API directly, the CLI wraps the same calls in dr workload / dr artifact commands (falling back to curl where no subcommand exists), and Pulumi declares the locked artifact and Workload as code. Prefer a runnable notebook instead:
Locked-artifact Workloads at a glance¶
| Property | Value |
|---|---|
| Lifetime | Indefinite. Persists until explicitly stopped or deleted. |
| Artifact mutability | Immutable once locked. |
importance |
Optional; defaults to low. Set explicitly for production (critical, high, moderate, or low). |
| Workloads per artifact | Unlimited. One locked artifact can back many Workloads. |
| Replace | Supported. Replace locked with locked only. |
See Workload concepts for the full draft-vs-locked comparison.
Prerequisites¶
- A terminal with
curlandjq. - Your DataRobot API endpoint and token:
export DATAROBOT_ENDPOINT="https://app.datarobot.com/api/v2"
export DATAROBOT_API_TOKEN="<your-api-token>"
- The DataRobot CLI (
dr), authenticated, with Workload commands enabled (export DATAROBOT_CLI_FEATURE_WORKLOAD=true). curlandjqfor the calls that have no CLI equivalent—sharing and metadata updates.
- Pulumi CLI and the DataRobot Pulumi provider installed, with a stack configured (
datarobot:endpoint,datarobot:apikey). curlfor sharing, which Pulumi doesn't model—see When to use Pulumi.
Then set the values that vary per run:
export MODEL="azure/gpt-5-nano-2025-08-07"
export RECIPIENT_USER_ID="" # user, group, or organization ID to share with; leave blank to skip
Create the Workload¶
Artifacts are always created as draft, so with cURL and the CLI you create the Workload first with importance set, then lock the artifact in the next step. Locking flips its backing Workload into the locked (production) lifecycle: indefinite lifetime, immutable spec, eligible for locked-to-locked replace.
The artifact's spec defines container topology (image, port, entrypoint, environment variables, probes)—anything that travels with the artifact across deployments. Replica count, CPU/memory, and autoscaling are deployment-time concerns and live in runtime.containerGroups[]; entries are matched to the artifact by group and container name. The container authenticates against the DataRobot LLM Gateway using a per-Workload API token the platform resolves and injects automatically via {"source": "api-key"}—no value is set in the spec, so the token never appears in GET /artifacts/{id} responses, and DATAROBOT_ENDPOINT doesn't need to be set either since it's platform-managed. See Environment variable types for the full list.
readinessProbe.path gates running—the platform polls that path and only transitions the Workload to running once it returns 2xx. This tutorial points the probe at /healthz, which returns 2xx as soon as the FastAPI process is up. The container also exposes a deeper /readyz endpoint that exercises the LLM connection, but keep the readiness probe pointed at /healthz: gating running on an external dependency makes the Workload's status flap whenever that dependency has issues. Keep deep checks reachable as explicit endpoints for monitoring and runbooks instead of blocking startup on them.
curl -s -X POST "${DATAROBOT_ENDPOINT}/workloads" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg model "$MODEL" \
'{
"name": "agent-service",
"importance": "high",
"artifact": {
"name": "agent-service-artifact",
"type": "service",
"spec": {
"containerGroups": [{
"name": "default",
"containers": [{
"name": "agent",
"imageUri": "otkachnlp/fastapi-server-example:latest",
"port": 8080,
"primary": true,
"entrypoint": ["python", "server.py"],
"environmentVars": [
{"name": "MODEL", "value": $model},
{"source": "api-key"}
],
"readinessProbe": {"path": "/healthz", "port": 8080}
}]
}]
}
},
"runtime": {
"containerGroups": [{
"name": "default",
"replicaCount": 1,
"containers": [{
"name": "agent",
"resourceAllocation": {"cpu": 1, "memory": "512MB"}
}]
}]
}
}'
)" | tee /tmp/workload.json
export WORKLOAD_ID=$(jq -r '.id' /tmp/workload.json)
export ARTIFACT_ID=$(jq -r '.artifactId' /tmp/workload.json)
Save the spec to a file, substituting $MODEL:
cat > workload.yaml <<EOF
name: agent-service
importance: high
artifact:
name: agent-service-artifact
type: service
spec:
containerGroups:
- name: default
containers:
- name: agent
imageUri: otkachnlp/fastapi-server-example:latest
port: 8080
primary: true
entrypoint: ["python", "server.py"]
environmentVars:
- name: MODEL
value: $MODEL
- source: api-key
readinessProbe:
path: "/healthz"
port: 8080
runtime:
containerGroups:
- name: default
replicaCount: 1
containers:
- name: agent
resourceAllocation:
cpu: 1
memory: "512MB"
EOF
dr workload create --spec-file workload.yaml --output-format json | tee /tmp/workload.json
export WORKLOAD_ID=$(jq -r '.id' /tmp/workload.json)
export ARTIFACT_ID=$(jq -r '.artifactId' /tmp/workload.json)
A datarobot.Artifact resource always resolves to locked by the end of pulumi up (see When to use Pulumi), so there's no separate lock step in this flow—pulumi up creates the artifact already locked.
import pulumi
import pulumi_datarobot as datarobot
artifact = datarobot.Artifact(
"agent-service-artifact",
name="agent-service-artifact",
type="service",
spec={
"container_groups": [{
"name": "default",
"containers": [{
"name": "agent",
"image_uri": "otkachnlp/fastapi-server-example:latest",
"port": 8080,
"primary": True,
"entrypoint": ["python", "server.py"],
"environment_vars": [
{"name": "MODEL", "value": pulumi.Config().require("model")},
{"source": "api-key"},
],
"readiness_probe": {"path": "/healthz", "port": 8080},
}],
}],
},
)
workload = datarobot.Workload(
"agent-service",
name="agent-service",
importance="high",
artifact_id=artifact.artifact_id,
runtime={
"container_groups": [{
"name": "default",
"replica_count": 1,
"containers": [{
"name": "agent",
"resource_allocation": {"cpu": 1, "memory": "512MB"},
}],
}],
},
opts=pulumi.ResourceOptions(replace_on_changes=["artifact_id"]),
)
pulumi.export("artifactId", artifact.artifact_id)
pulumi.export("workloadId", workload.id)
pulumi.export("endpoint", workload.endpoint)
pulumi config set model "azure/gpt-5-nano-2025-08-07"
pulumi up
pulumi up blocks until the Workload is running, so once it returns, skip ahead to Invoke the service.
Lock the artifact¶
Transition the artifact from draft to locked. Because this Workload is the only one backing the draft artifact, the Workload's lifecycle transitions to locked alongside it. Locking is one-way: locked artifacts cannot return to draft.
curl -X PATCH "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"status": "locked"}'
PATCH /artifacts/{artifact_id} also accepts name, description, and spec for other updates while an artifact is still in draft.
dr artifact lock "$ARTIFACT_ID"
Nothing to do here—the artifact was created already locked in the previous step.
Wait for running¶
Poll the Workload's status until it reaches running. Expected happy-path progression: submitted → provisioning → launching → running. errored is a terminal state, not a transient blip to poll through—if you see it, stop and inspect (dr workload get "$WORKLOAD_ID" or the events endpoint) rather than continuing to wait.
curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" | jq -r '.status'
dr workload status "$WORKLOAD_ID"
Nothing to do here—pulumi up already blocked until running.
Invoke the service¶
Read the invoke URL from the Workload, then call the application routes against it.
ENDPOINT=$(curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" | jq -r '.endpoint')
curl -X POST "${ENDPOINT}/chat/completions" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"model": "'"${MODEL}"'", "messages": [{"role": "user", "content": "Hello!"}]}'
ENDPOINT=$(dr workload endpoint "$WORKLOAD_ID")
curl -X POST "${ENDPOINT}/chat/completions" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"model": "'"${MODEL}"'", "messages": [{"role": "user", "content": "Hello!"}]}'
ENDPOINT=$(pulumi stack output endpoint)
curl -X POST "${ENDPOINT}/chat/completions" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"model": "'"${MODEL}"'", "messages": [{"role": "user", "content": "Hello!"}]}'
The server also has an agent with a tool call to ArXiv. To test it, call the same way against ${ENDPOINT}/agent with a {"query": "..."} body.
Govern the Workload¶
Now that it's a production Workload, wire up importance and sharing.
# Raise importance to critical
curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"importance": "critical"}'
# Share with another user, group, or organization (skip if RECIPIENT_USER_ID is blank)
if [ -n "$RECIPIENT_USER_ID" ]; then
curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/sharedRoles" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"operation": "updateRoles",
"roles": [{"id": "'"${RECIPIENT_USER_ID}"'", "role": "USER", "shareRecipientType": "user"}]
}'
fi
Importance and sharing updates go through the REST API directly—dr workload covers create, read, lifecycle, and logs, not metadata or sharing updates:
# Raise importance to critical
curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"importance": "critical"}'
# Share with another user, group, or organization (skip if RECIPIENT_USER_ID is blank)
if [ -n "$RECIPIENT_USER_ID" ]; then
curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/sharedRoles" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"operation": "updateRoles",
"roles": [{"id": "'"${RECIPIENT_USER_ID}"'", "role": "USER", "shareRecipientType": "user"}]
}'
fi
importance is declarative—change it in code and reapply:
workload = datarobot.Workload(
"agent-service",
name="agent-service",
importance="critical", # was "high"
artifact_id=artifact.artifact_id,
runtime={ ... },
opts=pulumi.ResourceOptions(replace_on_changes=["artifact_id"]),
)
pulumi up
Sharing isn't modeled by the provider—manage /sharedRoles via REST or the Console (see When to use Pulumi):
if [ -n "$RECIPIENT_USER_ID" ]; then
curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/sharedRoles" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"operation": "updateRoles",
"roles": [{"id": "'"${RECIPIENT_USER_ID}"'", "role": "USER", "shareRecipientType": "user"}]
}'
fi
PATCH scope
PATCH /workloads/{id} accepts name, description, and importance. For runtime changes (replicas, resources) use PATCH /workloads/{id}/settings, which triggers a rolling replacement—see Runtime settings.
Observe¶
Locked Workloads expose the full monitoring surface with the organization's configured telemetry retention—see Monitoring concepts: Retention summary.
| Capability | Endpoint |
|---|---|
| Service health, latency, error rate | GET /workloads/{id} (computed fields on the Workload) |
| Lifecycle events (audit trail) | GET /workloads/{id}/events |
| Aggregate request statistics | GET /workloads/{id}/stats |
| Per-metric time series | GET /workloads/{id}/stats/{metric_name} |
| Per-replica status | GET /workloads/{id}/protons/{proton_id}/statusDetails |
These all read through the REST API directly, regardless of which interface you used to create the Workload—dr workload logs covers container stdout/stderr, a separate signal from the ones in this table.
curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/events" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}"
curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/stats" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}"
Summary¶
In this tutorial you:
- Created a Workload with
importanceset, backed by a draft artifact, then locked the artifact to move it onto the production (indefinite-lifetime) path. - Deployed a FastAPI agent service that calls the DataRobot LLM Gateway using an automatically injected, per-Workload API token—no credential ever appeared in the artifact spec.
- Invoked both the OpenAI-compatible
/chat/completionsroute and the LangGraph/agentroute. - Raised
importanceand shared the Workload with another user, group, or organization. - Read events and aggregate statistics from the monitoring surface.
Next steps¶
| Resource | Description |
|---|---|
| Workload concepts | The object model, lifecycle states, and the draft vs. locked decision in depth. |
| Tutorial: Replace the artifact behind a running Workload | Ship a new container version without dropping the endpoint. |
| Deploy an open-weight model from Hugging Face | Size a GPU compute bundle and serve a Hugging Face Hub model. |
| Instrument a Workload with OpenTelemetry (Python) | Add traces, metrics, and logs inside each request. |