The Work Queue
For a daily walkthrough, read A morning check.
Foundry runs engineering work continuously — one-shot tasks, campaign cycles, nightly maintenance, releases, remediations. The work-item ledger is the durable record of that work as units, so you can answer "what is running, and what still needs me?" without reading raw events, worktrees on disk and remote branches.
foundry queue is the operator's view of that ledger. The overview, show and open
read it; close and cancel let an owner discharge specific items; hold,
release and foundry pacing steer what starts next (see Pacing).
What a work item is
A work item is one unit of work Foundry dispatched. It is recorded at the root of its chain — the event that set the work in motion — so a dispatch that stops early is still in the ledger rather than invisible.
Each item carries a stable id (wi_ followed by 24 hex characters), the project
it belongs to, the objective it serves, its kind, its lane, an opaque origin
string Foundry never interprets, a typed source saying what dispatched it, the
timestamps of its life, its state, a one-line reason for that state, the
workflow trace_id it runs under, and — once it settles — its settlement
fields.
The ledger lives in a single JSON file, ~/.foundry/work-items.json
(override with FOUNDRY_WORK_ITEMS_PATH). It is owned by foundryd and
authoritative: every mutation loads the file, applies the change, and saves it
through a same-directory temp-file rename.
The seven kinds
| Kind | What it is | Admitted root | Settlement |
|---|---|---|---|
task | A one-shot task | Task ExecutionRequested | Typed TaskRunCompleted |
campaign_cycle | One campaign cycle | Task ExecutionRequested | Typed TaskRunCompleted |
major_upgrade | A nightly major upgrade | Task ExecutionRequested | Typed TaskRunCompleted |
maintenance | A project maintenance run | ProjectRunStarted, ProjectMaintenanceRequested | Whole project traversal |
release | A release | ReleaseRequested | Release, pipeline and installation traversal |
remediation | Pipeline or vulnerability work | Pipeline and vulnerability roots | Whole remediation traversal |
workflow | Other engineering work, including agents | Iteration, validation, formation, digest, summary and scan roots | Whole traversal |
Task settlement retains its typed verdict. Its execution slot stays occupied until task cleanup finishes. Other kinds settle after the whole traversal. A release reached during remediation uses that root's existing ownership.
The three lanes
| Lane | Who submitted the work |
|---|---|
interactive | A person at the CLI |
campaign | A campaign derived it |
maintenance | A scheduled maintenance run dispatched it |
Origin: how the work reached Foundry
An item's origin says how the work arrived. For work an automation dispatched
that is the lane itself:
| Dispatched by | Origin |
|---|---|
foundry task | foundry task |
| A campaign cycle | campaign <name> cycle <n> |
| The nightly majors lane | nightly majors lane |
For work a person asked for by hand, the useful extra fact is who asked and
from where. foundry task and foundry campaign advance therefore record the
CLI client's hostname, and accept an optional --origin <text> note that is
recorded verbatim beside it:
foundry task acme "Add a --quiet flag" --origin "asked by Stacey in standup"
foundry campaign advance tidy-cli --origin "kicked off by hand after the fix"
The recorded origins then read:
foundry task (host workbench: asked by Stacey in standup)
campaign tidy-cli cycle 4 (host workbench: kicked off by hand after the fix)
The operator context is appended to the dispatch origin rather than replacing
it, so a campaign cycle still says which campaign and cycle it is. If the
hostname cannot be read, the origin states host unknown host rather than going
silent — "the lookup failed" is a more honest record than "no operator was
involved".
Origin is opaque. Nothing parses, validates, filters or groups on it;
foundry queue only displays it, and no dispatch is ever rejected, delayed or
altered because of what its origin says — an empty --origin is accepted and
simply adds nothing. Cycles dispatched by the automatic post-result advance
carry no operator context, because no operator issued them.
Source: what dispatched the work
Origin is text for a person. source is the typed answer to the same
question, recorded at submission and never changed, so the ledger alone can
answer "everything this campaign dispatched" or "what did the nightly start"
without reading the event log to recover a campaign from a trace.
A source is a kind from a closed set, the one ref that names the
dispatcher, and, for a campaign, the cycle number:
| Kind | ref | Recorded when |
|---|---|---|
campaign | the campaign name | a campaign advance dispatches a cycle; cycle is the cycle number, whoever asked for the advance |
sentinel | the sentinel name | a sentinel-fired chain dispatches an item: the nightly's per-project maintenance runs, its majors-lane upgrades, a remediation or release it reaches |
operator | the hostname of the CLI that asked | foundry task, foundry iterate, foundry run, foundry release, foundry pipeline, and any item a chain they start reaches |
work_item | the parent item id | an item created from another item: a queue resume child or a nightly continuation (its resumes link is unchanged) |
The kinds are a closed enum; adding one is a code change, never a free string. The source travels on the event envelope: whatever emits a root event names it there, and every event below the root inherits it verbatim (see Tracing), which is how a per-project run three hops below the nightly's root still knows which sentinel fired it. A release that follows remediation retains the root's source and execution slot.
An item recorded before the source existed has none, and so does one whose
root named none (a raw foundry emit). Every reader treats that as not
recorded, exactly as worktree_removed is handled: queue show prints no
Source: line, --json emits no source key, and a row shows -. The
free-text origin is untouched by any of this; the two sit side by side.
To list one source's items:
foundry queue --source campaign:bedrock-gated-trials-v1 # every cycle of that campaign
foundry queue --source sentinel:nightly-maintenance # what the nightly started
foundry queue open --source operator:workbench # open work asked for from that host
The filter is an exact match on kind and ref, applied by the daemon's
ListWorkItems (and by the same selection offline); an unknown kind, or a kind
with no ref, is refused. --source goes before or after open. foundry campaign show <name> lists the campaign's cycles through this same filter. The
filter also selects escalated campaigns:
campaign:<name> shows that campaign's escalation, and every other kind shows
none.
The states
An item moves through unsettled states, then settles. Settled does not mean finished: three of the settled states still hold an obligation for a person, which is why the ledger says an item settles rather than completes.
| State | Meaning | Group |
|---|---|---|
submitted | Admitted to the ledger, not yet queued | Queued |
queued | Waiting for the scheduler to start it; reason says why it waits | Queued |
held | Taken out of the scheduler's hands by queue hold until released | Queued |
running | An agent is working on it | Running |
preserved | Work is held on a durable ref for a later cycle | Open |
needs_decision | Work stopped on a question only a person can answer | Open |
failed | Work stopped on a fault | Open |
landed | The work reached trunk | Terminal |
cancelled | An operator stopped the work | Terminal |
landed and cancelled are terminal — nothing more is owed.
preserved, needs_decision and failed are open — settled, but still
carrying something for you to discharge. foundry queue open is that set, plus
every escalated campaign.
Escalated campaigns wait on you too
An escalated campaign has stopped until its owner decides, resumes, completes or
cancels it. That is as much "waiting on you" as a preserved item, but it is
campaign state, not a unit of executed work, so the ledger never records it as
an item. On 2026-10-11 two of four escalated campaigns had no open item at all,
because they escalated after their final cycle landed, and queue open did not
show them.
So foundry queue open, and the open group of the foundry queue overview,
list every escalated campaign after the open items, one row each:
Open — needs a person
wi_ccc3 beta major_upgrade maintenance sentinel:nightly-maintenance preserved 2026-09-29T03:10:00+00:00 gates red after the bump
campaign mojentic-ex-v2 mojentic-ex escalated 2026-10-11T03:12:40+00:00 campaign cycle budget exhausted (6)
next: foundry campaign show mojentic-ex-v2 (decide, resume, complete, cancel)
The row carries the campaign, its project, when it escalated and why (see
What an escalated campaign shows its owner),
then the command that prints the full reason and the owner's command lines.
The reason reads (reason not recorded) when neither the campaign nor the
event log has one.
Every escalated campaign is listed, including one that also has an open cycle
item. The two are not duplicates: the item is the work and its evidence (a
preserved ref, a failure), the campaign row is the decision and the commands
that answer it. The row names the item, as (open cycle wi_…), so they read as
one obligation. Hiding the campaign row when an item exists would hide the
reason and the commands again, which is the defect this fixes; hiding the item
would make the ledger view incomplete.
Every kind enters queued and starts only when the shared scheduler permits.
Its reason names what it waits on, or says ready. The submitted state
exists in the model, but normal admission records the queued state directly.
Queued and held items survive a daemon restart as they are: the scheduler reads them back off the ledger and picks up where it left off.
Daemon restarts settle running items as failed
A running item means "an agent is working on it", and after a daemon restart
no agent is. On every start foundryd settles each item still running as
failed, with the reason daemon restarted. This is deliberate: reporting
long-dead work as running is worse than reporting it as failed, and a failed
item is visible in foundry queue open where you can decide what to do with it.
The six lifecycle events
The ledger emits six events. They are the owner-specified exception to
Foundry's *Started/*Completed pairing rule.
| Event | Meaning |
|---|---|
work_item_submitted | A unit of work entered the ledger; for paced work, state is queued and reason says why it waits |
work_item_started | An agent started on a ledger item |
work_item_settled | A ledger item reached a settled state, with its disposition |
work_item_cancelled | An operator, or the item's own campaign, stopped a ledger item |
work_item_held | An operator took a queued item out of the scheduler's hands |
work_item_released | An operator returned a held or dependency-blocked item to the queue |
work_item_started pairs with work_item_settled rather than a
work_item_completed, because an item does not complete — it settles, into a
state that may still hold an obligation.
work_item_cancelled is emitted by foundry queue close, foundry queue cancel, and foundry campaign cancel <name> --reason … --now, which stops the
in-flight cycle outright and cancels the campaign's queued cycles with it. It
is also emitted when a campaign moves past an open cycle (see "Campaign cycles
the campaign has moved past" below). It
carries the item's id, project, kind, lane, state, reason and origin, plus the
settlement fields, and for the aborted cycle it rides the aborted cycle's
trace rather than the cancellation's — so it sits with the rest of the events
about that unit of work. Like the other five it reaches foundry watch and the
durable JSONL event log.
Commands
foundry queue [--source <kind>:<ref>] [--json] [--offline]
foundry queue show <id> [--json] [--offline]
foundry queue open [--source <kind>:<ref>] [--json] [--offline]
foundry queue hold <id> [--origin <text>]
foundry queue release <id> [--origin <text>]
foundry queue
Prints four groups on one screen, in this order:
- Running — items an agent is working on, by start time.
- Queued — items in
submitted,queuedorheld, by submission time. - Open — needs a person —
preserved,needs_decisionandfailed, newest settlement first. - Settled (last 20) — the newest 20 terminal (
landed,cancelled) items. Older terminal items are omitted; the terminal group grows forever, so the overview shows only its newest page. The cap applies to this group alone.
Each line shows the item's id, project, kind, lane, source (- when none was
recorded), state, the timestamp that placed it in its group, and the one-line
reason:
Running
wi_aaa1 alpha task interactive operator:workbench running 2026-09-30T01:00:05+00:00 running
Queued
(none)
Open — needs a person
wi_ccc3 beta major_upgrade maintenance sentinel:nightly-maintenance preserved 2026-09-29T03:10:00+00:00 gates red after the bump
Settled (last 20)
wi_ddd4 alpha release maintenance - landed 2026-09-28T02:40:00+00:00 released v1.2.0
The group order and the order within each group come from the daemon's
ListWorkItems response. The CLI groups by state; it never re-sorts. With
--source <kind>:<ref> the daemon returns only that source's items, in the
same order, and the groups show those.
foundry queue show <id>
Prints one item's full durable record, one field per line, including every
settlement field, then a blank line and the item's own work_item_* events.
An optional field the ledger never recorded produces no line at all, so you
never have to tell a recorded empty string from an unset field.
worktree_removed is the sharpest case: a recorded false prints
Worktree removed: no, while "no worktree recorded" prints nothing. Source:
follows the same rule: an item recorded before the source existed prints no
Source: line.
Id: wi_ccc3
Project: beta
Objective: bump serde to 2.0
Kind: major_upgrade
Lane: maintenance
Origin: nightly majors lane
Source: sentinel:nightly-maintenance
State: preserved
Reason: gates red after the bump; work held on a branch
Submitted: 2026-09-29T02:00:00+00:00
Started: 2026-09-29T02:00:01+00:00
Settled: 2026-09-29T03:10:00+00:00
Trace: abcdef0123456789abcdef0123456789
Verdict: remainder
Preservation ref: foundry/majors/serde
Worktree: /home/you/.foundry/worktrees/beta/x
Worktree removed: no
Events:
2026-09-29T02:00:00+00:00 work_item_submitted submitted evt_1a2b… submitted
2026-09-29T02:00:01+00:00 work_item_started running evt_3c4d… agent started
2026-09-29T03:10:00+00:00 work_item_settled preserved evt_5e6f… gates red after the bump; work held on a branch
Each event line shows when it occurred, its type, the state it left the item
in, the event id and the one-line reason. The events are read from the durable
event log (FOUNDRY_EVENTS_DIR, one YYYY-MM.jsonl file per month) through the
daemon's ListWorkItemEvents RPC, and are:
- selected by the item's id in the event payload — never by trace or project. Multiple independently queued roots can share a trace and project; each shows only its own events.
- read from every monthly file, however old, so an item from last year still shows its history rather than a false "no events".
- in chronological order — oldest first, ties in the order they were logged.
An item with no events in the log (or no event log at all) prints
(no events) under the heading rather than nothing. A line in the log that is
not valid JSON — or that holds two events run together — is skipped with a
warning in the daemon log; the item's other events are still shown. A fault
reading the log itself is an error, never an empty history.
foundry queue open
Prints only the open group — the work that still needs a person, then the escalated campaigns. This is the command to run when you want the shortest possible answer to "what is waiting on me?"
--json
All three commands take --json. The JSON form renders from the same fetched
data as the human form, so the two can never disagree about which items the
daemon returned.
foundry queue --jsonandfoundry queue open --jsonemit one object with two arrays:items, the ledger records, andescalated_campaigns, one entry per escalated campaign the view lists. Through 0.44.4 both emitted a bare array of items; a script that read.[]now reads.items[]. A campaign entry carriesname,project,status,cycles_completed,cycles_landed,max_cycles,open_escalation(absent when no reason was recorded),open_cycles(the ids of its open items, absent when none) andowner_options(control,command,effectfor each). The two arrays are separate because a campaign escalation is not a ledger record: a consumer that counts items never counts a campaign.foundry queue show <id> --jsonemits a single JSON object: the record's keys, unchanged, plus aneventsarray. Each event carriesid,event_type,occurred_at,state,reasonand, when it has one,trace_id; an item with no events has"events": [].
Optional fields are absent when unset rather than emitted as null, ""
or false, so the JSON round-trips the same facts the record carries:
{
"items": [
{
"id": "wi_ccc3",
"project": "beta",
"objective": "bump serde to 2.0",
"kind": "major_upgrade",
"lane": "maintenance",
"origin": "nightly majors lane",
"source": { "kind": "sentinel", "ref": "nightly-maintenance" },
"submitted_at": "2026-09-29T02:00:00+00:00",
"started_at": "2026-09-29T02:00:01+00:00",
"settled_at": "2026-09-29T03:10:00+00:00",
"state": "preserved",
"reason": "gates red after the bump; work held on a branch",
"trace_id": "abcdef0123456789abcdef0123456789",
"verdict": "remainder",
"preservation_ref": "foundry/majors/serde",
"worktree": "/home/you/.foundry/worktrees/beta/x",
"worktree_removed": false
}
],
"escalated_campaigns": []
}
Note what is not there: landed_commit. The item did not land, so the field
was never recorded, so it is absent. A source has the same keys the ledger
writes (kind, ref, and cycle for a campaign); an item that records none
has no source key. The items array is the fetched list unchanged — neither
grouped nor capped at 20 — so a consumer can apply its own rules.
Online versus offline
| Command | Daemon required? | Notes |
|---|---|---|
foundry queue [--source <kind>:<ref>] | Yes (or --offline) | Renders ListWorkItems; --source is its exact-match source filter. The open group adds the escalated campaigns from ListCampaigns |
foundry queue show <id> | Yes (or --offline) | Renders GetWorkItem, then ListWorkItemEvents |
foundry queue open [--source <kind>:<ref>] | Yes (or --offline) | Renders ListWorkItems, open group only, then the escalated campaigns from ListCampaigns |
Online is the default and is daemon-authoritative. All three commands render
the daemon's response directly and never read, create or mutate the client-side
ledger or events files: if FOUNDRY_WORK_ITEMS_PATH or FOUNDRY_EVENTS_DIR is
absent, the online path leaves it absent, and an existing file is left byte-for-byte untouched. If foundryd is
not listening, the command fails with a stable actionable error naming the
matching --offline recovery command. There is no silent fallback.
A NOT_FOUND from GetWorkItem surfaces as an id-not-found error and a
non-zero exit, not as an empty record.
--offline is explicit recovery for when the daemon is stopped and you
intentionally want direct file access. It reads FOUNDRY_WORK_ITEMS_PATH
(and, for show, FOUNDRY_EVENTS_DIR) directly and never contacts the daemon,
applying the same grouping order the ListWorkItems contract documents and the
same event selection and order ListWorkItemEvents documents — --offline
differs from the online path only in transport, never in reading order.
- A missing ledger file renders four empty groups and exits zero. Absence is the normal starting state, not a fault, and a read never creates the file.
- A malformed ledger file is an error: the command exits non-zero and names the parse failure.
--sourceapplies the same exact-match selection to the file the daemon applies to its store, so a filtered--offlineread lists the same items.- The overview and
openread escalated campaigns fromFOUNDRY_CAMPAIGNS_PATH, and recover a pre-record escalation reason fromFOUNDRY_EVENTS_DIR, the same wayListCampaignsdoes. A missing campaign store has no campaigns, and a read never creates it.
Close or cancel an item
foundry queue close wi_0123456789abcdef01234567 --reason "Reviewed; no further work required" --origin "owner review"
foundry queue cancel wi_0123456789abcdef01234567 --origin "withdrawn request"
close discharges an open obligation in preserved, needs_decision or
failed; it requires a nonblank --reason. cancel stops a submitted,
queued or held item and records the reason cancelled by operator, without
requiring a reason argument. Both settle exactly the requested id as cancelled.
Both require the daemon, reject --offline, and never fall back to local
writes. They record this CLI's hostname and optional --origin in
operator_action, separately from the original submission origin. The action
retains the previous state, reason and settlement timestamp. Submission
identity, objective, trace and known disposition remain intact, including
preserved refs and worktree-removal observations. No agent starts, running
workflow stops, or preserved work is disposed of by either command.
The daemon reloads the ledger under its shared write gate and saves by atomic
replacement before emitting work_item_cancelled to Watch and the durable
log. Earlier event bytes stay untouched. Unknown ids return NOT_FOUND,
blank input returns INVALID_ARGUMENT, and other states return
FAILED_PRECONDITION. Malformed ledgers also return FAILED_PRECONDITION;
read or save failures return INTERNAL and a failed save emits no cancellation.
Concurrent requests for one item can succeed only once.
Follow up on open work
foundry task <project> "Fix the review findings on wi_X's branch" --continues wi_0123456789abcdef01234567
A follow-up task often finishes work that another item left open: it builds on
that item's branch, fixes its review findings, and lands. foundry task <project> "<description>" --continues <id> (repeatable) records that lineage. Each id must
name an item in the ledger (otherwise INVALID_ARGUMENT, as for --after) that
belongs to the same project and is open, that is preserved, needs_decision
or failed (otherwise FAILED_PRECONDITION). A refused dispatch admits and
runs nothing.
--continues is lineage only. It does not change the task's base or its
objective, and it does not make the task wait: combine it with --after for
that. Use foundry queue resume instead to re-run a preserved item's own
objective from its preserved branch.
The new item records the ids in continues, shown by foundry queue show in
human and JSON output, by the queue overview as (continues <id>, …), and in
its lifecycle payloads beside resumes. When the new item settles landed
with a landing commit, each continued item that is still open settles landed
with landed_commit set and the reason superseded by <commit> via <new item id>. A work_item_settled event records it on that item's own trace, and the
guarded branch cleanup runs as for any landed item. The decision is taken on
the ledger as loaded under the shared write gate, so an item the owner already
closed or cancelled stays as it is. A follow-up that does not land, including
a complete that delivered nothing, leaves the continued items open and
unchanged.
Resume preserved work
foundry queue resume wi_0123456789abcdef01234567 --origin "finish preserved work"
foundry queue resume wi_0123456789abcdef01234567 --after wi_89abcdef0123456789abcdef --not-before 30m
Owner-directed preserved-work continuation uses foundry queue resume <id> [--origin <text>] [--after <id>]... [--not-before <RFC3339|duration>]. It
requires a live daemon and refuses --offline. Only a preserved item with a
usable preservation ref and a registered project can be resumed. Foundry
records a new task with the original objective, queued for the pacing
scheduler, that starts from the preserved local branch, remote ref or
bundle:<path> through the existing continuation path. When the preserved work does not contain current trunk, Foundry rebases it onto freshly fetched trunk before the agent starts (the preservation ref itself is not rewritten). If that rebase conflicts, the child is refused before any agent session: it settles failed with land_blocked: continuation_conflict and a reason that names the conflicting files, and the original stays preserved. --after and
--not-before carry the same meaning as on foundry task. The new record and its lifecycle payloads expose
resumes, the exact original id. Queue reads show this link in human and JSON
output. Submission identity and the parent's prior evidence remain intact;
the child records the hostname and optional origin in its resume operator
action.
The original obligation stays preserved until the linked task actually lands.
Then Foundry records its landing commit and appends work_item_settled for the
original. Failed, blocked, preserved and no-landing results leave it open.
An original cancelled by its owner stays cancelled even if its child lands.
Unknown ids return NOT_FOUND, blank inputs INVALID_ARGUMENT, ineligible
states or unusable evidence FAILED_PRECONDITION, and persistence failures
INTERNAL. A rejected admission dispatches no execution and invokes no agent.
Admission first saves a child in failed with reason resume admission incomplete; execution not dispatched, no started_at, a settled_at, and the exact resumes
and operator context. Only after both work_item_submitted and work_item_started
roots have been appended successfully does Foundry save that child as running
and dispatch it. If the initial ledger save fails, no child or lifecycle event is
created. A later lifecycle or final ledger-save failure leaves the staged failed
child; the parent's obligation and unrelated records remain unchanged. The failed
child has no preservation disposition; continue using the preserved parent's id.
An interruption during admission also leaves this non-running record.
Watch publishes each admission lifecycle root only after its write succeeds. A rejected admission may retain a successfully written submitted event, or both roots if the final ledger save fails. These are evidence of an admission attempt, not proof of execution: consult the ledger and RPC result. Earlier event bytes, including any partial failed append, remain intact; Foundry never truncates or rewrites history to undo admission. Ordinary engine roots, block outputs, scatter/gather, progress, close/cancel and restart settlement retain their existing best-effort event-persistence behaviour.
When an otherwise eligible nightly major upgrade matches a preserved ledger
item by exact registered project, package and target version, the majors lane
resumes that obligation from its preservation ref and original objective. Its
child records kind: major_upgrade, lane: maintenance, origin nightly majors lane and the exact resumes id; automation records no operator action. The
parent retains its submission identity and evidence. Only an actual child
landing settles the preserved parent landed; other outcomes leave it open,
and owner cancellation remains terminal. A selected resume admission failure
is reported and never falls back to a fresh task.
Continuations count against the existing nightly caps and run sequentially
with fresh upgrades in the existing order. Update policies, holds, security
eligibility, successful maintenance and in-flight suppression still apply.
Reviews, dry runs and interrupted-cycle summaries dispatch nothing. Without a
matching preserved ledger item, the existing fresh-task and history-suppression
rules apply. Owner queue resume remains an interactive, asynchronous task.
After a task actually lands, Foundry also checks that registered project's
other open ledger items (preserved, needs_decision or failed) that
recorded a preservation ref against the registered trunk branch, using the
registered checkout path. Three proofs, tried in this order, can settle such
an item landed:
- Ancestry: the preserved commit is reachable from trunk.
- Patch equivalence:
git cherrymatches every one of a nonempty set of preserved commits to a patch on trunk. - Content containment:
git merge-tree --write-treemerges the preserved commit into trunk without a conflict, and the merged tree is trunk's own tree. Trunk then already holds every change the branch makes relative to their merge base. This is how work that a later cycle squashed, or rebased with the same conflict resolution, is recognised when its patches no longer match.
Its landed_commit is the verified trunk commit and its reason is exactly
superseded by <commit>, whichever proof succeeded; the structured log names
the proof. The appended work_item_settled event carries
that item's exact id, project and original trace. Objective, submission origin,
resumes and prior preservation evidence remain intact.
A needs_decision or failed run keeps a preservation ref even when it made
no commits. Its branch then still points at the trunk commit it started from,
which every later trunk contains. Such an item settles only when its preserved
tip was committed after the item started, compared at whole seconds; otherwise
it stays open with a diagnostic, because its obligation is the question or the
fault, not work that can land. An open item without a preservation ref, such as
one blocked on a dependency before it ran, is not checked. Content containment
changes no ref, index or worktree; it writes the merged tree as unreferenced
loose objects, which garbage collection removes. It needs Git 2.38 or later. Once an item settles
landed, its recorded task branch locally and preservation branch locally and on
origin are eligible for best-effort deletion. Each ref needs fresh proof by
one of the three checks above against the registered trunk. Unowned refs, bundles,
branches checked out in any worktree, and refs also owned by unlanded items in
any registered project sharing the repository are kept. Ownership compares
exact refs across repository slugs and shared Git common directories, including
separate clones and linked worktrees. Deletion uses the proved commit as a guard against concurrent ref changes.
The disposition and settlement event retain each ref name, observed commit,
deletion result and failure reason. A failed deletion leaves the item landed.
Preservation evidence for unlanded work is never deleted.
Cleanup decides eligibility under the ledger write gate, releases it for all
Git operations, then reloads under the gate to record outcomes. Concurrent
ledger updates are retained; a changed state or ref ownership is never
overwritten by stale cleanup evidence.
This check runs only after actual task landing. Failed, blocked, preserved and no-landing task results do not trigger it. It checks only open items with a preservation ref in the same registered project; owner-cancelled items remain cancelled. A repeated terminal does not append another settlement. The ledger is reloaded under the shared write gate before saving, so concurrent owner actions and unrelated updates are retained. A failed save emits no settlement.
Missing refs or objects, failed Git commands, preserved changes that trunk
does not hold (unmatched patches whose merge into trunk conflicts or would
still change trunk's tree), and ambiguous evidence leave the item open with an
explicit supersession diagnostic in the settlement block's result and
structured log. When patch equivalence and content containment both fail, the
diagnostic gives each reason. Empty Git output is never proof. Local branch refs and
commit ids are checked directly. Branch names are also checked by exact name
on origin when configured, without fetching; conflicting local and remote
heads remain unresolved. A remote-only head must already have locally available
objects. A bundle must advertise exactly one head and that commit must already be
locally available. Patch comparison and content containment require complete,
shared history; patch comparison rejects merge commits that git cherry cannot
account for. Queue show and JSON
expose the persisted commit and reason through the daemon's read path.
Scheduled invocation and the reporting reconciler are not delivered by this
landing-triggered check.
What queue does not do
queue close, queue cancel, queue hold and queue release do not stop
running work. To stop an in-flight campaign cycle use foundry campaign cancel <name> --reason … --now.
Pacing
Pacing is the stage between admission and execution. Every engineering workflow enters the same durable queue before any block runs. This includes manual and nightly maintenance, tasks, campaign formation and cycles, iteration, validation, releases, remediation, supply-chain scans, and agents that write digests. Raw emitted events pass through the same boundary.
The nightly records all project roots before it waits for their results. Queued or held maintenance remains outstanding in its cycle's gather. A restart rejoins these roots. Cancellation produces a failed project completion, so the cycle reports what stopped instead of reporting success.
One admitted root owns its downstream workflow through release installation and task cleanup. Those continuations use the existing slot. Independent tasks and campaign advances enter the queue before their parent releases its slot; they cannot deadlock by waiting inside that parent.
System digests and maintenance summaries consume host capacity without locking project repositories. They share a system resource lock so concurrent digests cannot overwrite shared output files or watermarks. A supply-chain scan with auto-fix enabled reserves every active repository it can change. The current scanner handles those projects sequentially and writes one aggregate digest. This conservative reservation can wait behind a busy repository; it never skips that repository. With auto-fix disabled, the scan consumes only a system slot.
Dry runs use the same queue because observers can run agents or checks.
Mutators remain simulated, and the settlement reason says
dry-run completed; mutations simulated. Lightweight status,
work reconciliation and operator controls remain immediate. Submission and
start lifecycle writes must succeed before execution begins. The scheduler
validates the complete persisted root; a payload with an item id cannot grant
execution ownership.
The rules
The scheduler evaluates every queued item on each tick, in this order, and starts the first items the rules let through:
- One mutating item per repository. A repository is the registered GitHub slug, or the registered checkout path when there is no slug, so two projects on one slug share a repository. An admitted workflow occupies it until all its side effects finish.
- Host running cap. At most
max_runningitems (default 2) run on the host at once, whatever their lane. not_beforehas passed.- Every
depends_onitem has settledlanded. - The item's lane is not paused.
Candidates are considered interactive lane first, then oldest submission first. A start earlier in a tick counts against the repository and host rules for the items after it.
An item can reserve the repositories of other projects. A supply-chain scan with auto-fix waits until all its reserved repositories are free. An initiative assessment reserves its repositories only if they are free.
For an initiative assessment, rule 1 applies to its own repository only. When the other rules let it start, it holds all reserved repositories if all are free. If one of them is busy, it starts with its own repository only. Its work item then shows no reserved repositories.
A dependency that can never land is not worth waiting for. When a dependency
settles any way other than landed, or names an id that is not in the ledger,
the dependent settles needs_decision with the reason waits on <id>, which settled <state> (or which is not in the ledger). foundry queue release <id>
returns it to the queue: the settled dependencies are dropped from depends_on
and the item runs regardless of them.
The reasons
A queued item's reason is the scheduler's current verdict, refreshed on every
tick and visible on foundry queue rows, queue show, pacing show and in
--json:
| Reason | Meaning |
|---|---|
repository busy: wi_… | That running item occupies the repository |
host at capacity N/N | The host running cap is reached |
not before <time> | Its not_before has not passed |
waits on wi_… | That item has not yet settled landed |
lane paused | Its lane is paused |
ready | Nothing holds it; the next tick starts it |
held by operator | queue hold took it out of the scheduler's hands |
Scheduling a task
foundry task <project> "<description>" --after wi_a --after wi_b --not-before 2h
foundry queue resume wi_c --after wi_a --not-before 2026-10-11T09:00:00Z
--after <id> (repeatable) adds a dependency; every named id must already be
in the ledger or the dispatch is refused with INVALID_ARGUMENT. --not-before
takes an RFC 3339 timestamp or a duration from now: 90s, 30m, 2h, 1d.
foundry task waits for its own run exactly as before; the wait now includes
the time the item spends queued.
Hold and release
foundry queue hold wi_0123456789abcdef01234567 --origin "wait for the review"
foundry queue release wi_0123456789abcdef01234567
hold takes a queued item out of the scheduler's hands; it sits held with
the reason held by operator until released. release returns a held item, or
one in needs_decision over a dependency, to queued with the scheduler's
current verdict as its reason. Both require the daemon, reject --offline,
record this CLI's hostname and optional --origin in operator_action, and
emit work_item_held / work_item_released on Watch and in the durable log.
queue cancel also accepts a held item.
Pausing lanes
foundry pacing show [--json] [--offline]
foundry pacing pause [--lane interactive|campaign|maintenance|all] [--origin <text>]
foundry pacing resume [--lane interactive|campaign|maintenance|all] [--origin <text>]
foundry pacing drain [--timeout <duration>] [--origin <text>]
pacing show prints the limits in force, every running item with the
repository it occupies, every waiting item with its reason in the scheduler's
priority order, and the paused lanes. A waiting item that reserves
repositories only if free shows them as (and <repositories> if free).
--offline reads the ledger and the pacing files directly when the daemon is
stopped.
pacing pause stops new starts in a lane (default all); running items are
untouched. pacing resume allows starts again, and the next tick starts the
next eligible item. Both emit pacing_paused / pacing_resumed for the
project system, durable and on Watch. The pause state is persisted through an
atomic rename and is in force after a daemon restart.
pacing drain pauses every lane, then waits until nothing is running, printing
each item as it settles, and exits 0 when the daemon is idle. With --timeout
it gives up after that long and exits 1 naming what still runs. It does not
stop the daemon and does not resume the lanes afterwards: run pacing resume
when you are ready for work to start again.
The two files
| File | Owner | Shape |
|---|---|---|
~/.foundry/pacing.json (FOUNDRY_PACING_PATH) | You | {"max_running": 2}; a missing file means the defaults |
~/.foundry/pacing-state.json (FOUNDRY_PACING_STATE_PATH) | foundryd | {"version": 1, "paused": ["campaign"]}; written by pause and resume |
A malformed pacing.json falls back to the defaults with a warning. A
malformed state file fails closed: every lane is treated as paused until the
file is repaired or rewritten by a pacing resume.
What pacing does not do
Per-provider caps, starts-per-hour limits, provider-health gating and dashboard
controls are out of scope. pacing drain does not refuse a daemon stop; it only
tells you when a stop would interrupt nothing.
What campaign cancel --now does to the cycle's item
A --now cancellation kills the in-flight cycle, so the TaskRunCompleted that
normally settles its item never arrives. Foundry therefore settles that item
itself:
- The
runningitem carrying the aborted run's trace settlescancelled, with your--reasontext as its reason and the settlement time recorded. It moves out ofRunningand into theSettled (last 20)group;cancelledis terminal, so it never shows up underqueue open. - Its disposition records the cycle's worktree and whether that worktree is gone,
observed after disposal ran — plus the branch (or
bundle:path) the work was preserved on. With--discard-workthere is no preservation ref, because the work was thrown away. A cycle that had built no worktree records no disposition at all. - Correlation is by trace alone, exactly as a normal settlement: if no running item carries the aborted run's trace, nothing is settled. Foundry never falls back to "the project's newest running item", because that would report an unrelated concurrent run as cancelled.
- A graceful cancel (no
--now) does not stop the in-flight cycle: it finishes on its own and then settlescancelled, as described in the next section. A--nowcancel with nothing in flight, and cancelling an already-cancelled campaign, both change no item either.
The ledger is bookkeeping beside the cancellation, never a precondition for it:
if the ledger cannot be read or written, the campaign is still cancelled and the
CampaignCancelled event is still emitted — the fault is logged, and the item is
left for the restart sweep.
Campaign cycles the campaign has moved past
A campaign forms each cycle from the previous cycle's preserved ref, or from
trunk. An earlier cycle left preserved, needs_decision or failed is
therefore not owed by a person: the campaign carried it, and the campaign's own
escalation is what reaches a person. Foundry settles such cycles itself, as
cancelled, without an operator_action, keeping their preservation ref and
worktree fields:
- When the scheduler starts a later cycle of the same campaign, every open
cycle with a lower cycle number settles with the reason
carried forward by campaign <name> cycle <n> (<item id>). This happens before the new cycle's agent runs. The newest cycle submitted carries; one that was itself cancelled carries nothing. An escalated (needs_decision) cycle is the live question, so it stays open until the campaign advances again or ends. - When the campaign completes (formation
doneorfoundry campaign complete) or is cancelled, its remaining open cycles settle with the reasoncampaign <name> completedorcampaign <name> cancelled, followed by: <reason>when one was recorded. - A graceful cancel lets the in-flight cycle finish after
CampaignCancelled. When that cycle settles open, its ownwork_item_settledfinds the campaign already cancelled in the campaign store, and the cycle settlescancelledthen. It is not stranded open. - The campaign's formation runs (
campaign_advance_requested), which durable admission records asworkflowitems with the campaign's source, follow the same rules. An open formation run settles with the carried-forward reason when a cycle submitted after it starts, and with the terminal reason when the campaign completes or is cancelled. A formation run never carries other items.
Each settlement emits work_item_cancelled on that item's own trace. Only
campaign_cycle items whose typed source names the campaign and a cycle, and
workflow items whose typed source names the campaign, are touched. An item
recorded before the typed source existed, another campaign's item, a
non-campaign item and a settled item never change. The decision is taken on the
ledger as loaded under the shared write gate, in the same pass that writes it,
so an owner action that landed first wins. The Settle Carried Campaign Work
block runs on every campaign cycle's and formation run's work_item_submitted,
work_item_started and work_item_settled, and on campaign_completed and
campaign_cancelled; each trigger decides from the current ledger, so a
repeated or reordered trigger changes nothing more. Durable admission persists
and broadcasts work_item_submitted without routing it to blocks, so in the
daemon the carry happens at work_item_started.
Work reconciliation
The work-reconciler sentinel runs at 30 */3 * * *, or use
foundry queue reconcile against the daemon to run it now and print that
invocation’s report. It reports exact inventory identities and conservatively
settles verified preserved obligations without deleting their evidence. Orphan,
broken and unresolved findings, including inspection errors, reach the ops
anomaly path even below the normal pressure threshold. See the
work reconciler guide for proof and failure semantics.
Worktree housekeeping after validation
Successful validation runs Cleanup Branches, including during nightly maintenance
and foundry validate. It prunes Git metadata for directories that no longer
exist. An existing secondary worktree is removed only when it is under that
project's Foundry worktree root (FOUNDRY_WORKTREES_DIR, default
~/.foundry/worktrees), has no submitted, queued or running work-item owner or
active campaign cycle, has no uncommitted files (including ignored files), and
has no commits absent from every remote-tracking ref. Cleanup uses ordinary
git worktree remove, without force.
Every retained worktree is named in the validation housekeeping summary and
structured log, with its reason: outside Foundry ownership, owned by live work,
or holding unpreserved work. A failed safety check also retains the worktree and
reports the error. Cleanup reloads the daemon-owned ledger for each deletion and
releases its write gate and campaign store lock before every Git await. Ownership
is checked again immediately before each individual removal, after inspecting
worktree contents. A task is admitted on ExecutionRequested, before workspace
creation on PlanCompleted; the worktree path is recorded after git worktree add. Until that path is known, the live dispatch conservatively protects every
candidate for its project. Creation refuses an existing path, so a later admission
cannot adopt the existing worktree that cleanup has selected for removal.
Merged branches still use git branch -d. Branches used by live work or recorded
preservation refs are retained. This housekeeping does not dispose of the
workspace of a task undergoing review; task finalization remains responsible
for its own workspace after landing or preservation.
Initiative work
An initiative groups campaigns under a shared outcome.
Its cycles carry source: initiative:<name> while retaining the member's
campaign and campaign_cycle in their task payload. Initiative steering and
standalone dispatches use the same source filter. Their typed root and member
identities distinguish them from campaign cycles.
foundry queue --source initiative:shared-contract
foundry queue open --source initiative:shared-contract
The open queue lists an escalated initiative once and suppresses its member
campaign escalation rows. Open ledger items retain their own rows.
foundry initiative show <name> lists each shared question, its applicable
members and the command that answers it.
A resumed initiative member receives the current brief path and content hash, stage providers and bounded findings. Its diagnosis remains available until work that saw the finding lands.