Job output secrets
A job that produces values — Terraform outputs, provisioned resource ids, generated connection strings — publishes them as project/environment-rung secrets through one route, the mirror of the lease-bound resolve:
POST /v1/organizations/{orgId}/projects/{projectId}/state/runs/{runUlid}/output-secrets
Request and response
{
"runnerId": "runner-8f2c",
"jobId": "api.prod.deploy",
"leaseEpoch": 1,
"secrets": [
{ "key": "DATABASE_URL", "value": "postgres://…" },
{ "key": "QUEUE_ENDPOINT", "value": "https://…" }
]
}
{
"data": {
"published": ["DATABASE_URL", "QUEUE_ENDPOINT"],
"environment": "prod",
"component": "api"
},
"meta": { "requestId": "req_5f2d1c0b9a8e7f6d5c4b3a36", "cursor": null }
}
Gates
The same two-gate posture as the resolve:
- Bearer authorization — the action is
state.object.write, the state plane's write posture. Workflow actors grant by token-bound scope; everyone else via role policy. Denied as404. - A live lease — the body's
(runnerId, jobId, leaseEpoch)must match the live lease on this run's job; only the runner actually holding the job can publish. A lapsed lease is409 lease_lost.
Scope is 100% server-derived
The publish can aim nowhere the leased job does not already live:
- Org and project come from the route (already authorized).
- Component comes from the leased
run_jobsrow. - Environment is parsed from the job id, which is shaped
<component>.<environment>.<job>— using the row's component as the authoritative prefix.
Nothing in the body can point the write elsewhere. A job with no derivable (component, environment) cannot publish at all — 422, there is no rung to aim at.
Validation
| Rule | Limit |
|---|---|
| Keys per publish | ≤ 32 |
| Key shape | ^[A-Z][A-Z0-9_]{0,127}$ (UPPER_SNAKE), unique within the request |
| Values | Non-empty strings, ≤ 64 KiB |
Validation errors never echo values — a malformed slot may itself be a secret.
What the write does
The publish lands on the project/environment rung of the secret store:
- First publish of a key creates the secret —
source: "static", created by thesystemactor. - Later publishes of the same key append a new version — a rotate, never an overwrite. History is preserved, and consumers pinned to
@Nkeep resolving the version they pinned.
Provenance — the run, job, and component that produced the value — rides the audit event, so every published value is traceable to the job that emitted it.
This run-lease channel is the only way CI writes secrets. The config API's create and rotate routes are not CI-writable — workflow actors can read secret metadata under their bound project but hold no write grant on the config surface (an earlier CI write grant there was reverted in favor of this channel). See CI workflow grants.