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$usageauthorize 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.