Define a Background Job Beyond Its Acceptance Receipt
Drafted and reviewed using AI assistance.
On this page
A background job needs more than a spinner and a final “done.” Before asking a coding agent to implement it, define what the service has accepted, which work is running, which output a user can retrieve, and what happens when observation stops.
Microsoft's asynchronous request-reply pattern separates an acceptance response, a status resource and the eventual result. In that pattern, a successful status lookup can return HTTP 200 while the job is still running. Its suggested status fields include timestamps, an optional progress indicator and an error. Those are useful starting points; the application still needs its own state and result contract.
Give one job three concrete stages
The fictional Atlas Bundle app produces a downloadable set of text tiles from a built-in preset. All IDs, paths, histories and timings below are original teaching fixtures. No service, queue, renderer or download endpoint was run for this example.
The complete input is:
{
"job_id": "j-41",
"preset": "coast",
"pages": ["cover", "summary"],
"format": "text-bundle-v1"
}
The preset's initial plan has cover-title, cover-key, summary-counts and summary-notes. Expansion discovers two required companions: cover-legend and summary-source. These six names are also the final .txt filenames. Nothing in this example depends on customer documents or a real rendering package.
| Stage | Work unit | Completion condition |
|---|---|---|
| Validate | One check: schema, preset, page list or format | All four checks pass |
| Build | One named text tile | All six tiles in the revised plan exist in private staging |
| Package | One action: write manifest.json, then write bundle.zip |
Both actions finish; archive index contains the six tiles and manifest |
The proposed manifest lists the job ID, format and six filenames. An archive index is a requirement here, not an archive already generated by this article. After packaging, the service must publish the staged bundle and establish owner-accessible retrieval before marking the job succeeded.
The companion fixture, B62-14-case.json, contains the input, named work units, archive index and all four histories used below.
Specify states separately from delivery
Use these server-side transitions. Repeated observations of an unchanged state are allowed.
| Current state | Allowed next state | Meaning |
|---|---|---|
accepted |
running, cancel_requested |
Job record exists; execution need not have begun |
running |
running, cancel_requested, succeeded, failed |
Work is active; finishing one stage need not finish the job |
cancel_requested |
canceled, succeeded, failed |
Cancellation is pending; the service must resolve the race |
succeeded |
No new execution state | Result publication completed |
failed |
No new execution state | This execution ended with a recorded error |
canceled |
No new execution state | Service confirmed it stopped before result publication |
Delivery has a different vocabulary: none, partial_private, staged_private, available, blocked or expired. These are proposed app fields, not HTTP status codes.
Partial tiles are never downloadable under this contract. A completed local staging file is also insufficient: publication might still fail. A successful job has an owner-accessible result reference at publication; a particular client's later fetch may still fail. Keep that fetch observation separate from the job state.
Walk the successful history without inventing an ETA
Times are elapsed seconds in the fictional fixture. Progress always names its stage and plan revision.
| Time | Job state / stage | Stage work | Delivery | What the user can conclude |
|---|---|---|---|---|
| 0 | accepted / validate | 0 of 4 | none | Received, not started |
| 1 | running / validate | 0 of 4 | none | Validation started |
| 3 | running / validate | 4 of 4 | none | Validation finished; job still running |
| 4 | running / build, revision 1 | 0 of 4 | none | Initial build plan selected |
| 6 | running / build, revision 1 | 2 of 4 | partial_private | Two tiles exist privately |
| 7 | running / build, revision 2 | 2 of 6 | partial_private | Two additional required tiles were discovered |
| 10 | running / build, revision 2 | 6 of 6 | partial_private | Tile creation finished |
| 11 | running / package | 0 of 2 | partial_private | Packaging started |
| 13 | running / package | 2 of 2 | staged_private | Bundle is staged; publication remains |
| 14 | succeeded / package | 2 of 2 | available | Result published at /jobs/j-41/result |
| 74 | succeeded / package | 2 of 2 | expired | Retention ended; this job still succeeded earlier |
The build display moves from 50% to roughly 33% when its denominator changes. Preserve completed units and increment the plan revision; explain “2 of 6 tiles; plan expanded.” Do not silently reset completed work or keep showing the old percentage.
These units have different costs. Four validation checks, six tiles and two packaging actions do not form twelve equally weighted time slices. This design therefore shows stage counts, with no overall percent or remaining-time estimate. Even 2 of 2 packaging actions is not “download ready” before publication.
For this exercise, results are retained for 60 seconds after publication, making the boundary t=74 for j-41. In a real product, choose retention deliberately. After expiry, report “Finished; result expired,” rather than silently rerunning the job.
Decide what failure and missing observation mean
| Case | Recorded evidence | Decision in this contract |
|---|---|---|
j-42 build failure |
Validation completed; build reached 2 of 6, then TILE_BUILD_FAILED at t=8 |
Terminal failed; partial output blocked; no result link |
j-43 confirmed cancellation |
Accepted at 0; validate 1 of 4 at 1; cancel requested at 2; stopped at 3 | Terminal canceled; no result link |
j-44 cancellation race |
Validation and build finished; package 2 of 2 at 13; cancellation requested at 13.2; publication wins at 14 | Terminal succeeded; show “Finished before cancellation took effect” |
| Status lookup times out | Client last saw j-41 at t=7; at t=20 it cannot read status |
Client shows status unavailable, last known build 2 of 6; server outcome remains unknown to it |
| Result fetch fails after success | Client has a succeeded status but cannot fetch the result | Preserve succeeded; show retrieval problem, not a fabricated download success |
An unknown observation is a client condition, not a server execution state. In this fixture, a last update older than ten seconds is marked stale; it is not automatically converted into failed. At t=22, a successful new status observation can reveal that j-41 finished at 14. The lost-status interval did not restart or cancel it.
Likewise, Stop waiting stops this client's observation. Request cancellation asks the service to stop work. Neither proves a terminal cancellation until the service records it. Terminal state resolution must be serialized with publication so the UI never reports both canceled and successfully published for the same execution.
Recovery starts from the recorded state. For a known terminal failure, inspect its error and propose an explicitly requested replacement job if appropriate. This example provides no resumable checkpoint contract. For a lost status response, recover the existing job's status first; creating another job would be a new side effect.
Give the agent the lifecycle, not just the button label
Design the fictional Atlas Bundle job protocol and client presentation.
Work from B62-14-case.json and the states/tables in this guide.
The input is preset coast, pages cover and summary, format text-bundle-v1.
Validate four named checks. Build four initially planned tiles; expansion
adds cover-legend and summary-source, retaining completed work under plan
revision 2. Package manifest.json and bundle.zip. Publish only after all
required outputs are complete and owner retrieval has been established.
Keep server execution state, stage/count/plan revision, delivery state,
and client observation/fetch status separate. A status HTTP 200 is a read
result, not a job success signal. Never expose private partial/staged files.
Use the transition table, including cancel_requested resolving to canceled,
succeeded or failed. Define one serialized winner for publication versus
cancellation. Stop waiting is local; it does not cancel server work.
Do not change a terminal execution state when a result later expires.
Walk j-41 success and denominator expansion, j-42 failure, j-43 cancellation,
j-44 publication winning the race, and the t=7 to t=22 status-observation gap.
Show exact visible labels and whether a result link is available in each case.
Use stage units, not an invented overall percentage or ETA.
Return the complete state/output/error matrix and proposed status/result
representations first. Identify the persistence/publication/cancellation
boundary an implementation must enforce. Leave missing checkpoint, access
and retention implementation decisions explicit; the 60-second fixture
retention is illustrative, not a production requirement.
Use only a local teaching model for this task. Do not create workers/queues,
call cloud APIs, deploy endpoints or turn a status timeout into a new job.
Separate model checks, real implementation evidence and remaining unknowns.
This lifecycle extends the work after a creation receipt. It does not replace the separate decision about whether a retried creation refers to the same intended job. Keeping those contracts distinct lets a reviewer ask two answerable questions: which job was created, and what happened to that job afterward?