# Tutorial: Build a Workload from source

> Tutorial: Build a Workload from source - Take a FastAPI project from a local directory to a running
> Workload endpoint without writing a Dockerfile or pushing an image.

This Markdown file sits beside the HTML page at the same path (with a `.md` suffix). It summarizes the topic and lists links for tools and LLM context.

Companion generated at `2026-09-30T19:40:40.962573+00:00` (UTC).

## Primary page

- [Tutorial: Build a Workload from source](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md): Full documentation for this topic (Markdown sidecar).

## Sections on this page

- [Prerequisites](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#prerequisites): In-page section heading.
- [Create the sample project](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#create-the-sample-project): In-page section heading.
- [Create the artifact](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#create-the-artifact): In-page section heading.
- [Upload the code](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#upload-the-code): In-page section heading.
- [Build the image](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#build-the-image): In-page section heading.
- [Confirm the image is attached](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#confirm-the-image-is-attached): In-page section heading.
- [Deploy the Workload](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#deploy-the-workload): In-page section heading.
- [Invoke the Workload](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#invoke-the-workload): In-page section heading.
- [Change the code and rebuild](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#change-the-code-and-rebuild): In-page section heading.
- [Lock for production](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#lock-for-production): In-page section heading.
- [Clean up](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#clean-up): In-page section heading.
- [Next steps](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#next-steps): In-page section heading.

## Related documentation

- [Workload API](https://docs.datarobot.com/en/docs/workload-api/index.html.md): Linked from this page.
- [Build artifacts](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/index.html.md): Linked from this page.
- [Build artifacts from source code](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/index.html.md): Linked from this page.
- [DataRobot CLI](https://docs.datarobot.com/en/docs/workload-api/workload-interfaces/workload-cli/index.html.md): Linked from this page.
- [Choose how the Dockerfile is obtained](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/build-from-code.html.md#choose-dockerfile-source): Linked from this page.
- [Build statuses](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/build-operations.html.md#build-statuses): Linked from this page.
- [Diagnose a failed build](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/build-logs.html.md#diagnose-a-failed-build): Linked from this page.
- [Replace and roll out](https://docs.datarobot.com/en/docs/workload-api/update-workloads/replace-artifact-rollouts.html.md): Linked from this page.
- [Artifact lifecycle](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/artifacts-concepts.html.md#artifact-lifecycle): Linked from this page.
- [Instrument a Workload with OpenTelemetry](https://docs.datarobot.com/en/docs/workload-api/monitor-workloads/instrument-with-otel.html.md): Linked from this page.

## Documentation content

> [!NOTE] 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.toml at its root. A uv.lock beside it is required for the build; if you use the CLI, dr artifact code sync generates one for you when it is missing. Otherwise run uv lock before uploading.
- The ID and version ID of an execution environment to use as the base image.
- A service , agent , or mcp artifact—code-to-workload builds don't apply to other artifact types. This tutorial omits type from the artifact spec, which defaults to service .

**cURL:**
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>"
```

**CLI:**
The [DataRobot CLI](https://docs.datarobot.com/en/docs/workload-api/workload-interfaces/workload-cli/index.html.md) ( `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
```

```
# my-api/pyproject.toml
[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",
]
```

```
# my-api/app.py
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:

```
# my-api/views/version.py
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:

```
# artifact.json
{
  "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:**
```
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)
```

**CLI:**
```
dr artifact create --spec-file artifact.json --output-format json | tee /tmp/artifact.json
export ARTIFACT_ID=$(jq -r '.id' /tmp/artifact.json)
```


> [!TIP] 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](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/build-from-code.html.md#choose-dockerfile-source).

## Upload the code

Upload the code through the REST API directly, or with the CLI, which also updates the artifact's `codeRef` automatically.

**cURL:**
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"]
            }
          }
        }]
      }]
    }
  }'
```

**CLI:**
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:**
```
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}'
```

**CLI:**
```
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](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/build-operations.html.md#build-statuses).

If the build reports `FAILED`, read the log:

**cURL:**
```
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)"'
```

**CLI:**
```
dr artifact build logs ${ARTIFACT_ID} ${BUILD_ID}
```


[Diagnose a failed build](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/build-logs.html.md#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:**
```
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)
```

**CLI:**
```
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
```

> [!NOTE] 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.

**cURL:**
Re-upload the source and patch the `codeRef` as in [Upload the code](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/build-from-code/tutorial-build-from-code.html.md#upload-the-code), then:

```
curl -s -X POST "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}/builds" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}"
```

**CLI:**
```
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](https://docs.datarobot.com/en/docs/workload-api/update-workloads/replace-artifact-rollouts.html.md).

> [!TIP] 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:**
```
curl -s -X PATCH "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"status": "locked"}'
```

**CLI:**
```
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](https://docs.datarobot.com/en/docs/workload-api/build-artifacts/artifacts-concepts.html.md#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 |
