Skip to contents

Some agent work shouldn’t run inside the process that asked for it: a Shiny app that queues a long analysis, or a batch of tasks spread over workers. The job functions let you queue a task in one R process, run it in another, and check the result later, even after both have exited.

Deputy stores each job’s state in a directory on disk and runs it with the ordinary agent, so permissions, hooks and limits apply as usual. Your application supplies the rest: the scheduler that starts workers (callr, mirai, cron, a queue), user authentication, and the code that rebuilds the agent in the worker.

Queue a job

Write one function that builds the agent, and call it both when you queue the job and in the worker:

library(deputy)

make_agent <- function() {
  Agent$new(
    ellmer::chat("openai/gpt-6-luna"),
    system_prompt = "Summarise the report you are given.",
    agent_id = "report-agent",
    session_id = "report-session",
    permissions = permissions_readonly(),
    usage_limits = UsageLimits(max_requests = 4)
  )
}

path <- job_create(
  directory = "jobs",
  agent = make_agent(),
  task = "Summarise the quarterly report.",
  owner_id = "user-17",
  definition_revision = "report-agent-v1",
  context_revision = "q3-report",
  usage_limits = UsageLimits(max_requests = 4),
  associations = list(conversation_id = "chat-42")
)

job_create() records the task, a description of the agent, and the revisions and owner you supply, and returns the job’s directory. Store that path with the request in your own database. The revisions are labels you choose for the version of the agent’s configuration and of the input data. If either has changed by the time the job runs, the worker refuses to run it, so bump them whenever something the job depends on changes, including anything the tools’ code relies on.

job_read(path) returns the job’s status and details. It never builds an agent or calls a model, so it’s safe to use for status pages.

Run it in a worker

In the worker, job_run() rebuilds the agent with your bind function, asks your authorize function whether the job may still run, and runs it to the end:

authorize_job <- function(job) {
  current <- look_up_job_in_your_database(job$id)
  list(
    job_id = current$id,
    owner_id = current$owner_id,
    definition_revision = current$definition_revision,
    context_revision = current$context_revision
  )
}

outcome <- job_run(
  path,
  bind = function(job) make_agent(),
  authorize = authorize_job
)
outcome$status
outcome$result
outcome$usage

authorize should check your current records, not echo the job back: Deputy compares its answer with the job and refuses to run when they differ. bind returns the agent, or list(agent = agent, cleanup = function() ...) if the worker should clean up resources, such as an R session or MCP connection, after the job. job_run() blocks until the job finishes; it doesn’t start processes or schedule anything itself.

The job directory holds the task and results, so protect it like the rest of your application’s data.

Restarts, approvals and cancellation

A queued job can be run by any process, at any time after it was created. Running a finished job again just returns its stored outcome.

If a worker dies while a job is running, the job becomes "indeterminate". Deputy won’t run it again, because a tool may have done something (sent an email, written a file) before the crash. Check for those effects yourself before queueing a replacement. The job’s budget stays reserved.

A job whose agent uses durable approvals can stop in the "approval_pending" state. Continue it through job_run() with a decision, from a worker that builds the agent with the same permission callback:

outcome <- job_run(
  path,
  bind = function(job) make_agent(),
  authorize = authorize_job,
  decision = "approve"
)

Completed tool calls are kept and not repeated, and the usage from before the pause still counts.

job_cancel() asks a job to stop. It can be called from another process while the job is running; the worker notices at its next check and stops:

job_cancel(path, authorize = authorize_job, reason = "user_cancelled")

If a worker fails after producing a result but before its cleanup finishes, the job keeps the result and reports the cleanup failure separately.

Graphs of agents

A job can run the root of a graph of retained specialists. The bind function must rebuild the same agents and routes. The job remembers the graph’s usage so far, so a new worker continues with the remaining budget rather than a fresh one. Approvals inside a graph aren’t supported yet.

A job’s agents can’t have fallback_chats, and can’t use tools that run on the provider’s side, because Deputy has to record each tool call as it happens. Create and run jobs only while the agents involved are idle.