Async jobs
Most mutations on managed.dev aren’t instantaneous — running a build, cloning an
environment, creating a backup. Rather than block the request, the API returns a
job: a 202 Accepted, a Location header, and a tracking object you watch to
completion. This async-native model is what lets the CLI, the SDK, and your own
automation all wait for the same work correctly.
What a non-instant mutation returns
Section titled “What a non-instant mutation returns”A mutation that can’t complete synchronously responds with 202 Accepted, a Location
header pointing at the job, and the job in the body:
HTTP/1.1 202 AcceptedLocation: /v1/jobs/job_01J9…Content-Type: application/json{ "data": { "id": "job_01J9…", "type": "environment.create", "status": "queued", "progress": 0, "created_at": "2026-06-23T18:04:11.412Z", "resource": { "type": "environment", "id": "env_01J8…", "site_id": "site_01J7…", "env_id": "env_01J8…" }, "result": null, "error": null, "links": { "self": "/v1/jobs/job_01J9…", "stream": "/v1/jobs/job_01J9…/stream" } }, "request_id": "req_01J9…"}The job envelope
Section titled “The job envelope”Every job, from every endpoint, has the same shape:
| Field | Description |
|---|---|
id |
The job id (job_01J9…). Quote it to support; use it to poll or stream. |
type |
What’s running — a dotted public type like deployment.deploy, build.run, environment.push, backup.create, or malware.scan. |
status |
queued → running → succeeded | failed. Exactly these four values. |
progress |
0–1. Coarse today (0 queued, 0.5 running, 1 terminal) until operations report per-step progress. |
created_at |
When the job was accepted. |
resource |
The thing being acted on: its type (e.g. deployment, build, environment, backup), its id, and the tenancy context (site_id, and env_id when environment-scoped). |
result |
Populated on succeeded with the operation’s output ids — keys like artifact_id, deployment_id, snapshot_id, build_id, scan_id, url. null until then. |
error |
Populated on failed with the error object. null otherwise. |
links.self |
The job’s own URL — GET it to poll. |
links.stream |
The SSE endpoint for a live tail. |
A terminal status (succeeded, failed) is final — the job stops changing once it
reaches one. A cancelled operation surfaces as failed with an explanatory error.
Three ways to consume a job
Section titled “Three ways to consume a job”Pick the path that fits your environment. All three watch the same job.
1. The 202 body
Section titled “1. The 202 body”For fire-and-forget work, the job in the 202 body may be all you need — you have the
id for later, and you can move on. Come back and GET it whenever you want the
outcome.
2. SSE live tail
Section titled “2. SSE live tail”For a CLI, a UI, or anything that wants real-time progress, stream the job over
server-sent events. The stream carries the job’s live output as it’s produced —
build logs, command output — and closes with a terminal done event carrying the
final status:
curl -N https://api.managed.dev/v1/jobs/job_01J9…/stream \ -H "Authorization: Bearer mfk_live_…" \ -H "Accept: text/event-stream" \ -H "Forge-Version: 2026-06-23"data: Building wordpress artifact from refs/heads/main…data: Signed artifact art_01J9… (sha256:9f2c…)
event: donedata: succeededThe done payload speaks the same public status set as GET /v1/jobs —
succeeded or failed. The mf jobs watch command and the dashboard both ride
this stream.
3. ETag long-poll
Section titled “3. ETag long-poll”For SSE-hostile environments — CI runners behind proxies that buffer streams —
GET the job with If-None-Match. The job returns an ETag; send it back and you
get 304 Not Modified until something changes, then the new state:
curl -i https://api.managed.dev/v1/jobs/job_01J9… \ -H "Authorization: Bearer mfk_live_…" \ -H 'If-None-Match: "w/qd-1"' \ -H "Forge-Version: 2026-06-23"A 304 means “nothing new — keep waiting”; a 200 carries the updated job and a fresh
ETag to poll against next. This is the right path for automation that can’t hold a
stream open.
A worked example — run a build and tail it
Section titled “A worked example — run a build and tail it”curl -i -X POST \ https://api.managed.dev/v1/sites/site_01J7…/environments/env_01J8…/builds \ -H "Authorization: Bearer mfk_live_…" \ -H "Forge-Version: 2026-06-23" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "builder": "wordpress", "source_ref": "refs/heads/main" }'# → 202 Accepted, Location: /v1/jobs/job_01J9…curl -N https://api.managed.dev/v1/jobs/job_01J9…/stream \ -H "Authorization: Bearer mfk_live_…" \ -H "Accept: text/event-stream" \ -H "Forge-Version: 2026-06-23"# → build output → event: done, data: succeededjob, err := client.Deployments.CreateBuild(ctx, siteID, envID, forge.BuildCreateParams{Builder: "wordpress"})if err != nil { log.Fatal(err)}
// Follow streams output over SSE (with a poll fallback) until terminal;// FollowSuccess additionally returns a *forge.JobError on a failed job.done, err := client.Jobs.FollowSuccess(ctx, job.ID, os.Stdout)if err != nil { var jobErr *forge.JobError if errors.As(err, &jobErr) { log.Fatalf("build failed: %v", jobErr) // jobErr.Job holds the failed job } log.Fatal(err)}fmt.Println(done.Status, done.Result["artifact_id"])Prefer client.Jobs.Wait / WaitSuccess when you only need the terminal state, or
client.Jobs.Stream for raw SSE events.
mf deploy build --builder wordpress --ref refs/heads/main --follow# streams the job to completion, exits non-zero if it failsAsync mf commands accept --wait (poll to completion) or --follow (stream
output); without either they print the job id and return.
Listing and finding jobs
Section titled “Listing and finding jobs”GET /v1/jobs lists jobs for the principal, cursor-paginated and filterable by
status, resource_type, and site_id:
curl "https://api.managed.dev/v1/jobs?status=failed&resource_type=deployment&site_id=site_01J7…&limit=20" \ -H "Authorization: Bearer mfk_live_…" \ -H "Forge-Version: 2026-06-23"Reading jobs requires the jobs:read scope. From the CLI, use
mf jobs list [--site S] [--status ST] and mf jobs get <id>. See the
jobs API resource for the full parameter set.