Tutorial: Build a Workload from source¶
Premium feature
The Workload API is a premium feature. Contact your DataRobot representative or administrator for information on enabling the feature.
This tutorial takes a small FastAPI project from a local directory to a running Workload endpoint. You do not write a Dockerfile, run docker build, or push to a registry—the platform builds the image from your source and deploys it.
You'll create a draft artifact that builds from source, upload your code, run a build, deploy the result, invoke it, make a change and rebuild, and finally lock the artifact for production.
Pick the tab that matches how you want to work: cURL calls the REST API directly, and the CLI wraps the same calls in dr commands.
Prerequisites¶
Before starting, confirm the following are in place; the build step later in this tutorial fails without them.
- A Python project with
pyproject.tomlat its root. Auv.lockbeside it is required for the build; if you use the CLI,dr artifact code syncgenerates one for you when it is missing. Otherwise runuv lockbefore uploading. - The ID and version ID of an execution environment to use as the base image.
- A
service,agent, ormcpartifact—code-to-workload builds don't apply to other artifact types. This tutorial omitstypefrom the artifact spec, which defaults toservice.
A terminal with curl and jq, and your endpoint and token exported:
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
dr auth login
The project used here is a FastAPI app that serves GET /version:
my-api/
├── app.py # FastAPI application
├── pyproject.toml # Dependencies
├── uv.lock # Locked dependencies
└── views/
├── __init__.py
└── version.py # GET /version endpoint
Create the sample project¶
Create the project this tutorial builds and deploys. Every command below runs from the directory that will hold my-api/—the same directory the rest of this tutorial runs from—so none of it requires changing directories.
mkdir -p my-api/views
[project]
name = "my-api"
version = "0.1.0"
description = "FastAPI app built from source"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.115",
"uvicorn>=0.30",
]
from fastapi import FastAPI
from views.version import router as version_router
app = FastAPI(title="my-api")
app.include_router(version_router)
Add an empty my-api/views/__init__.py, then the route:
from fastapi import APIRouter
router = APIRouter()
VERSION = "0.1.0"
@router.get("/version")
def get_version() -> dict:
return {"version": VERSION}
Generate the lockfile the build requires:
uv lock --project my-api
Create the artifact¶
Create a draft artifact whose container builds from source. There is no imageUri and no codeRef yet: the image does not exist, and the code has not been uploaded.
Save the spec:
{
"name": "my-api-artifact",
"description": "FastAPI app built from source",
"spec": {
"containerGroups": [{
"name": "default",
"containers": [{
"name": "primary",
"primary": true,
"port": 8080,
"imageBuildConfig": {
"dockerfile": {
"source": "generated",
"executionEnvironmentId": "<execution-environment-id>",
"executionEnvironmentVersionId": "<execution-environment-version-id>",
"entrypoint": ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
}
},
"readinessProbe": {
"path": "/version",
"port": 8080,
"initialDelaySeconds": 10
}
}]
}]
}
}
curl -s -X POST "${DATAROBOT_ENDPOINT}/artifacts" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d @artifact.json | tee /tmp/artifact.json
export ARTIFACT_ID=$(jq -r '.id' /tmp/artifact.json)
dr artifact create --spec-file artifact.json --output-format json | tee /tmp/artifact.json
export ARTIFACT_ID=$(jq -r '.id' /tmp/artifact.json)
Bringing your own Dockerfile instead
To build from a Dockerfile in your project rather than a generated one, replace the dockerfile block with {"source": "provided", "path": "./Dockerfile"}. Everything else in this tutorial is unchanged. See Choose how the Dockerfile is obtained.
Upload the code¶
Upload the code through the REST API directly, or with the CLI, which also updates the artifact's codeRef automatically.
Upload a zip of the project, then attach the resulting catalog version to the artifact as its codeRef:
cd my-api && zip -r ../source.zip . -x '.venv/*' && cd ..
curl -s -X POST "${DATAROBOT_ENDPOINT}/files/fromFile/" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-F "file=@source.zip" | tee /tmp/upload.json
export CATALOG_ID=$(jq -r '.catalogId' /tmp/upload.json)
export CATALOG_VERSION_ID=$(jq -r '.catalogVersionId' /tmp/upload.json)
export STATUS_ID=$(jq -r '.statusId' /tmp/upload.json)
The upload finishes asynchronously. Poll its status by id until it's done—unlike the build and Workload status endpoints used later in this tutorial, this one signals completion with a 303 redirect rather than a status field in the response body:
until [ "$(curl -s -o /dev/null -w '%{http_code}' \
"${DATAROBOT_ENDPOINT}/status/${STATUS_ID}/" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}")" = "303" ]; do
sleep 2
done
Then patch the artifact:
curl -s -X PATCH "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"containerGroups": [{
"name": "default",
"containers": [{
"name": "primary",
"primary": true,
"port": 8080,
"imageBuildConfig": {
"codeRef": {
"datarobot": {
"catalogId": "'${CATALOG_ID}'",
"catalogVersionId": "'${CATALOG_VERSION_ID}'"
}
},
"dockerfile": {
"source": "generated",
"executionEnvironmentId": "<execution-environment-id>",
"executionEnvironmentVersionId": "<execution-environment-version-id>",
"entrypoint": ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
}
}
}]
}]
}
}'
Link the project directory to the artifact once, then sync. The CLI uploads the directory and sets the artifact's codeRef for you:
cd my-api
dr artifact code init ${ARTIFACT_ID}
dr artifact code sync
Preview what would be uploaded first with dr artifact code sync --dry-run.
Build the image¶
Trigger the build the same way you triggered the upload, through the REST API or the CLI.
curl -s -X POST "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}/builds" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" | tee /tmp/build.json
export BUILD_ID=$(jq -r '.buildIds[0]' /tmp/build.json)
The trigger returns 202 immediately. Poll until the build fails, is cancelled, or completes with its image applied to the artifact:
until curl -s "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}/builds/${BUILD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
| jq -e '(.status == "COMPLETED" and .imageApplied) or (.status | test("FAILED|CANCELLED"))' >/dev/null; do
sleep 10
done
curl -s "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}/builds/${BUILD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" | jq '{id, status, imageApplied}'
dr artifact build create ${ARTIFACT_ID} --wait
--wait polls until the build reaches a terminal status and prints the tail of the log if it fails. It stops at COMPLETED, which is before the artifact is updated to use the image, so confirm that below before deploying.
Wait for COMPLETED and for imageApplied to be true. BUILT means the image exists but the build has not finished, and COMPLETED means the image exists but the artifact is updated to use it a moment later. See Build statuses.
If the build reports FAILED, read the log:
curl -s "${DATAROBOT_ENDPOINT}/otel/artifact/${ARTIFACT_ID}/logs/?searchKeys=external_build_id&searchValues=${BUILD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" | jq -r '.data[] | "\(.timestamp) \(.level) \(.message)"'
dr artifact build logs ${ARTIFACT_ID} ${BUILD_ID}
Diagnose a failed build maps the common failures to fixes.
Confirm the image is attached¶
Check that the build populated the container's imageUri.
curl -s "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
| jq '.spec.containerGroups[0].containers[0] | {imageUri, build}'
imageUri is now populated, and its tag is the build ID.
Deploy the Workload¶
Deploy by artifactId. An artifact that builds from source cannot be supplied inline.
curl -s -X POST "${DATAROBOT_ENDPOINT}/workloads" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "my-api",
"artifactId": "'${ARTIFACT_ID}'",
"runtime": {
"containerGroups": [{
"name": "default",
"replicaCount": 1,
"containers": [{
"name": "primary",
"resourceAllocation": {"cpu": 1, "memory": "512MB"}
}]
}]
}
}' | tee /tmp/workload.json
export WORKLOAD_ID=$(jq -r '.id' /tmp/workload.json)
name: my-api
artifactId: <artifact-id>
runtime:
containerGroups:
- name: default
replicaCount: 1
containers:
- name: primary
resourceAllocation:
cpu: 1
memory: "512MB"
dr workload create --spec-file workload.yaml --output-format json | tee /tmp/workload.json
export WORKLOAD_ID=$(jq -r '.id' /tmp/workload.json)
Wait for the Workload to reach running:
until [ "$(curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" | jq -r '.status')" = "running" ]; do
sleep 10
done
Workload statuses are lowercase
Build statuses are uppercase (COMPLETED); Workload and artifact statuses are lowercase (running, draft). Match the exact casing in scripts.
Invoke the Workload¶
Confirm the deployed container serves live traffic by invoking its endpoint directly.
ENDPOINT=$(curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" | jq -r '.endpoint')
curl -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" "${ENDPOINT}/version"
Change the code and rebuild¶
Edit the app, then sync and rebuild. There is no need to cancel the previous build—triggering a new one supersedes it.
Re-upload the source and patch the codeRef as in Upload the code, then:
curl -s -X POST "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}/builds" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}"
dr artifact code sync
dr artifact build create ${ARTIFACT_ID} --wait
While the new build runs, the Workload keeps serving the previous image. To roll the new image out to the running Workload, see Replace and roll out.
Changed your dependencies?
Re-run uv lock before syncing. Builds install the lockfile as-is, so an edited pyproject.toml with a stale uv.lock builds the old dependency set without any error.
Lock for production¶
Locking makes the artifact immutable so it can back production Workloads. It requires a completed build—the platform verifies that imageUri came from one.
curl -s -X PATCH "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"status": "locked"}'
dr artifact lock ${ARTIFACT_ID}
Locking is irreversible, and a locked artifact accepts no further builds. To keep iterating, create a new draft artifact. See Artifact lifecycle.
Clean up¶
Stop and delete the Workload, then delete the artifact if you did not lock it in the previous step.
curl -s -X POST "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/stop" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}"
curl -s -X DELETE "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}"
# Only if you did not lock it: locked artifacts cannot be deleted
curl -s -X DELETE "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}" \
-H "Authorization: Bearer ${DATAROBOT_API_TOKEN}"
If you used the CLI, remove the local link with rm -rf .datarobot/workload/.
Next steps¶
This tutorial covered one full pass through the build-to-deploy flow; the following resources go deeper on configuration, build management, and post-deployment operations.
| Goal | Go here |
|---|---|
Understand codeRef, base images, and Dockerfile options |
Build an image from source code |
| Manage builds in detail | Trigger and manage builds |
| Roll a new image out to a running Workload | Replace and roll out |
| Instrument the service with OpenTelemetry | Instrument a Workload with OpenTelemetry |