Job protocol

A job is one repository's share of decided work. Your agent claims it, builds on a branch, and closes it with a recap.

Where jobs come from

There is no create call. A job exists because a decision was resolved, or because a theme was fast-tracked past its decision. One job is one repository, so work across repositories is several jobs that share a decision and list each other in linkedJobIds. You learn of a new job from the job.created event; there is no wait endpoint, and GET /v1/jobs (at most 100 newest) is a read, not a loop.

StatusCovers
openqueued
claimedrunning, paused, awaiting_quick_decision, reviewing
closingclosing
closedsucceeded, failed, skipped

Proved by jobs.claim

Claim and lease

Claiming commits judgment, so it needs a person key with job:claim. A claim takes a lease of 2 hours that is never renewed: there is no heartbeat, and an agent still working at expiry loses the job. An org runs at most 4 live leases. The same key claiming again while its lease is live gets the same body with alreadyClaimed: true.

409 reasonMeaning
claimed_by_anotherAnother executor holds this job
not_openThis job is not open: it is closing, closed or waiting on a question
backing_offThis job is backing off after a failure and opens again later
at_capacityThe org already runs its maximum of 4 jobs
not_heldThis key does not hold the job
no_open_jobsThe decision has no open job
runner_onlyA design job delivers its document through a runner and cannot be claimed with an API key
org_pausedThis org is paused: Belora starts no new work until the plan is active again
org_lapsedThis org's plan has lapsed: Belora starts no new work until the plan is active again

POST/v1/jobs/{id}/claim

Claim an open job and commit to doing it; the lease lasts one generous window and the same key claiming again gets the same lease

jobs.claimperson keyjob:claimsuccess 200mcp claim_job

Path parameters

idstring

Response

idstring
status"open" | "claimed" | "closing" | "closed"
repoobject | nullThe one repository this job works in
decisionRefobject | null
linkedJobIdsstring[]The other jobs of the same decision ref, one per repository, to run together
claimablebooleanFalse for a design job: a runner delivers the document, so an API key cannot claim it (409 runner_only)
attemptsinteger
leaseExpiresAtnumber | nullWhen the claim lapses and the job returns to open; null unless claimed
claimedByobject | nullWho last claimed it; null while open
gradeobject | nullThe outcome read from the pull request, the same for every executor; null until one is recorded
createdAtnumber
decisionAnswerstring | nullThe verdict, or the assumption it proceeded on, that reopened this job after a pause; null otherwise
branchstringThe branch every commit and the pull request of this job must use
errorstring | nullWhy the job last returned to open or stalled, when it did
payloadobject | null
alreadyClaimedbooleanTrue when this key already held the live claim, so nothing changed

Errors: 401 unauthorized404 not_found409 conflict429 rate_limited403 key_cannot_commit_judgment403 key_lacks_capability

The claim returns the payload in the card above: the decision and its rationale, the theme with its signals and prior verdicts, acceptance criteria, the repositories with their base refs, the linked job ids, and pointers to the tools to pull more. It carries pointers, never repository content or procedure bodies.

Commit and open the pull request on the branch the claim returns, which starts with belora/. A pull request event is matched to the job by that head branch. The job's grade then reads the newest pull request recorded on its theme in the job's repository, and a check suite's result is matched by theme and repository.

Proved by jobs.claim jobs.lease

Linked jobs

POST /v1/decisions/{id}/jobs/claim claims every open job of one decision in a single step, for a session that works the repositories together. It checks every sibling and the lease cap before leasing any: if one is held by another, backing off, or would pass the cap, nothing is claimed.

POST/v1/decisions/{id}/jobs/claim

Claim every open job of a decision together, all or none, for work that spans repositories

jobs.claimLinkedperson keyjob:claimsuccess 200mcp claim_linked_jobs

Path parameters

idstring

Response

jobsobject[]
jobs[].idstring
jobs[].status"open" | "claimed" | "closing" | "closed"
jobs[].repoobject | nullThe one repository this job works in
jobs[].decisionRefobject | null
jobs[].linkedJobIdsstring[]The other jobs of the same decision ref, one per repository, to run together
jobs[].claimablebooleanFalse for a design job: a runner delivers the document, so an API key cannot claim it (409 runner_only)
jobs[].attemptsinteger
jobs[].leaseExpiresAtnumber | nullWhen the claim lapses and the job returns to open; null unless claimed
jobs[].claimedByobject | nullWho last claimed it; null while open
jobs[].gradeobject | nullThe outcome read from the pull request, the same for every executor; null until one is recorded
jobs[].createdAtnumber
jobs[].decisionAnswerstring | nullThe verdict, or the assumption it proceeded on, that reopened this job after a pause; null otherwise
jobs[].branchstringThe branch every commit and the pull request of this job must use
jobs[].errorstring | nullWhy the job last returned to open or stalled, when it did
jobs[].payloadobject | null
jobs[].alreadyClaimedbooleanTrue when this key already held the live claim, so nothing changed

Errors: 401 unauthorized404 not_found409 conflict429 rate_limited403 key_cannot_commit_judgment403 key_lacks_capability

Proved by jobs.linked

Recap and the synthesis fallback

Closing a job takes a recap: a headline, what changed, the decisions you made, what you verified and what you are unsure of. With a recap the job is closed at once and synthesized is false. Without one, the job enters closing and Belora writes the recap from the first 48 KiB of the pull request diff, answering synthesized: true. A failed synthesis is tried 3 times, waiting 1 minute and doubling each time; if the pull request merged, the last resort is a recap built from its title and description with the uncertainty stated. A job still closing 1 day after its last update fails unless its pull request has merged.

POST/v1/jobs/{id}/recap

Close a claimed job with a recap, or with no body to have one synthesized from its pull request diff

jobs.recapperson keyjob:claimsuccess 200mcp close_job

Path parameters

idstring

Body

headlinestring
changedobject[]
changed[].pathstring
changed[].whatstring
decisionsobject[]
decisions[].questionstring
decisions[].choicestring
verifiedstring[]
uncertaintiesstring[]
prTitle?string
prBody?string
deltas?object | object | object | object[]Memory this run learned, filed for the reviewer: a procedure edit (sop_edit), a claim to test (hypothesis) or a term to coin (vocab). Nothing here changes canon on its own

Response

idstring
status"open" | "claimed" | "closing" | "closed"
synthesizedbooleanTrue while a recap is still being synthesized from the pull request diff

Errors: 401 unauthorized404 not_found400 validation_failed409 conflict429 rate_limited403 key_cannot_commit_judgment403 key_lacks_capability

Proved by jobs.recap jobs.recap_synthesized

Asking a question

A held job that needs a product answer pauses with a question and 2 to 4 options. The lease is released and the job still reads claimed; a verdict through any door reopens it with the answer in decisionAnswer. The API door addresses the question by the job's id: POST /v1/decisions/{id}/verdict with that id and a person key that holds decision:resolve, the same guard the console and Slack doors pass. A question answered once is 409 conflict with details.reason already_answered and details.answer carrying the answer that won, which is also what a verdict that lost a race receives. A quick bet states how long the question may wait, between 1 minute and 4 hours, and whether to block or to proceed on a stated assumption when the window closes.

Stated, not run by the suite: decision.verdict_paused

Grading

Every claimed job is graded from its pull request, the same way whoever executed it. The grade on the job is null until recorded, then reports whether the pull request merged, whether its checks passed, whether it was closed without merging, whether Belora wrote the recap, and who closed it. It is rewritten on every pull request event, so it follows the pull request.

Proved by jobs.grade

Expiry and requeue

Every claim adds one to the job's attempts. A release hands that attempt back; a question the job pauses on does not. A lease that lapses without a close or release returns the job to open and emits job.expired with outcome requeued, unless its attempts have reached 10, in which case the job fails with outcome failed. That cap is checked only when a lease lapses. A run reported as failed for a reason worth retrying is retried after a backoff and fails after 4 such failures, whatever its attempts.

POST/v1/jobs/{id}/release

Give a claimed job back: it returns to open with its attempt handed back and the release visible on its theme

jobs.releaseperson keyjob:claimsuccess 200

Path parameters

idstring

Response

idstring
status"open" | "claimed" | "closing" | "closed"
repoobject | nullThe one repository this job works in
decisionRefobject | null
linkedJobIdsstring[]The other jobs of the same decision ref, one per repository, to run together
claimablebooleanFalse for a design job: a runner delivers the document, so an API key cannot claim it (409 runner_only)
attemptsinteger
leaseExpiresAtnumber | nullWhen the claim lapses and the job returns to open; null unless claimed
claimedByobject | nullWho last claimed it; null while open
gradeobject | nullThe outcome read from the pull request, the same for every executor; null until one is recorded
createdAtnumber

Errors: 401 unauthorized404 not_found409 conflict429 rate_limited403 key_cannot_commit_judgment403 key_lacks_capability

Stated, not run by the suite: jobs.expiry

Proved by

Every claim on this page is one of these checks; those marked stated are documented behaviour the suite cannot drive. Run them against your org:

npx @belora/conformance --key $BELORA_CONFORMANCE_KEY --api $API --mcp https://mcp.belora.dev/mcp
  • jobs.claim A person key claims a job

    POST /v1/jobs/{id}/claim from a person key answers the job claimed with a lease, a branch and a payload; the same key again answers alreadyClaimed true.

  • jobs.lease A lease has one holder and a fixed termstated, not run by the suite

    While a lease is live another key's claim answers 409 conflict with reason claimed_by_another, the org never runs more live leases than the published cap, and the lease expires at the published term without a heartbeat.

  • jobs.linked Linked jobs are claimed together

    POST /v1/decisions/{id}/jobs/claim claims every open job of the decision or none, and a repeat answers alreadyClaimed per job.

  • jobs.recap A recap closes a held job

    POST /v1/jobs/{id}/recap with a recap body closes the job with synthesized false; a key that does not hold the job answers 409 conflict.

  • jobs.recap_synthesized A missing recap is synthesized from the diffstated, not run by the suite

    POST /v1/jobs/{id}/recap with no recap moves the job to closing with synthesized true, and the job closes with a recap marked synthesized when the synthesis lands; when the pull request has merged and the published retries are spent it closes with a recap built from the pull request's title and description, and a job whose pull request has not merged stays closing until its expiry.

  • decision.verdict_paused A paused job's question is answered oncestated, not run by the suite

    POST /v1/decisions/{id}/verdict with the id of a job that asked a question reopens the job with decisionAnswer set; a second verdict, or one that lost a race, answers 409 conflict with details.reason already_answered and details.answer carrying the winning answer.

  • jobs.grade A closed job is graded from its pull request

    After the job's pull request merges, GET /v1/jobs/{id} carries a grade with merged true, the checks verdict, recapSynthesized and closedBy api_key, and the grade follows later pull request events.

  • jobs.expiry A lapsed lease requeues the jobstated, not run by the suite

    A job whose lease passes its term without a close or release returns to open with attempts kept and a job.expired event with outcome requeued, and fails with outcome failed once the published reclaim cap is reached.