Skip to content

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 /agent endpoint: a ReAct agent with ArXiv search.
  • /healthz, /readyz, and /health for 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:

 View as a runnable notebook

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 curl and jq.
  • 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).
  • curl and jq for the calls that have no CLI equivalent—sharing and metadata updates.

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.

__main__.py
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: submittedprovisioninglaunchingrunning. 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 importance set, 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/completions route and the LangGraph /agent route.
  • Raised importance and 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.