Skip to main content
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.

How a gate works

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:
  • Manually — bd gate resolve <gate-id> (human gates always close this way).
  • Via bd gate check — evaluates open timer, GitHub, and bead gates against the real world and closes the ones whose condition is met.

Gate types

A bead gate also closes when its awaited bead is deleted, as long as an earlier bd gate check saw that bead in this rig; the check records the sighting in the gate’s await_seen metadata. An await ID that no check has seen, such as a typo or a bead deleted before the first check, keeps the gate pending with a diagnostic: correct the await ID or close the gate with bd gate resolve. Renaming the awaited bead with bd rename or bd rename-prefix moves the gate’s await ID and sighting to the new ID, so a rename does not count as a deletion, even to a bd gate check or bd close that read the gate before the rename; a rename that fails partway leaves the gate pending without a sighting, never resolved. 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 takes that value from --repo, or, without the flag, inherits a valid metadata.repo value from the issue it blocks; human/timer/bead gates do neither (--repo is refused on them), 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, and when a PR number does not resolve, the escalation names the repository it was looked up in. A gate for a PR in another repository whose blocked issue carries no metadata.repo needs --repo: without it the number is looked up in the current repository and escalates as not found on every check.

Known limitations: multi-rig and proxied-server topologies

Bead gates and prefix routing (routes.jsonl) do not work in every topology. Two limitations to expect, both tracked in #5861:
  • Cross-rig bead gates never resolve on their own. A <rig>:<bead-id> await value reports cannot be checked (multi-rig routing removed) and stays pending regardless of the awaited bead’s status. Close it with bd gate resolve.
  • Prefix routing cannot open a proxied-server target rig. A routed lookup into a rig that is itself in proxied-server mode fails with proxy server store needs to be uow provider, so an all-proxied shared-server topology cannot resolve routed targets at all.

Gates in formulas

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:
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:
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 every other var-bearing step field (title, description, design, acceptance_criteria, notes, assignee, labels, await_id, and the string values inside metadata). 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:
And a cooling-off timer:
Verify what the parser actually understood before pouring — unknown keys in TOML are dropped silently:

Creating gates outside formulas

bd gate create attaches a gate to existing work:

Fan-in: waiting on other steps

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:
all-children and any-children wait for the children of needs[0], so a step using them must declare needs; children-of(step-id) names the spawner itself and does not. A bare gate with neither is rejected when the formula is cooked, because it would wait for no one.

Working with gated molecules

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.