Machine Mode and Diagnostics¶
Machine mode contract¶
Use global --json for machine-consumable output.
Envelope shape:
{
"data": {},
"meta": {
"bbVersion": "v4.1.0"
}
}
data holds command-specific payloads. meta.bbVersion reports which binary produced the
document -- provenance for stored output, not a compatibility switch.
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": "v4.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.
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 bulk artifacts against published schemas when 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": "v4.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.
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)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 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 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.
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.
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.
bb bulk apply sets operationId there, because on the failure path the error envelope is
the only document written (ADR-075) and the status artifact is reached by id:
operationId=$(bb bulk apply --from-plan plan.json --json | jq -r '.error.details.operationId // empty')
bb bulk status "$operationId" --json
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": "v4.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.