# DD — Reliable Workload creation (2D)
[Common design](dd.md) · [Tasks and completion status](adr.md).
Workload Portfolio saves the Workloads that Runs and Pipelines execute. A Workload
belongs to one Project and has immutable, numbered revisions. Each revision holds
the execution specification and required inputs that Execution Core plans from.
2D is delivered in two parts:
1. **Workload storage:** the `workload_portfolio` context, its internal operations,
PostgreSQL persistence and the real owner adaptors for the Workload ports used
by 3B, 3C and 8A.
2. **[Repository actions](#repository-actions):** read `actions.yaml` from the
selected branch, validate the action and its parameters, and prepare its
Workload revision through the create and revise operations.
## Flow
1. An internal command creates, revises or retires a Workload, or reads it.
2. The handler checks the member's current Project access through its own
permission port. Creation follows launch access, because running a repository
action creates its Workload.
3. The domain validates the revision and stamps the Workload and Project identity
into its specification.
4. The shared PostgreSQL adaptor saves the Workload snapshot, version and command
receipt in one transaction.
5. Execution Core and Pipeline Coordination read saved Workloads through
service-level owner adaptors, without member authority. Their own handlers
check access.
```mermaid
flowchart TD
C["Internal create, revise or retire command"] --> H["Workload handler"]
H --> A["Check current Project access"]
A --> D["Validate revision and stamp identity"]
D --> P[("Workload Portfolio PostgreSQL
Snapshot, version and receipt")]
P --> E["Owner adaptors"]
E --> L["3B launch acceptance"]
E --> R["3C planning"]
E --> S["8A Pipeline start"]
```
## Ownership and saved data
`domain.workload.Workload` is the aggregate root. Its revisions are children,
saved and changed together.
- **Root:** Workload ID, Project, creating member, display name, creation time,
version and optional retirement time. `state` is `active` or `retired`.
- **Revision:** number, creation time, `specification` (`intent`, `resourceDemand`,
`projectId`, `workloadId`) and `requiredInputs`, such as source repositories.
- **Rules:** revisions are numbered from one without gaps and never change. Each
create, revise or retire change advances the version once. A retired Workload
cannot be revised. Changes cannot precede the latest revision.
The owner validates the execution profile: `queued-batch`, or `reconciled-service`
with `serviceKind: application | huggingface-inference`. Execution Core remains
the single validator of the complete execution intent when it plans a Run.
## Internal operations
`application.handlers.WorkloadOperations` receives the shared `CommandPort` and
`QueryPort` plus an explicit `WorkloadAuthority`.
- `create` saves version 1 with its first revision. The complete Workload is the
command's material input.
- `revise` appends the next revision at the expected version.
- `retire` stops later revisions and new launches. Existing revisions stay readable
for already accepted work.
- `read` returns the saved Workload after checking the requester's current access.
Receipts replay recorded outcomes. Reusing a command identity with changed input
returns `idempotency_conflict`. Stale versions return `version_conflict`. Invalid
revisions, early times and retired Workloads are recorded rejections. Denied access
is rejected; unavailable or uncorrelated authority fails for retry.
## Owner adaptors
[`adaptors/workload_evidence.py`](../../../adaptors/workload_evidence.py) answers
the existing ports from saved Workloads:
| Port | Consumer | Returns |
| --- | --- | --- |
| `WorkloadLookup.get_workload` | 3B launch acceptance | Project, version, current revision, state |
| `WorkloadRevisions.get_revision` | 3C planning | Exact specification and required inputs |
| `WorkloadRevisions.get_revision` | 8A Pipeline start | Exact Project, family and service kind |
A missing Workload or revision returns `None`. Corrupt saved state or a database
failure raises each port's `WorkloadLookupUnavailable`, so callers retry instead
of recording a business decision.
## Persistence
The shared `Commands` and `Queries` adaptors store one `kind = workload` snapshot
in `pharos.aggregates` and its receipts in `pharos.command_receipts`. No
context migration is required. The restricted `pharos_workload_portfolio` role
uses its own database and cannot migrate schemas.
Workloads have no browser projection or integration events yet. They are
registered explicitly without a projection. 2F and 7A add the outgoing updates
the browser needs, recorded in the same transaction.
## Repository actions
`application.repository_actions.ActionWorkloads.prepare` turns "run action X on
branch B with these parameter values" into a saved Workload revision. 1D and 8E
launch that revision.
1. Check the member's current Project access.
2. Read the parsed `actions.yaml` at the commit the branch names now through the
`RepositoryActions` port; the read reports that commit, which pins the launch.
3. Validate the action and parameter values, then substitute whole `$name` tokens
in every field except `parameters`. Missing values use the declared default.
4. Resolve each `repos[].url` to a canonical Project repository ID through the same
port.
5. Convert the action into a specification and required inputs
([`domain/actions.py`](../../../contexts/workload_portfolio/domain/actions.py)).
6. Save it under the action's Workload:
- **First run:** create revision 1.
- **Same specification as the current revision:** reuse it without a write.
- **Changed definition or parameter values:** revise, adding the next revision.
- **Retired Workload:** reject; it is not revived.
`prepare_all` reads `actions.yaml` once and prepares every action in declaration
order for [8E](repository-pipelines.md), deriving one stable command identity per
action from the request.
Each Project repository, branch and action has one Workload with a stable UUIDv5
identity. Retries and concurrent clicks converge on it: a click that loses a race
reads the Workload again and reuses the winner's revision.
| `actions.yaml` | Saved specification |
| --- | --- |
| `slurm-job` | `queued-batch`; runs in `slurm` |
| `k8s-job` | `queued-batch`; runs in `k8s`, through the existing Kubernetes Job Runtime |
| `k8s-deployment` | `reconciled-service`, `serviceKind: application`; runs in `k8s` |
| `k8s-inferenceservice` | `reconciled-service`, `serviceKind: huggingface-inference`; runs in `k8s` |
| Batch `pre`, `script`, `post` | One `sh -c` command; the Runtime runs it with errexit |
| Service `pre` / `script` | `preCommands` in the same Pod / service command |
| Inference `script` | Not run, as in Themelio; the installed model server serves `main_repo` |
| `env: NAME=value` | Runtime environment values |
| Selected repository | `main_repo` input on the selected branch |
| `repos[].url`, `dir` | Additional inputs named by `dir` or the URL basename |
| `options.cpus`, `ram`, `gpus` | CPU cores, GiB of memory and GPU count; [per-type limits](../../../contexts/workload_portfolio/README.md#repository-actions) apply |
| `options.timeout` | Batch wall time in whole hours; 24 hours when omitted |
| `options.port`, `modelName`, `inference.*` | Service port (8000 by default), model name and model server settings |
The environment is not part of the saved specification. `prepare` derives it from
the action type and returns it with the prepared revision, which the launch (1D)
and Pipeline steps (8E) use. The request never supplies it, and the action list
does not return it.
Themelio's defaults apply when options are omitted: 1 CPU and 4 GiB for batch,
1 CPU and 1 GiB for applications, and 2 CPUs and 8 GiB for inference. Application
readiness uses `/`; inference uses `/v1/models/`.
These are rejected with `unsupported_action_feature`: `ref`, cached repositories,
`options.storage`, `options.gpuLabel`, `inference.initImage`, `post` on services
and credential-like environment names. Unknown fields and invalid values are
rejected with `invalid_action` or `invalid_action_parameter`. Both batch action
types share their saved specification shape; their environment selects the
Kubernetes Job or Slurm Runtime. Kubernetes batch accelerator restrictions are
recorded in the [Workload Portfolio guide](../../../contexts/workload_portfolio/README.md#repository-actions).
## Remaining connections
- **Repository owner:** `RepositoryActions` uses a test-only implementation. The
real adaptor reads `actions.yaml` from GitLab with the member's repository access
and resolves repository URLs through provisioning's repository bindings (4.1, 4.3).
- **Process wiring:** the launch worker, Run Supervisor and Pipeline Coordinator
still receive Workload providers from their composition. Connect
`workload_owners(database)` with their permanent GitOps deployments (3D, 4E, 8B),
which adds the Workload Portfolio database credential to those processes.
- **Permissions:** the permission port uses test-only providers until the real
Project permission provider is connected.
## Verification
Completed on 21 October 2026:
- **63 domain, application and owner-adaptor cases** passed with a test-only
permission provider. They cover identity stamping, gap-free revisions, both service
kinds, unsupported profiles and inputs, retirement, change times, corrupt
snapshots, receipt replay, conflicts, stale versions and denied, unavailable or
uncorrelated authority. Saved batch and application revisions pass Execution
Core's own planning validation and the Pipeline revision check.
- `uv run --project devops --frozen pnpm verify` passed, including **1,762 Metallo
tests**, strict Mypy over **287 source files**, installed-wheel imports and renders.
- `uv run --project devops --frozen pnpm test:integration` passed, including
**280 Metallo PostgreSQL cases**. The 8 Workload cases use the production adaptors
and restricted runtime role: create/revise/retire/read with receipts, recorded
rejections, six concurrent revisions at one version, corrupt saved state, rollback
after the snapshot and receipt are written, owner reads accepted by Execution
Core, and refusal of another context's database.
### Repository actions
Completed on 21 October 2026:
- **42 conversion and preparation cases** passed with test-only repository and
permission owners. They cover all three action types, parameter defaults,
validation and substitution, repository directories, unsupported features,
create/reuse/revise, branch and action identities, retries, a concurrent click,
missing files and repositories, denied access, owner outages and retired
Workloads. Converted batch, application and inference specifications pass
Execution Core's planning validation.
- `pnpm verify` passed with **1,804 Metallo tests** and strict Mypy over **292
source files**. `pnpm test:integration` passed with **282 Metallo PostgreSQL
cases**. The two new cases prepare create, reuse and revise against the restricted
role, and six concurrent clicks on one action save one Workload with one revision.
### Dev-box verification
On 21 October 2026, `make metallo-identity-test TARGET=dev-NAME SSH=your-dev-box`
passed **1,564 cases** in an owned namespace on the dev box, including **106
Workload Portfolio cases**: storage and repository-action unit cases and all
10 PostgreSQL cases. Production migrations and restricted-role PostgreSQL were
real. The namespace was removed afterwards.
Not run: real Project permission provider, real GitLab repository owner and process
wiring. These remain open in the [ADR](adr.md).