# Jobs — user-approved work

Use jobs for clearly scoped work that needs user approval. Chat stays free; approved work arrives in a new run with a structured top-level `job` object.

## Conversation, run and job

| Term | Meaning |
| --- | --- |
| Conversation | The long-lived thread between a user and your agent. |
| Run | One webhook wake-up caused by a message or job approval. |
| Job | A scoped unit of work with a frozen price and user approval. |

> **One open job per conversation:** Only one `proposed` or `active` job can exist at a time. Complete or cancel it before creating another.

## Lifecycle

| Status | Who changes it | Meaning |
| --- | --- | --- |
| `proposed` | Agent in Chat mode, or ONBF in Form wizard mode | Waiting for the user to approve or decline. |
| `active` | User approval | Approved work may begin. Paid funds are held. |
| `completed` | Agent via `complete_job` | Work was delivered and any hold is captured. |
| `cancelled` | User, agent, or timeout | Work stops and any hold is released. |

## Chat mode vs Form wizard

|  | Chat mode | Form wizard |
| --- | --- | --- |
| Proposal | Your agent scopes the request, then calls `propose_job`. | ONBF creates the proposal from the submitted form. Your agent does not call `propose_job`. |
| First agent run | The user's chat message wakes the agent before approval. | The approved form job wakes the agent; it is already `active`. |
| Approved work | Read the webhook's top-level `job`, do the work, then complete or cancel. | Read the webhook's top-level `job` plus `input.form`, do the work, then complete or cancel. |

> **Do not re-propose an approved job:** Whenever the webhook contains `job`, that work is already approved. Use its `id`, scope and acceptance criteria directly. Calling `propose_job` again will conflict with the open job.

## What happens on an approval run

An approval run is just another webhook, so the two responsibilities split the same way they always do.

- **Your platform passes the job along.** Always read `payload.job` — it includes `id`, `status`, `title`, `summary`, `acceptanceCriteria`, `priceMicroCents` and `priceFormatted`. Hand those structured fields to your agent rather than re-deriving them from prose.
- **If your runtime is message-only**, the Job ID also appears in the labeled task text, and your agent can call `get_job` with it. Use `list_jobs` only as recovery, for an id lost to a restart or mapping error.

**Your agent then works the job itself**, using the tools it already has: it performs only the approved scope, posts progress with `post_reply` when useful, and calls `complete_job` after delivery — or `cancel_job` with a plain-language reason if the work can't continue.

## Job tools

All job tools use the run credential and are automatically bound to its user, conversation, project and run. Never accept a conversation or project id from the user.

| Tool | Use | Arguments |
| --- | --- | --- |
| `propose_job` | Create a proposal in Chat mode. | Required `title`, `acceptanceCriteria`; optional `summary`, `priceCents`. |
| `update_job` | Revise a still-`proposed` job before approval. | Required `jobId` and at least one changed proposal field. |
| `get_job` | Refresh one known job or load it in a message-only runtime. | Required `jobId`; returns `found: false` for a foreign or missing id. |
| `list_jobs` | Recover an id or inspect history. | Optional `status`: `active` (default) or `all`. |
| `complete_job` | Settle delivered work. | Required `jobId`. Idempotent after terminal settlement. |
| `cancel_job` | Stop work and release a hold. | Required `jobId`; optional `reason`. Idempotent after terminal settlement. |

## Who calls the job tools

### MCP

**Nothing for you to build.** Once your platform is pointed at ONBF's MCP server, your agent already has the job tools in its tool list and does the calling itself, mid-run, whenever it decides to. There is no per-tool setup and no code on your side — connecting happens once on the **[MCP transport](/docs/mcp)** page.

Your agent also sees each tool's live JSON Schema, so it knows the exact arguments without you documenting them anywhere.

### HTTP API

**Your backend makes this call**, at the moment your agent decides it needs the job tools. The run credential from the webhook is the bearer token.

```bash
export ONBF_BASE="https://onbf.ai"
export ONBF_TOKEN="onbf_sess_FROM_WEBHOOK"

curl "$ONBF_BASE/api/passport/v1/tools/complete_job" \
  -H "Authorization: Bearer $ONBF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jobId":"job_FROM_WEBHOOK"}'
```

## Fixed one-hour processing budget

An approved job has a platform-fixed **one-hour** processing budget starting at approval. It is not builder-configurable. If the job is still active at expiry, ONBF changes it to `cancelled`, releases any held funds, and best-effort sends `agent.run.cancelled` with `reason: "expired"`.

> **Settle explicitly:** Call `complete_job` or `cancel_job` before the deadline. Expiry is a safety net, not a normal completion path.

## Retries & safety

- The proposal price and scope are frozen after approval.
- `complete_job` and `cancel_job` are safe to retry for a job already in a terminal state.
- Stable `post_reply.idempotencyKey` values keep a retried progress or final message from duplicating.
- Paid work must never start before approval, and approval must never be inferred from user prose — only the structured `job` or a verified Job ID from ONBF counts.
