beads.Store
operation to a user-supplied script, the same pattern the
exec session provider uses for sessions. It
makes the bead store a pluggable boundary: change one config line and Gas City
persists beads through your script instead of the default bd (Dolt-backed)
store.
Why
The defaultbd provider couples the store to a specific stack — the Go bd
CLI wrapping a Dolt SQL database. A script backend lets you point Gas City at:
- A different beads engine — for example
beads_rust(br), a SQLite + JSONL hybrid with no JVM/Dolt dependency. - Custom persistence — bead writes that also trigger S3 snapshots, git commits, or other durability strategies.
- Alternative databases — Postgres, SQLite, flat files, or any backend reachable from a CLI.
Usage
Select the provider incity.toml:
GC_BEADS overrides the config for a single command:
GC_BEADS → [beads] provider → bd (the default).
Calling Convention
The script receives the operation name as its first argument:Exit Codes
Exit code 2 is the forward-compatibility mechanism: when Gas City adds an
operation, an older script returns exit 2 and the provider treats it as a
no-op success, so scripts only implement the operations they care about.
The one exception is
ready invoked with contract arguments (such as
--include-ephemeral): there, exit 2 is surfaced as an error rather than a
silent success, so runnable ephemeral beads are never quietly hidden from the
orchestrator. A script that cannot serve those ready queries should exit 1 with
a stderr message instead.
Operations
These are the subcommands a backend implements. Bead reads return a JSON array ([] when empty, never null).
The remaining
Store methods are composed by the provider from the
subcommands above, so a script that implements this table supports them with no
extra work:
ListOpen,ListByAssignee,ListByMetadataroute tolist; the provider applies the status/assignee/metadata filter after the script returns.CloseAllis a sequence ofclosecalls;SetMetadataBatcha sequence ofset-metadatacalls (neither is atomic — design scripts to be idempotent).Txruns its callback sequentially against the same store; writes are not grouped into a single transaction.Pingrunslistto confirm the script is reachable.
ready and ephemeral tiers
ready returns open, unblocked beads. The orchestrator’s demand scans pass
--include-ephemeral, and the script must then include ephemeral run beads in
its output; Gas City applies the caller’s tier filter after parsing. Future-
deferred beads (defer_until in the future) are filtered at the store boundary,
so a script may return them and let the provider drop them.
Wire format
Beads are exchanged as JSON matching thebd --json shape:
id, status, created_at, and updated_at on create.
On create the provider defaults a missing type to task before calling the
script; on every read it normalizes a missing or empty status to open.
metadata values may be any JSON type but are coerced to strings.
A closed bead may carry close_reason, a string saying why it was closed (bd’s
close_reason field); the provider reports it as the bead’s close reason and
ignores it on a bead that is not closed. The close operation carries no
reason; Gas City stamps metadata.close_reason before closing, so a script
that records reasons can take it from there.
Preserve ephemeral, no_history, and defer_until on every read path so the
provider can keep run beads out of normal work queries. no_history=true marks
durable work that carries no version-control history: unlike ephemeral beads
it stays visible in issues-tier reads, and also appears in wisps-tier queries.
Backends without native columns for these bits may store them internally (e.g.
a private label) but must round-trip them as the matching JSON fields.
The create request is a subset (title, type, priority, labels,
parent_id, ref, needs, description, assignee, from, metadata,
ephemeral, no_history, defer_until). The update request carries only
the fields being changed (title, status, type, priority, description,
parent_id, assignee, plus labels/remove_labels and a metadata
overlay); omitted fields are left unchanged, and labels appends rather than
replaces. dep-list returns objects of { "issue_id", "depends_on_id", "type" }.
Conventions
- JSON on stdin for mutations avoids shell-quoting issues with titles,
descriptions, and labels;
set-metadatapasses its value as raw stdin bytes so it can hold newlines, quotes, or JSON. - Not found → exit 1 with
not found(orno issue found) on stderr;get,close, andreopenmap that to the platform’s not-found error. - Idempotent writes —
closeon an already-closed bead should still exit 0; batch and transaction operations are not atomic. - Return supersets —
list,children, andlist-by-labelshould return every matching bead the script can see, including ephemeral ones; the provider applies the caller’s tier, query filters, and result limit after parsing (it always passeslist-by-labela limit of0). - Three-state status — the store vocabulary is
open,in_progress, andclosed. A backend with a richer status set maps the extras onto these three.
Environment Variables
The provider sets these before exec’ing the script:
The parent environment is otherwise inherited, except that
BEADS_* and
GC_DOLT_* keys are stripped so a script cannot accidentally inherit the
default store’s backend configuration.
Writing Your Own Script
- Start from
contrib/beads-scripts/gc-beads-bras a template. - Implement the operations your backend supports; let the provider compose the rest.
- Return exit 2 for operations you do not implement (but exit 1 for
ready --include-ephemeralif you cannot serve it). - Test with
GC_BEADS=exec:./your-script gc status.
Minimal script (create / get / list)
Shipped Scripts
Seecontrib/beads-scripts/ for maintained implementations:
- gc-beads-br —
beads_rust(br) backend: SQLite + JSONL, no Dolt dependency. Dependencies:br,jq,bash. - gc-beads-k8s — runs
bdinside a “beads runner” pod viakubectl exec, connecting to Dolt running as a StatefulSet in the cluster. Dependencies:kubectl,jq,bash.
bd provider itself ships an exec script (gc-beads-bd.sh) under
the bd pack’s assets, which is how the default Dolt-backed store is driven
through the same protocol.