docs/workflows/gates.md
Some workflow steps can't proceed on code alone: a release needs CI to go green, a deploy needs a human sign-off, a cleanup should wait 24 hours. A gate is an issue that represents that wait. It blocks a step the same way any blocker does — the step leaves the ready frontier until the gate closes — so agents never need to poll or spin.
A gate is a bead like any other: created open, it blocks its waiters through a normal dependency edge, and the step becomes ready the moment the gate closes. Gates close in one of two ways:
bd gate resolve <gate-id> (human gates always close this
way).bd gate check — evaluates open timer and GitHub gates against
the real world and closes the ones whose condition is met.bd gate list # open gates
bd gate list --all # include closed
bd gate show <gate-id> # details and waiters
bd gate check # evaluate open gates, close satisfied ones
bd gate check --dry-run # report without closing
bd gate resolve <gate-id> # close a gate manually
| Type | Waits for | Closed by |
|---|---|---|
human | a person's decision | bd gate resolve only |
timer | a duration after gate creation | bd gate check once the timeout elapses |
gh:run | a GitHub Actions workflow to complete successfully | bd gate check (uses gh run view) |
gh:pr | a pull request to merge | bd gate check (uses gh pr view) |
bead | a bead in another rig to close | cannot be checked because multi-rig routing was removed; resolve these gates manually |
Timeouts use Go duration syntax: 30m, 1h, 24h (there is no d unit —
write 24h, not 1d).
GitHub gates use the current Git repository by default. To evaluate a PR or
workflow run in another repository, set the gate's string metadata.repo value
to OWNER/REPO or HOST/OWNER/REPO. An ad-hoc gh:run/gh:pr gate created
with bd gate create inherits a valid metadata.repo value from the issue it
blocks; human/timer/bead gates do not, since metadata.repo is
unrelated, ordinary metadata for those types. bd gate check rejects
malformed repository values instead of falling back to the current
repository.
A formula step declares a gate with a [steps.gate] block. When the formula
is instantiated, bd creates the gate issue and wires it as a blocker of that
step. The schema has five fields: type, id, await_id, timeout, and
repo.
This is the release gate from beads' own release formula — the step that waits for the GitHub release workflow:
[[steps]]
id = "wait-for-ci"
title = "Wait for release workflow"
[steps.gate]
type = "gh:run"
id = "release.yml" # which workflow to watch
timeout = "30m" # escalate if it takes longer
For a gh:run or gh:pr gate that watches another repository, set repo
the same way a metadata.repo value works for an ad-hoc gate — OWNER/REPO
or HOST/OWNER/REPO. Malformed values are rejected when the gate is checked:
[[steps]]
id = "wait-for-downstream"
title = "Wait for downstream release"
[steps.gate]
type = "gh:run"
id = "release.yml"
repo = "org/downstream-repo" # check gh:run against this repo, not the current one
repo accepts a {{var}} placeholder (e.g. repo = "{{gate_repo}}"); for a
formula persisted with bd cook --persist, the placeholder is substituted
when the proto is later poured with bd mol pour --var gate_repo=..., the
same as title, description, and await_id.
bd gate discover (auto-discovery of a gh:run gate's run ID) requires a
workflow name hint (await_id/id, not left blank) for a gate targeting
another repository — without one, the local commit/branch heuristics that
narrow a same-repo match don't apply across repos, so nothing but the
workflow name can identify the right run. A cross-repo gate discovery also
ignores the local checkout's branch unless --branch is passed explicitly;
an auto-detected local branch has no relationship to the target repo's
branches.
A human sign-off gate:
[[steps]]
id = "approve-deploy"
title = "Human approves the deploy"
[steps.gate]
type = "human"
And a cooling-off timer:
[[steps]]
id = "wait-24h"
title = "Let the release bake"
[steps.gate]
type = "timer"
timeout = "24h"
Verify what the parser actually understood before pouring — unknown keys in TOML are dropped silently:
bd formula show <formula> --json # inspect the parsed gate blocks
bd gate create attaches a gate to existing work:
# Block bd-abc until a PR merges
bd gate create --type=gh:pr --blocks bd-abc --await-id=42
# Block bd-abc until a human resolves the gate
bd gate create --type=human --blocks bd-abc --reason "Design sign-off"
# Add another waiter to an existing gate
bd gate add-waiter <gate-id> <issue-id>
Waiting on other steps is not a gate — it's a dependency. Use needs to
fan in on named steps, and waits_for when a step must wait for
dynamically-created children:
[[steps]]
id = "merge-results"
title = "Merge results"
needs = ["test-a", "test-b"] # fan-in on named steps
[[steps]]
id = "summarize"
title = "Summarize all spawned work"
waits_for = "all-children" # or "any-children", or "children-of(step-id)"
bd ready --gated # molecules where a gate just closed (ready to resume)
bd blocked # what's waiting, and on which gates
Automation patterns: run bd gate check on a schedule (cron, CI, or an
orchestrator loop) so timer and GitHub gates close without a human in the
loop; keep human gates for the decisions that should never auto-close.