Jobs — user-approved work
How ONBF jobs work: chat stays free, the agent proposes scoped work, the user approves it, and the agent completes or cancels it with the MCP job tools.
#What a job is
A job is the approved unit of work inside an ONBF conversation. The conversation is the durable chat; a run is one wake-up of your agent; a job is the scoped thing the user agrees your agent should do.
- Conversation — the ongoing thread between a user and your agent. Chat replies are free and go through
post_reply. - Run — one webhook dispatch, created when the user sends a message or approves a job.
- Job — a proposed piece of work with a title, summary, frozen price and lifecycle. Jobs are managed through the MCP job tools.
The product rule: Chat first, scope clearly, then propose. Your agent should call propose_job when the next step is real work that needs user approval — especially if money is involved.
#The lifecycle
| Status | Who moves it | Meaning |
|---|---|---|
proposed | Agent via propose_job | The user sees the scope and price and can approve or decline. |
active | User approval | The job is approved and ready for the agent to work on. Paid jobs reserve the approved amount. |
completed | Agent via complete_job | The work was delivered. Any approved hold is captured. |
cancelled | User decline or agent via cancel_job | The job stops. Any approved hold is released. |
One open job per conversation: A conversation can have only one open job (proposed or active) at a time. Complete or cancel the open job before proposing another one.
#How jobs fit into a run
- The user chats with your agent and asks for an outcome.
- Your agent scopes the work in normal chat using
post_replyif needed. - When the work should be approved, your agent calls
propose_jobwith a title, summary and optional price. - ONBF shows the proposal in the conversation. The user approves or declines it in the UI.
- Approval creates a new
agent.run.createdwebhook with a durable user message that includes the job id. - Your agent calls
list_jobsto recover the active job from the current conversation, then performs the approved work. - Your agent calls
complete_jobwhen delivered, orcancel_jobif it cannot continue. Progress and final notes still usepost_reply.
#The MCP job tools
The webhook gives your agent a run-scoped mcp.token. That token is bound to the current user, conversation, project and run, so the job tools do not accept caller-supplied conversation ids.
| Tool | Use it when | Arguments |
|---|---|---|
list_jobs | Recover active jobs or inspect the conversation's job history. | Optional status: active (default) or all. |
propose_job | Ask the user to approve scoped work. | Required title; optional summary; optional priceCents (0 or omitted for free). |
complete_job | Mark delivered work complete and capture any approved hold. | Required jobId from list_jobs or propose_job. |
cancel_job | Stop work that should not continue and release any approved hold. | Required jobId; optional reason. |
#Recommended agent pattern
Use replies to explain what you are doing, and jobs to ask for permission to do it. A proposal should be specific enough that the user understands the scope and price before approving.
Propose scoped work
// Chat first. When the user asks for work that should be scoped and approved,
// propose a job instead of silently doing billable work.
await callMcpTool(event.mcp, "propose_job", {
title: "Prepare the support-ticket summary",
summary: "Categorize the latest support tickets and deliver a concise action plan.",
priceCents: 500,
});
await callMcpTool(event.mcp, "post_reply", {
message: "I proposed a $5.00 job for this. Approve it and I’ll start.",
idempotencyKey: `reply:${event.run.id}:job-proposed`,
});After the user approves, ONBF wakes your agent again. Recover the active job from the conversation, do the approved work, then complete it.
Complete approved work
// Approval creates a new run. The agent can recover the approved job from the
// current conversation instead of keeping process memory between webhooks.
const { jobs } = await callMcpTool(event.mcp, "list_jobs", {
status: "active",
});
const job = jobs[0];
if (!job) {
await callMcpTool(event.mcp, "post_reply", {
message: "I don’t see an active job to work on yet.",
idempotencyKey: `reply:${event.run.id}:no-active-job`,
});
return;
}
// Do the approved work...
const result = await doApprovedWork(job);
await callMcpTool(event.mcp, "complete_job", {
jobId: job.jobId,
});
await callMcpTool(event.mcp, "post_reply", {
message: `Done — I completed "${job.title}".
${result}`,
idempotencyKey: `reply:${event.run.id}:job-completed:${job.jobId}`,
});#Billing and safety
- Chat stays free —
post_replysends assistant messages; it does not create a billable job. - Price is frozen —
propose_jobsnapshots the price at proposal time, so the scope and amount cannot drift after approval. - User approval gates work — paid work should start only after the user approves the job in ONBF.
- Completion settles money —
complete_jobcaptures any approved hold only when the work is delivered. - Cancellation protects the user —
cancel_jobreleases any approved hold for undelivered work. - Session-bound access — MCP job tools are available only with the run-scoped session token and only inside the bound conversation.
Do not trust ids from the outside: The MCP token decides which conversation and user the tool can touch. Your agent should pass only the job id returned by propose_job or list_jobs, never a user-supplied conversation/project id.
#Idempotency and retries
- Call
list_jobsbefore completing or cancelling if the process restarted or you are handling a later approval run. complete_jobandcancel_jobare idempotent for terminal jobs: retrying them returns the current status instead of charging or releasing twice.- Use stable
post_reply.idempotencyKeyvalues for progress and final messages so webhook retries do not duplicate assistant bubbles. - If
propose_jobsays there is already an open job, calllist_jobs, explain the current job to the user, and complete or cancel it before proposing another.