Machine Mode and Diagnostics¶
Machine mode contract¶
Use global --json for machine-consumable output, or --yaml for the same document as YAML.
The two differ only in the encoding: the members, the exit code and the line on stderr are
the same, and passing both is a validation error.
Envelope shape:
{
"data": {},
"meta": {
"bbVersion": "v6.1.0"
}
}
data holds command-specific payloads. meta.bbVersion reports which binary produced the
document -- provenance for stored output, not a compatibility switch -- and meta.command
names the command that wrote it, so a document held on its own says which --describe
describes it.
The flags choose the member, and nothing that happens during the run changes it: data, or
error when the run fails; under --dry-run, preview, a verdict on the real run (see
Dry-Run Planning); under --describe, description, what the command
returns (see JSON Schemas).
There is no contract version. Adding a field to data is additive; removing or renaming one,
changing its type, or changing whether it can be null is a breaking change that cuts a new
major release (ADR-064). Pin the
binary version to pin the contract.
Failure envelope¶
When a command fails while --json is set, stdout carries an error object where data would be:
{
"error": {
"kind": "validation",
"message": "no Bitbucket host configured: set BITBUCKET_URL or run 'bb auth login <host>'",
"exitCode": 2
},
"meta": {
"bbVersion": "v6.1.0"
}
}
Which key is present tells you the outcome: data on success, error on failure. Never both. This stays unambiguous for a command whose successful data is legitimately null.
kind and exitCode come from the taxonomy below, so you can branch on either without parsing message. exitCode always matches the process exit status.
The human-readable line still goes to stderr, exactly as it does without --json.
This applies to usage errors too — an unknown flag or command produces an envelope, not just a bare string:
bb --json repo list --nonexistent-flag
The failure envelope has the same shape for every command, so there is one schema for it rather than one per command.
Dry-run envelope¶
Under --dry-run, stdout carries a preview, a verdict on the real run, where data would
be. bb --json pr merge 42 --dry-run, for a pull request that is already merged:
{
"preview": {
"tier": "preconditions-checked",
"effects": [
{
"action": "update",
"target": { "id": "42", "repository": "PROJ/app" },
"outcome": "would-fail",
"reasons": ["pull request is already merged"]
}
],
"error": { "kind": "conflict", "message": "pull request is already merged", "exitCode": 5 }
},
"meta": {
"command": "pr merge",
"bbVersion": "v6.1.0"
}
}
tieris the weakest check behind the verdict:server-validated,preconditions-checkedorpredicted.- Each effect is one change, with an
outcomeofwould-apply,no-oporwould-failand thereasonsfor it. erroris present exactly when the real run would fail, and is what it would fail with.- A command that only reads runs as usual, and its data is in
preview.data.
In machine mode a verdict exits 0, whatever it says; without --json or --yaml, one that
finds the run would fail exits with the code the real run would. A top-level error under
--dry-run means no verdict was reached: 10 when Bitbucket did not answer, 12 when
interrupted, 1 for a bug in bb.
Dry-Run Planning says what each command checks.
Describe envelope¶
Under --describe, stdout carries a description of what the command returns, and nothing
runs. bb --json pr merge --describe, with its two schemas elided:
{
"description": {
"run": { "outputSchema": "…" },
"dryRun": {
"behaviour": "verifies",
"tier": "preconditions-checked",
"outputSchema": "…"
}
},
"meta": {
"command": "pr merge",
"bbVersion": "v6.1.0"
}
}
run.outputSchemais the JSON Schema of the whole document a run writes,dataandmetaorerrorandmeta, so a document validates against it as it comes.dryRunis what--dry-rundoes for the command, at which tier, and the schema of the document it writes.- A group, or
bbitself, answers withcommands: each command beneath it, with what--dry-rundoes for it. - A path that names no command answers with
description.error, and exits0.
Without --json or --yaml, --describe prints the same for a person: an outline of the
fields. JSON Schemas has the members in full.
Diagnostics behavior¶
- Diagnostics are emitted to
stderrto preservestdoutcontracts. - Use
--log-format jsonlfor machine-filterable diagnostics. - Use
--log-levelto tune verbosity (error,warn,info,debug). - Sensitive values are redacted from diagnostic output.
Example:
bb --json --log-level warn --log-format jsonl auth status 2> diagnostics.jsonl
Recommended scripting pattern¶
- Use
--jsonand parse only thedatapayload needed for automation. - Branch on the
errorkey, or on the exit code, before readingdata. - Keep diagnostics in separate stderr capture.
- Validate the document against
description.run.outputSchemafrombb <command> --describe --jsonwhen integrating with CI.
if output=$(bb --json pr get 42 2>/dev/null); then
echo "$output" | jq -r '.data.title'
else
echo "$output" | jq -r '"\(.error.kind): \(.error.message)"'
fi
Listings that stop at --limit¶
A command that lists something returns at most --limit results, and says whether
it reached that limit. bb --json repo list --limit 1, on a server holding more than
one repository:
{
"data": [
{ "projectKey": "PAY", "slug": "payments", "name": "Payments", "public": false }
],
"meta": { "bbVersion": "v6.1.0", "limitReached": true }
}
limitReached: true means there may be more: raise --limit, or pass --all. It is
present, true or false, on every command that takes --limit, and absent elsewhere.
Text output gives the same answer as a line on stderr, so a pipeline counting rows still
counts rows, and the MCP list tools return it as limit_reached.
Responses that are not text¶
bb api returns whatever the endpoint answers, and a JSON string cannot carry
arbitrary bytes. Under --json a response body that is not text goes into data
as base64, and meta says so: meta.encoding is base64 and meta.contentType
names the body's type. JSON and other text arrive as before, with no
meta.encoding. A body larger than 64 MiB is refused under --json; without it,
bb api writes the bytes as they arrive. An answer with no body, such as a 204
to a DELETE, is data: null.
Error kinds and exit codes¶
Command failures use deterministic exit codes by error kind.
validation-> exit code2(includes unknown flags and commands)authenticationorauthorization-> exit code3not_found-> exit code4conflict-> exit code5transient-> exit code10not_implemented-> exit code11cancelled-> exit code12(interrupted; not something to retry automatically)unknown_outcome-> exit code13(the request was sent and whether it was applied is unknown)unsupported-> exit code14(the Bitbucket instance's version cannot do it; a newer one can)permanentandinternal(or unknown) -> exit code1
unknown_outcome is the one worth wiring into a script deliberately. It means bb
cannot say whether the server applied the request: a mutation bb does not send twice,
such as a POST, whose connection was lost, timed out or was interrupted after it was
sent, that a gateway answered with 502 or 504, or that Bitbucket answered with 500
or with an error it raised while writing the answer. Retrying it may repeat work that
already happened, so the answer is to check the state and then decide. That is why it
sits outside transient, which is the code a retry loop should key on. A GET, PUT
or DELETE in the same position is idempotent: bb sends it again itself, and reports
transient when every attempt fails, or cancelled when it was interrupted.
One command reports what it found through the exit status as well as what happened to
it: bb pr checks (and bb pr build status), as gh pr checks does. Without --json
it exits 1 when a build failed and 8 when none failed and one is still running or has
no result. Neither is a failure of bb's: the output is complete, and a line on stderr counts
the builds. With --json it exits 0 and each build's state is in data
(ADR-091).
bb webhook create and bb project webhook create make that check themselves: when the
webhook they asked for is there, they report it and exit 0. So does running any webhook
create again, with created false in data to tell it from a webhook just made:
{
"data": {
"status": "ok",
"repository": { "projectKey": "PROJ", "slug": "repo" },
"webhook": {
"id": 12,
"name": "ci",
"url": "https://ci.example.com/hooks/bitbucket",
"active": true,
"events": ["repo:refs_changed"],
"sslVerificationRequired": true,
"scopeType": "repository",
"secretConfigured": false
},
"created": false
},
"meta": { "command": "webhook create", "bbVersion": "v6.1.0" }
}
A webhook with the same name and URL and other settings is a conflict, which names it and
what differs.
A failure retrying cannot fix is permanent: a rejected TLS certificate, a host that
does not resolve. bb does not retry those itself either.
Handles on the failure envelope¶
A failure may carry an optional error.details object: a flat map of strings naming what you
need to act on it. It is absent when there is nothing to carry. Read handles from
error.details, not by parsing error.message.
A failure Bitbucket answered carries upstreamStatus, the HTTP status, and
upstreamException when Bitbucket named its exception. The exception name is the stable
part: branch on it rather than on the wording of error.message, which Bitbucket
rewords between releases.
For such a failure, error.message is Bitbucket's own message, redacted and cut to 300
characters unless --full-error-body is passed.
Example failure behavior:
bb tag list --repo BADFORMAT
echo $?
validation: invalid repository selector (expected PROJECT/slug)
2
Under --json, the same failure additionally produces the envelope shown above on stdout.
Malformed invocations¶
An unknown flag, unknown command, bad flag value or wrong argument count is the caller's mistake,
and reports validation with exit code 2 — the same as any other input the CLI rejects:
bb --json repo list --nonexistent-flag
{
"error": {
"kind": "validation",
"message": "unknown flag: --nonexistent-flag",
"exitCode": 2
},
"meta": { "bbVersion": "v6.1.0" }
}
This matters for automation: internal means the CLI broke, and a caller that retries or
escalates on it would do the wrong thing with its own typo. A failure bb did not cause keeps
the kind it was classified as: a refused connection is transient, a rejected certificate
permanent, and a server response whatever its status maps to.