Skip to content

Scripting & Automation - Bitbucket CLI for CI/CD Workflows

Add --json for machine-readable output:

Terminal window
bb pr list --json
bb repo list --json
bb pr view 42 --json

Paginated 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.

Terminal window
bb pr list --json --all
bb pr list --json --limit 100

Output 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.

Two flags inspired by the gh CLI slice and filter output without an external jq binary:

Terminal window
# Project to a comma-separated field list
bb pr list --json id,title,state
# Filter through the embedded jq (requires --json)
bb pr list --json --jq '.pullRequests[].title'
# Combine both
bb 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:

Terminal window
bb pr list --json --jq '.pullRequests[].title'
bb pr list --json | jq '.pullRequests[].title'
Terminal window
# Open PR count (--state defaults to OPEN)
bb pr list --json --all --jq '.count'
# Merged PR count
bb pr list -s MERGED --json --all --jq '.count'
# Project specific fields
bb pr list --json --all --jq '.pullRequests[] | {id, title, author: .author.display_name}'
# Filter by author
bb pr list --json --all --jq '.pullRequests[] | select((.author.nickname // .author.display_name) == "alice")'
# PR web URLs
bb pr list --json --all --jq '.pullRequests[].links.html.href'
# Web diff URL for one PR
bb pr diff 42 --web --json --jq '.url'
# PRs updated in the last 7 days
bb 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 main
bb pr list --json --all | jq '.pullRequests[] | select(.destination.branch.name == "main")'
# Group PRs by author
bb pr list --json --all | jq '.pullRequests | group_by(.author.nickname // .author.display_name) | map({author: (.[0].author.nickname // .[0].author.display_name), count: length})'

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:

Terminal window
bb pr list | cut -f1,2 # "#42<TAB>Fix login redirect"
bb repo list | sort -t$'\t' -k1

Dates 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.

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.

Terminal window
# Any endpoint, authenticated
bb api /user --jq '.username'
# Fields become a query string on GET, a JSON body otherwise
bb api /repositories/myworkspace/myrepo/pullrequests -X GET -f state=MERGED
# Follow pagination and pull a single field across every page
bb api /repositories/myworkspace/myrepo/pullrequests --paginate \
--jq '.values[].title'
# Send a JSON body from a file
bb 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.


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.


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/bash
set -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 1
fi

The 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:

Terminal window
bb pr bogus --json 2> err.json
jq -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.


For non-interactive scripts, use environment variables:

Terminal window
export BB_USERNAME=[email protected]
export BB_API_TOKEN=ATBB_token
bb auth login # Uses env vars
bb pr list -w workspace -r repo

See Environment Variables Reference for details.


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 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.


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.

Terminal window
# Good - explicit and reliable
bb pr list -w myworkspace -r myrepo --json
# Bad - fails when the checkout has no Bitbucket remote
bb pr list --json

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.

When making many sequential API calls, add short delays to avoid hitting rate limits:

#!/bin/bash
set -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 1
done

Approve all open pull requests (PRs) from a specific author:

#!/bin/bash
set -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 1
done
echo "Done!"

Generate a markdown report of open PRs:

#!/bin/bash
set -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"'

Decline PRs not updated in 30 days:

#!/bin/bash
set -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 0
fi
for pr_id in $stale_prs; do
echo "Declining stale PR #$pr_id..."
bb pr decline "$pr_id" -w "$WORKSPACE" -r "$REPO"
sleep 1
done