Documentation — Jobs, daemons and triggers
Concepts

Jobs, daemons and triggers

agentd has one runtime and no modes. A one-shot job and a long-lived daemon run the same loop over the same durable state; they differ only in what wakes the agent and when the process exits. This page explains how that shape is decided, which start nodes fire runs, how a daemon runs work when it starts and stops, and where A2A fits. Read it before you choose between running agentd from a scheduler and running it as a service.

One runtime, two shapes

JobDaemon
Exitswhen nothing is left to doonly when told to: a signal, a finish with exit: true, or another stop condition
Exit codethe outcome of its work0 after a clean drain
Woken byits startup work: a once start, --instruction, --prompttriggers that keep firing, an A2A listener, and agent.prompt at startup
The instructionthe task, when no workflow or prompt is giventhe policy each message is answered under
Durable statenone unless you set a store: a crash re-runs itthe local file store unless you set one

Both shapes share the reactor, the turn workers, the tool registry and the inbox. A job is a daemon that has run out of reasons to stay up.

How the shape is decided

lifecycle.run_until decides when the process stops.

run_untilShapeExits when
auto (default)a job, unless the instance has an A2A listener (a2a.listen) or a long-lived start node — then a daemonas for the shape it resolves to
idlea jobnothing is left: no runs, turns, queued or waiting work, timers or children
draineda daemonnever on its own; a drain finishes in-flight work, then exits 0

Every start kind is long-lived except once and manual. Under auto the question is asked of the live workflow set, not only the configured one: a job whose prompt tells the agent to create a schedule (workflow.create) becomes a daemon, rather than idling out from under the work it was asked to set up.

A webhook listener or a goal gets an instance the durable store by default, but does not make it a daemon: under auto it stays a job, and a goal watches the job's own work.

A job with no long-lived start exits as soon as it is idle. When an instance is forced to idle while it still has long-lived starts, lifecycle.idle_grace (default 5 seconds) is how long it waits with nothing to do before it exits.

A job's exit code comes from the outcome of the workflows its once starts ran; with several, the worst outcome wins, so a partial success is never reported as clean. The table of codes is in CLI, environment and exit codes.

A daemon ends with a drain. These begin one:

  • SIGTERM or SIGINT; a second signal forces the kill ladder;
  • a finish with exit: true;
  • the signal named in lifecycle.until_signal, when the operator or the runtime sends it (another principal's signal of that name is refused);
  • the goal watchdog's finish action, a spent lifetime token budget under lifetime_exhausted: drain (or exit, which exits 7), or an instruction source that is unavailable under unavailable: drain;
  • an operator's drain command over A2A.

A drain stops intake, lets in-flight work finish within lifecycle.drain_timeout (default 25 seconds), checkpoints and exits: 0 after a signal or an operator's drain, or the code a finish or a failure set. Work still running at the timeout is stopped and left for the next start rather than failed: its step runs again, its message is answered again, its subagent is spawned again. An exit the agent asks for — a finish with exit: true, lifetime_exhausted: exit, a store that cannot write — stops in-flight work at once and leaves it the same way, but still runs the deinit workflows below. A job drained before its work finished exits with its work's code — 3 for a run left unfinished — not 0. Supervision and containment describes how children are stopped.

A job's task, a daemon's policy

The shape also decides what the instruction is when nothing else gives the agent work: no workflow and no prompt.

  • In a job, the instruction is the task. agentd --instruction "…" expands to a workflow named main — a once start, one agent step, a finish — that runs one turn and exits with that run's code. --effective-config shows the workflow, attributed to generated (the one-shot sugar workflow).
  • In a daemon, the instruction is the policy. A daemon with no workflow runs nothing when it starts. It waits for messages and answers each one under the instruction, which every conversation turn is given.

A daemon with work to do as soon as it starts says so:

  • agent.prompt (--prompt) delivers a message into the root conversation at every start, where the agent has its full tool surface: it can answer, or set up recurring work with workflow.create. In a job, the prompt is the job.
  • A workflow with a once start runs at boot: once per state with policy: ensure, the default, or on every boot with policy: always.

lifecycle.run_until: drained on an instance that nothing can give work to — no a2a.listen, no workflow and no agent.prompt — is refused at load, exit 2: it would wait for ever. A webhook listener or a goal does not count on its own, since each only starts workflows, and nor does a workflow the agent created at runtime and the store restores: the load cannot see it, so declare in workflows the work such an instance waits for.

The shape is settled when the process starts. A reload that changes lifecycle.run_until, from the configuration or from the instruction's own :::!config, is refused with config.reload.restart_required.

Triggers

A workflow's start node decides when a run begins. A workflow may have several. These are the eleven start kinds; Workflow node reference lists every field.

Start kindFires a run
onceat startup. policy: ensure (the default) fires unless a run it started is still on record, live or finished; policy: always fires on every boot
manualonly when asked: the workflow.run tool, an A2A workflow.run command, or a workflow step
loopagain after each run ends, paced by interval, until a condition or max_iterations
scheduleon a clock: a five-field UTC cron, an every interval, or an at delay
subscribewhen an MCP resource it subscribes to changes; the runtime reads the new contents
signalwhen a named signal arrives: workflow.signal, an A2A command, a webhook's signal:
eventon a runtime event: workflow.finished, workflow.failed, human.asked, human.answered, lifecycle.shutdown
streamon each event appended to a declared stream, or on each batch of them
correlatewhen a set of related stream events has arrived within a window: a join over events
a2awhen an A2A message, or a command the start node declares, arrives for it
webhookon an inbound HTTP request to its path on the webhook listener

Limit. ensure looks at the run records the instance holds. A finished run that store.retention.runs has evicted does not count, so a once start with ensure fires again on the next boot. Keep retention off for an instance whose once work must happen exactly once, or make that work idempotent.

Every firing is an inbox event first. A trigger's firing is written to the durable inbox before the run starts, so a crash between the two replays the firing rather than losing it. Delivery is at least once — a schedule, for one, can fire twice across a crash — so make a run's effects idempotent. Start-node state — last fired, next deadline, loop iteration, debounce window, stream offset — is durable in the manifest, so a restart resumes schedules and loops where they were.

Schedules collapse missed occurrences. A schedule that came due while the process was down fires once on restart, then arms its next occurrence; it does not replay every one it missed. at: 8h is a one-shot delay that consumes itself when it fires, not a wall-clock time. cron needs the cron build feature, which the release binaries include; every needs none. For time-driven work across a fleet, an external scheduler firing a job per tick is usually the better tool; the built-in schedule is for a standalone daemon.

Under pressure, a firing is refused, not lost. While the daemon sheds work for low disk or memory, a due schedule is skipped (start.shed) and its next occurrence armed, a webhook answers 429, and a stream consumer holds its offset until the event can be accepted.

A daemon that runs a nightly report:

agent:
  name: reporter
  instruction: You write short operational reports.

intelligence:
  endpoints: https://api.openai.com/v1/chat/completions
  model: gpt-5.1
  token: "{{secret:OPENAI_API_KEY}}"

workflows:
  - name: nightly-report
    concurrency: {max_runs: 1, on_overflow: drop}
    steps:
      tick:  {kind: schedule, cron: "0 2 * * *"}    # 02:00 UTC every day
      write: {kind: agent, depends_on: [tick], instruction: "Summarise yesterday's incidents in five lines."}
      done:  {kind: finish, depends_on: [write], output: "{{steps.write.output}}"}

lifecycle:
  run_until: auto       # a schedule start makes this a daemon
  drain_timeout: 25s

Concurrency

Each workflow bounds its own live runs with concurrency. max_runs is the cap, and on_overflow says what happens to a firing past it: queue (the default) holds it in the inbox, drop discards it, replace cancels the oldest live run. scope: key counts runs per value of the workflow's key instead of per workflow, which turns the cap into a per-entity lock: one run per order, per ticket, per customer. Across all workflows, limits.max_runs (default 8) caps the live runs of the whole instance, with the same overflow handling. Author workflows has worked examples.

Acting for a person

A firing's run is agentd's own work, attributed to identity.autonomous_as: it makes no call with anyone's grant, and a delegated server refuses it (no_delegation). A workflow's act_for changes that for its schedule, loop and once starts: their firings act for the person it names, with nobody there — via: ciba, each firing approved by them on their own device, or via: standing, with their grant and nobody asked — as far as that person's identity.delegation.templates entry allows (needs the xaa build feature; Work nobody is there for).

# fragment: under workflows:
- name: weekly-report
  act_for:
    principal: {source: operator, id: alice}   # a pinned person, or {source: oidc, issuer, sub}
    via: ciba
  concurrency: {max_runs: 1, on_overflow: drop}
  steps:
    tick:   {kind: schedule, cron: "0 7 * * MON"}
    issues: {kind: mcp.tool, depends_on: [tick], server: linear, tool: list_issues, args: {}}
    done:   {kind: finish, depends_on: [issues]}
  • Where it is declared. On the workflow, not on a start node: it holds for every schedule, loop and once start the workflow has, and only the operator's own configuration may carry it. A manual start's workflow.run runs as whoever asked for it, never as the person. act_for beside a webhook, stream, correlate, subscribe, signal, event or a2a start, or beside a webhook wait, is refused at load (act_for_trigger_not_allowed): whoever can fire one could make the person's phone buzz at every firing, and drive an approved run with a payload of their own.
  • Consent is to the definition. The person consents by signing in, to the workflow as it is then. A firing whose workflow has changed since — an edit, or a url: or dir: body that changed — starts no run and asks nobody: start.unattended.suspended says why (unattended_suspended), and the next firing after they sign in again runs. So does one for a person with no usable grant, or whose template no longer allows the via. A loop that started nothing fires again after its interval, or a minute.
  • A run waiting on approval. A via: ciba firing's first delegated call waits on the person's answer: the step is suspended on its wait for up to the CIBA request's expiry (300 s by default), the person sees the request on their device with agentd's message — the instance, the workflow, the run and the tool — and the gate among their own in delegation.list. Another call the firing makes meanwhile waits on the same request. Denied or expired, the call fails ciba_denied. Give the workflow a concurrency that drops overlapping firings: at most one request per person and workflow waits at a time, and another firing may ask once that wait has closed.
  • After a restart. A restart loses the request and any approval, and the run asks the person once more — the message on their device says again — and shows that second wait as it showed the first: a step that was waiting is suspended on the same wait for the new request, a later call that finds no approval waits on a new one, the run's task is in auth-required, and the run's record names the grant again under unattended.asked. A second restart fails the call closed; a denied request is never asked again. The workflow says nothing about any of it.
  • What it may do. The work is unattended and tainted: the person's consent rules ask them and deny unanswered, a delegated tool tagged egress is refused unless their template lists it in unattended_tools, and the firing's authority ends with its run.
  • What its work starts. The authority is the firing's run's alone. A run its work starts — a workflow step, workflow.run, or a signal or message that starts one — is a firing of its own for the same person (start.unattended.carried): it acts for them only where its own workflow declares an act_for naming them that they consented to by signing in, and its delegated calls fail unattended_suspended otherwise. Give a workflow a firing calls its own act_for (a manual start beside it is enough) when it should act for the person too. A conversation turn the work starts by message acts with no one's grant.

Init and deinit

Two start kinds let a daemon run work at the edges of its own life.

  • Init. A once start with policy: always fires on every boot. Use it for self-registration: tell a third party where to reach you as soon as the process can take calls.
  • Deinit. An event start with on: lifecycle.shutdown fires when a drain begins, and the drain waits for that run, within drain_timeout, before the process exits — a drain an exit asked for too. Use it to deregister, so the third party stops calling a process that is going away. Its payload says why: {"reason": "signal"} or {"reason": "exit"}, among others.
agent:
  name: orders
  instruction: You process paid orders.

intelligence:
  endpoints: https://api.openai.com/v1/chat/completions
  model: gpt-5.1
  token: "{{secret:OPENAI_API_KEY}}"

vars:
  registry: https://hooks.example.com
  my_hook: https://orders-agent.example.com/hooks/orders

workflows:
  - name: init
    steps:
      boot:     {kind: once, policy: always}       # every boot, not once per state
      register: {kind: http, depends_on: [boot], method: POST,
                 url: "{{config.registry}}/webhooks", json: {url: "{{config.my_hook}}"}}
      done:     {kind: finish, depends_on: [register]}
  - name: deinit
    steps:
      bye:        {kind: event, on: lifecycle.shutdown}   # fires when the drain begins
      deregister: {kind: http, depends_on: [bye], method: DELETE,
                   url: "{{config.registry}}/webhooks", query: {url: "{{config.my_hook}}"}}
      done:       {kind: finish, depends_on: [deregister]}

Keep deinit workflows to fast steps that need no model — http, mcp.tool, data steps. They share drain_timeout with the work in flight, and once the timeout passes, or a second signal forces the drain, it stops everything, deinit runs included, so an agent step there competes with the shutdown it belongs to. A deinit run cut short this way is cancelled, not resumed: the next start does not run the last one's shutdown at its boot. Only lifecycle.shutdown runs are admitted during a drain; every other queued firing stays in the durable inbox for the next start.

A2A as a trigger

A daemon's external channel is A2A, and it is a trigger like any other. Set a2a.listen and the instance becomes a daemon under run_until: auto. A plain message from a caller becomes a durable conversation turn, answered as the A2A task's result. A command — a structured data part sent with the command extension — runs a registered action with no model in the path, and an a2a start node can declare its own command so that a matching message fires a run instead of a turn. Every caller is resolved to a principal and held to what that principal may do. The A2A channel explains the listener and its principals; Connect agents over A2A shows the configuration.

Next steps