Scripting & Automation - Bitbucket CLI for CI/CD Workflows
JSON output mode
Section titled “JSON output mode”Add --json for machine-readable output:
bb pr list --jsonbb repo list --jsonbb pr view 42 --jsonPaginated list commands return 25 rows by default. Pass --all to fetch every
page, or --limit <n> for a specific cap. --all wins if you pass both.
--limit must be a positive integer; anything else fails with --limit must be a positive integer (error code 5002). There is no --page flag; pagination is
followed internally.
bb pr list --json --allbb pr list --json --limit 100Output of any size reaches a pipe in full, so bb pr list --json --all | jq
works the same as redirecting to a file, even when the reader is slow.
These flags belong to specific commands, not every list operation. Check
bb <group> <command> --help or the
list-command flag reference.
Some commands, such as bb pr checks, return complete collections without
these flags.
Field selection and --jq
Section titled “Field selection and --jq”Two flags inspired by the gh CLI
slice and filter output without an external jq binary:
# Project to a comma-separated field listbb pr list --json id,title,state
# Filter through the embedded jq (requires --json)bb pr list --json --jq '.pullRequests[].title'
# Combine bothbb pr list --json id,title,state \ --jq '.[] | select(.state == "OPEN") | .title'When --json fields is passed against a list-style command, the wrapper
envelope is dropped and the field list is projected per-item. The result is
a flat array. See JSON Output reference for details.
--jq matches the syntax of the standalone
jq binary, so existing expressions work either
way. These two are equivalent:
bb pr list --json --jq '.pullRequests[].title'bb pr list --json | jq '.pullRequests[].title'jq patterns
Section titled “jq patterns”# Open PR count (--state defaults to OPEN)bb pr list --json --all --jq '.count'
# Merged PR countbb pr list -s MERGED --json --all --jq '.count'
# Project specific fieldsbb pr list --json --all --jq '.pullRequests[] | {id, title, author: .author.display_name}'
# Filter by authorbb pr list --json --all --jq '.pullRequests[] | select((.author.nickname // .author.display_name) == "alice")'
# PR web URLsbb pr list --json --all --jq '.pullRequests[].links.html.href'
# Web diff URL for one PRbb pr diff 42 --web --json --jq '.url'
# PRs updated in the last 7 daysbb pr list --json --all | jq --arg date "$(date -d '7 days ago' -Iseconds 2>/dev/null || \ date -v-7d -Iseconds)" \ '.pullRequests[] | select(.updated_on > $date)'
# PRs targeting mainbb pr list --json --all | jq '.pullRequests[] | select(.destination.branch.name == "main")'
# Group PRs by authorbb pr list --json --all | jq '.pullRequests | group_by(.author.nickname // .author.display_name) | map({author: (.[0].author.nickname // .[0].author.display_name), count: length})'Piped table output
Section titled “Piped table output”When stdout is not a terminal, list tables come out as tab-separated values:
one line per row, no header, nothing padded or cut off. Quick one-liners work
without --json:
bb pr list | cut -f1,2 # "#42<TAB>Fix login redirect"bb repo list | sort -t$'\t' -k1Dates in piped tables are ISO 8601 timestamps (2026-07-28T11:14:00.000Z),
and hints such as “Showing N results” go to stderr. Use --json when a script depends on the exact fields; the TSV columns
follow the table and may change.
Raw API access (escape hatch)
Section titled “Raw API access (escape hatch)”When no typed command covers what you need, bb api calls any
Bitbucket Cloud 2.0 endpoint through the same authenticated stack. Its output is
already JSON, so --jq works without --json.
# Any endpoint, authenticatedbb api /user --jq '.username'
# Fields become a query string on GET, a JSON body otherwisebb api /repositories/myworkspace/myrepo/pullrequests -X GET -f state=MERGED
# Follow pagination and pull a single field across every pagebb api /repositories/myworkspace/myrepo/pullrequests --paginate \ --jq '.values[].title'
# Send a JSON body from a filebb api POST /repositories/{workspace}/{repo}/hooks --input body.json{workspace} and {repo} placeholders resolve from -w/-r or the current
checkout. See the API command reference for every flag.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Command failure, such as authentication, API, validation, network, or jq errors |
| 130 | An interactive prompt interrupted with Ctrl+C |
bb auth status exits 1 when you are not logged in, and
bb doctor exits 1 when any of its checks fails.
Set BB_DETAILED_EXIT_CODES=1 to split failures into 2 usage, 3 not found,
4 authentication and 5 confirmation required; see the
full map. For anything
finer, read the code field of the JSON error envelope, which is one of the
error codes.
Error handling
Section titled “Error handling”In --json mode, success JSON goes to stdout and the error envelope goes to
stderr. HTTP debug tracing also writes to stderr, so it never corrupts the
JSON on stdout. Redirect the two streams separately:
#!/bin/bashset -euo pipefail
if ! bb pr view 999 -w myworkspace -r myrepo --json > out.json 2> err.json; then code=$(jq -r '.code' err.json) case "$code" in 1001|1002|1003) echo "auth problem";; 2002) echo "PR not found";; *) echo "other failure ($code): $(jq -r '.message' err.json)";; esac exit 1fiThe error envelope carries:
| Field | Notes |
|---|---|
name |
Error class, BBError, AuthError, APIError, … |
code |
Numeric error code |
message |
Human-readable failure description |
context |
Extra detail (ids, keys, args); omitted when the error carries none |
statusCode |
HTTP status, APIError only |
response |
Raw API response body, APIError only, when non-empty |
hint |
Remediation advice; optional, see below |
hint appears on API errors with status 401, 403, or 404 (skipped on 404 when
the message already names the missing resource) and on argument-parsing errors.
Treat it as optional in scripts.
Argument-parsing failures use the same envelope whenever --json or --jq
appears before any --. An unknown subcommand or option, or too many
arguments, returns code 5002; a missing argument, option value or subcommand
returns 5001. Both carry context.parseError, context.commandPath and a
hint:
bb pr bogus --json 2> err.jsonjq -c '{code, parseError: .context.parseError, commandPath: .context.commandPath, hint}' err.json# {"code":5002,"parseError":"unknownCommand","commandPath":"pr","hint":"Run `bb pr --help` for usage."}An unknown top-level command (bb bogus --json) also returns 5002, with
context: { command, args } naming the token that didn’t match. Without
--json or --jq, parse errors stay plain text.
Environment variables
Section titled “Environment variables”For non-interactive scripts, use environment variables:
export BB_API_TOKEN=ATBB_token
bb auth login # Uses env varsbb pr list -w workspace -r repoSee Environment Variables Reference for details.
Built-in resilience
Section titled “Built-in resilience”Automatic retry
Section titled “Automatic retry”API requests that fail with HTTP 429, 502, 503, or 504 are retried up to 3 times with exponential backoff. Add your own retry loop only if you need more than 3 attempts.
OAuth token refresh
Section titled “OAuth token refresh”OAuth access tokens expire after 2 hours. The CLI refreshes them before expiry
and retries authentication after a 401. If refresh fails, for example because
the refresh token was revoked, the command raises AUTH_EXPIRED and you must
run bb auth login again.
Scripting best practices
Section titled “Scripting best practices”Always pass -w and -r
Section titled “Always pass -w and -r”Workspace and repository detection reads the git remote of the current checkout. CI runners frequently check out with a non-Bitbucket remote, or no remote at all, so context detection fails there.
# Good - explicit and reliablebb pr list -w myworkspace -r myrepo --json
# Bad - fails when the checkout has no Bitbucket remotebb pr list --jsonNever rely on prompts
Section titled “Never rely on prompts”bb only prompts when stdin and stdout are both terminals, so pipelines and CI
jobs never block on a question (bb completion install aside, which always
asks for your shell). Scripts run inside a pseudo-terminal (expect,
script, some IDE task runners) do look interactive, though. Set
BB_PROMPT_DISABLED=1 or pass --no-input there, and pass --yes and
--title explicitly, so a missing flag fails fast instead of waiting for an
answer.
Add delays in loops
Section titled “Add delays in loops”When making many sequential API calls, add short delays to avoid hitting rate limits:
#!/bin/bashset -euo pipefail
WORKSPACE="myworkspace"REPO="myrepo"
pr_ids=$(bb pr list -w "$WORKSPACE" -r "$REPO" --json --all | jq -r '.pullRequests[].id')for pr_id in $pr_ids; do bb pr view "$pr_id" -w "$WORKSPACE" -r "$REPO" --json sleep 1doneExample scripts
Section titled “Example scripts”Batch pull request approval
Section titled “Batch pull request approval”Approve all open pull requests (PRs) from a specific author:
#!/bin/bashset -euo pipefail
WORKSPACE="myworkspace"REPO="myrepo"AUTHOR="trusted-bot"
echo "Finding PRs from $AUTHOR..."
pr_ids=$(bb pr list -w "$WORKSPACE" -r "$REPO" --json --all | \ jq -r --arg author "$AUTHOR" '.pullRequests[] | select((.author.nickname // .author.display_name) == $author) | .id')
for pr_id in $pr_ids; do echo "Approving PR #$pr_id..." bb pr approve "$pr_id" -w "$WORKSPACE" -r "$REPO" sleep 1done
echo "Done!"PR status report
Section titled “PR status report”Generate a markdown report of open PRs:
#!/bin/bashset -euo pipefail
WORKSPACE="myworkspace"REPO="myrepo"
echo "# Open Pull Requests"echo ""echo "Generated: $(date)"echo ""
bb pr list -w "$WORKSPACE" -r "$REPO" --json --all | jq -r '.pullRequests[] | "## PR #\(.id): \(.title)\n" + "- **Author:** \(.author.display_name)\n" + "- **Branch:** \(.source.branch.name) → \(.destination.branch.name)\n" + "- **Created:** \(.created_on)\n" + "- **Link:** \(.links.html.href)\n"'Auto-close stale PRs
Section titled “Auto-close stale PRs”Decline PRs not updated in 30 days:
#!/bin/bashset -euo pipefail
WORKSPACE="myworkspace"REPO="myrepo"DAYS_OLD=30
cutoff=$(date -d "$DAYS_OLD days ago" -Iseconds 2>/dev/null || \ date -v-${DAYS_OLD}d -Iseconds) # macOS fallback
echo "Finding PRs older than $DAYS_OLD days..."
stale_prs=$(bb pr list -w "$WORKSPACE" -r "$REPO" --json --all | \ jq -r --arg cutoff "$cutoff" '.pullRequests[] | select(.updated_on < $cutoff) | .id')
if [ -z "$stale_prs" ]; then echo "No stale PRs found" exit 0fi
for pr_id in $stale_prs; do echo "Declining stale PR #$pr_id..." bb pr decline "$pr_id" -w "$WORKSPACE" -r "$REPO" sleep 1done