* docs: re-review and bump the three 90-day-stale freshness markers azure-devops.md, json-schema.md, and otel-data-model.md all carried Last reviewed: 2026-05-08 — exactly 91 days old today, so the doc-flags freshness gate went red on every branch at once. Each doc was genuinely re-reviewed against its Freshness source paths on main; drift found and fixed: - azure-devops.md: proxied-server mode note (ado sync/status/projects hard-error there); --parent and --project flags documented; duplicate --issues row dropped; real ado.org error text quoted; ado.pat's yaml-only secret storage noted. Source bug found during review filed separately: the cmd-level config reader cannot see a yaml-only PAT, so the documented Quick Start fails on main (bd-wvf77). - json-schema.md: bd show/close/update --json return top-level ARRAYS (contract-test-pinned); jq recipes fixed accordingly; envelope pagination key documented; show's always-present revision field and import's unchanged counter added; error envelope (optional code/hint, envelope-mode shape, stdout paths, exit 1) corrected; deprecation notice's tty-only condition noted. - otel-data-model.md: hard opt-in via BD_OTEL_ENABLED + standard OTel SDK env vars (BD_OTEL_* legacy); console-only trace export; dolt span attrs (shared doltSpanAttrs, dolt.remote, dolt.force_push, dolt.conflicts, dolt.merge_with_strategy); facade-wave storage role spans; seven new Dolt metrics; AI attrs (bd.ai.duration_ms, 4-attempt retry); appendix re-pinned to current main. scripts/check-doc-freshness.sh passes locally. * docs(otel): complete claim_verify_lost op label enumeration; qualify retry policy as compact-only Review findings on #5394: op also takes guarded-update (claim_verify.go:113) and ready-claim (issues.go:439); the retry-policy sentence describes compact's loop, find_duplicates makes one unretried call.
8.6 KiB
title, description
| title | description |
|---|---|
| JSON Output Schema Contract | The stable JSON output contract for bd --json commands, covering the schema_version envelope, per-command fields, and consumer guidelines. |
Last reviewed: 2026-08-07
Freshness source: cmd/bd/output.go, cmd/bd/errors.go, and
cmd/bd/protocol/json_contract_test.go.
All bd commands that support --json output can wrap their response in
a uniform envelope by setting BD_JSON_ENVELOPE=1. This will become the
default format in v2.0.
Migration Guide
Opt in to the envelope format
export BD_JSON_ENVELOPE=1
Envelope format (BD_JSON_ENVELOPE=1, default in v2.0)
Every --json command wraps output as:
{"schema_version": 1, "data": <original-payload>}
The original payload is untouched inside .data — no type corruption,
no field injection. Works identically for objects, arrays, and maps.
When a --limit-truncated listing runs in envelope mode (currently wired
for bd ready), the envelope also carries a pagination key:
{"schema_version": 1, "data": [...], "pagination": {"returned": 10, "total": 42, "truncated": true}}
total is omitted when unknown; the whole pagination key is absent when
the result was not truncated. Legacy mode keeps the stderr text hint instead.
Updating consumers
# Before (legacy):
bd list --json | jq '.[0].id'
bd show beads-abc --json | jq '.[0].title'
# After (envelope):
bd list --json | jq '.data[0].id'
bd show beads-abc --json | jq '.data[0].title'
# Version check (object commands, e.g. create):
bd create "Example" --json | jq '.schema_version'
Timeline
- Current release: Legacy format is default. Set
BD_JSON_ENVELOPE=1to opt in. A deprecation notice is printed to stderr when--jsonis used without the env var — but only when stderr is a terminal, and at most once per invocation, so scripts capturing stderr will not see it. - v2.0: Envelope becomes the default.
BD_JSON_ENVELOPE=0available as temporary escape hatch for one release cycle.
Schema Version
Current version: 1
The schema_version field is an integer that increments when:
- Fields are added, renamed, or removed
- Output structure changes (e.g., nesting depth)
- Field types change (e.g., string to integer)
Additive changes (new optional fields) do NOT bump the version.
Output Formats
Envelope mode (BD_JSON_ENVELOPE=1)
All commands emit a uniform envelope:
{
"schema_version": 1,
"data": {
"id": "beads-abc",
"title": "Example issue",
"status": "open"
}
}
Arrays are wrapped the same way:
{
"schema_version": 1,
"data": [
{"id": "beads-abc", "title": "First"},
{"id": "beads-def", "title": "Second"}
]
}
Legacy mode (default, until v2.0)
Object commands (create, ping, etc.)
Commands that return a single result emit a JSON object with
schema_version as a top-level field alongside the data:
{
"schema_version": 1,
"id": "beads-abc",
"title": "Example issue",
"status": "open",
"priority": 1,
"issue_type": "task",
"created_at": "2026-04-20T12:00:00Z"
}
List commands (list, ready, search, stale, show, close, update, etc.)
Commands that return one or more issues emit a raw JSON array — including
show, close, and update, which return one element per requested ID.
Array output carries no top-level schema_version field:
[
{"id": "beads-abc", "title": "First", ...},
{"id": "beads-def", "title": "Second", ...}
]
Error output
Errors with --json active emit JSON — most error paths write it to stderr,
though some command-result error paths emit the same shape to stdout:
{
"schema_version": 1,
"error": "issue not found: beads-xyz",
"code": "not_found"
}
code and hint (a remediation suggestion) are both optional — only
error and schema_version are always present. In envelope mode
(BD_JSON_ENVELOPE=1) the error payload moves inside the envelope:
{"schema_version": 1, "data": {"error": ..., "code": ..., "hint": ...}}.
JSON-mode errors exit with code 1.
Field Contracts by Command
bd list --json
Required fields per item:
id(string): Issue ID (e.g., "beads-abc")title(string): Issue titlestatus(string): open, in_progress, closed, deferredpriority(number): 0-4issue_type(string): bug, feature, task, epic, chorecreated_at(string): RFC3339 timestamp
Optional fields:
description,owner,updated_at,closed_atlabels(string[]): Attached labelsdependencies(object[]): Dependency recordsdependency_count,dependent_count,comment_count(number)parent(string|null): Parent issue ID
bd ready --json
Same schema as bd list --json. Items are filtered to unblocked issues only.
Each item includes dependency_count, dependent_count, comment_count,
and optional parent fields. In envelope mode a --limit-truncated result
adds the envelope-level pagination key (see the envelope section above).
bd blocked --json
Returns issues that are blocked by unresolved dependencies. Each item includes all standard issue fields plus:
blocked_by_count(number): Number of blocking dependenciesblocked_by(string[]): IDs of blocking issues
bd show --json
Returns a top-level JSON array with one element per requested ID; items do
not carry schema_version (this shape is pinned by a contract test — a
change here is a breaking wire change). Same required fields as list
items, plus:
description(string)acceptance_criteria(string)revision(number): guarded-write optimistic-concurrency token; always present, including a legacy0dependencies(object[]): Full dependency recordscomments(object[]): Comment thread — present only with--include-comments; the default response returnscomment_countonly (count-only, be-ijck6q)comments_omitted(boolean, optional):trueonly whencomment_countis nonzero andcommentswas left out of the response (no--include-comments). Absent when comments were included or when there are none to omit (ga-clgh)
import --json
Returns a summary object when --json is active:
source(string): File path or "stdin"created(number): Issues createdupdated(number): Existing issues updatedunchanged(number, optional): Rows identical to local state, untouchedskipped(number): Issues skipped (stale rows + dedup)dedup_skipped(number): Issues skipped by--deduptitle matchmemories(number): Memory records importedids(string[]): IDs of created issuesupdated_issues(object[]): Per-issue summary of what an update changedtie_kept_local_ids(string[]): Equal-updated_atrows where local state wonstale_skipped_ids(string[]): Rows older than the local issue, skippedskipped_dependencies(string[]): Dependency edges whose target id was absentdry_run(boolean): Whether--dry-runwas active
bd export --json
Outputs JSONL (one JSON object per line), not wrapped in an envelope.
Each line is a self-contained issue or memory record, discriminated by
_type ("issue" / "memory"). Export lines do not carry
schema_version — that field belongs to the --json command envelope,
not to the interchange stream. The interchange's own version marker is the
optional _schema header record ({"_schema":"beads-jsonl/1"}), which
readers skip.
Issue records carry an optional wisp_plane boolean: the explicit
wisps-plane marker. Export stamps it on rows that live in the wisps table
when the row flags alone cannot prove the plane (a no_history: true record
is otherwise ambiguous — an unpromoted no-history wisp and a promoted one
look identical). Import routes the storage plane by this marker, never by
no_history: marker absent means the durable issues table. The marker is a
fresh key rather than a reuse of the legacy wisp boolean so that older
readers, which do not know it, degrade to flag routing instead of importing
marked rows as ephemeral (purge-eligible and export-excluded). The v0.35–
v0.37 wisp key — those streams' spelling of ephemeral — is still honored
as a read-side legacy alias.
Consumer Guidelines
-
Check
schema_versionon object output. If the version is higher than expected, log a warning but attempt to parse anyway (additive changes are backward-compatible). -
For list commands, parse the output as a JSON array directly.
-
Ignore unknown fields. New fields may be added without bumping the schema version.
-
Use
--jsonflag, not--format json. The--jsonflag is the stable contract;--formatis for human-readable variants.