Fixes #441. The JIRA REST API v2 /search endpoint has been deprecated and returns HTTP 410 Gone. Migrate to API v3 /search/jql endpoint. Changes: - Update API endpoint from /rest/api/2/search to /rest/api/3/search/jql - Add URL encoding for JQL query parameter (Python 3.14 compatibility) - Add reference to Atlassian migration guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Jira Integration for bd
Two-way synchronization between Jira and bd (beads).
Scripts
| Script | Purpose |
|---|---|
jira2jsonl.py |
Import - Fetch Jira issues into bd |
jsonl2jira.py |
Export - Push bd issues to Jira |
Overview
These tools enable bidirectional sync between Jira and bd:
Import (Jira → bd):
- Jira REST API - Fetch issues directly from any Jira instance
- JSON Export - Parse exported Jira issues JSON
- bd config integration - Read credentials and mappings from
bd config
Export (bd → Jira):
- Create issues - Push new bd issues to Jira
- Update issues - Sync changes to existing Jira issues
- Status transitions - Handle Jira workflow transitions automatically
Features
Import (jira2jsonl.py)
- Fetch from Jira Cloud or Server/Data Center
- JQL query support for flexible filtering
- Configurable field mappings (status, priority, type)
- Preserve timestamps, assignees, labels
- Extract issue links as dependencies
- Set
external_reffor re-sync capability - Hash-based or sequential ID generation
Export (jsonl2jira.py)
- Create new Jira issues from bd issues
- Update existing Jira issues (matched by
external_ref) - Handle Jira workflow transitions for status changes
- Reverse field mappings (bd → Jira)
- Dry-run mode for previewing changes
- Auto-update
external_refafter creation
Installation
No dependencies required! Uses Python 3 standard library.
Quick Start
Option 1: Using bd config (Recommended)
Set up your Jira credentials once:
# Required settings
bd config set jira.url "https://company.atlassian.net"
bd config set jira.project "PROJ"
bd config set jira.api_token "YOUR_API_TOKEN"
# For Jira Cloud, also set username (your email)
bd config set jira.username "you@company.com"
Then import:
python jira2jsonl.py --from-config | bd import
Option 2: Using environment variables
export JIRA_API_TOKEN=your_token
export JIRA_USERNAME=you@company.com # For Jira Cloud
python jira2jsonl.py \
--url https://company.atlassian.net \
--project PROJ \
| bd import
Option 3: Command-line arguments
python jira2jsonl.py \
--url https://company.atlassian.net \
--project PROJ \
--username you@company.com \
--api-token YOUR_TOKEN \
| bd import
Authentication
Jira Cloud
Jira Cloud requires:
- Username: Your email address
- API Token: Create at https://id.atlassian.com/manage-profile/security/api-tokens
bd config set jira.username "you@company.com"
bd config set jira.api_token "your_api_token"
Jira Server/Data Center
Jira Server/DC can use:
- Personal Access Token (PAT) - Just set the token, no username needed
- Username + Password - Set both username and password as the token
# Using PAT (recommended)
bd config set jira.api_token "your_pat_token"
# Using username/password
bd config set jira.username "your_username"
bd config set jira.api_token "your_password"
Usage
Basic Usage
# Fetch all issues from a project
python jira2jsonl.py --from-config | bd import
# Save to file first (recommended for large projects)
python jira2jsonl.py --from-config > issues.jsonl
bd import -i issues.jsonl --dry-run # Preview
bd import -i issues.jsonl # Import
Filtering Issues
# Only open issues
python jira2jsonl.py --from-config --state open
# Only closed issues
python jira2jsonl.py --from-config --state closed
# Custom JQL query
python jira2jsonl.py --url https://company.atlassian.net \
--jql "project = PROJ AND priority = High AND status != Done"
ID Generation Modes
# Sequential IDs (bd-1, bd-2, ...) - default
python jira2jsonl.py --from-config
# Hash-based IDs (bd-a3f2dd, ...) - matches bd create
python jira2jsonl.py --from-config --id-mode hash
# Custom hash length (3-8 chars)
python jira2jsonl.py --from-config --id-mode hash --hash-length 4
# Custom prefix
python jira2jsonl.py --from-config --prefix myproject
From JSON File
If you have an exported JSON file:
python jira2jsonl.py --file issues.json | bd import
Field Mapping
Default Mappings
| Jira Field | bd Field | Notes |
|---|---|---|
key |
(internal) | Used for dependency resolution |
summary |
title |
Direct copy |
description |
description |
Direct copy |
status.name |
status |
Mapped via status_map |
priority.name |
priority |
Mapped via priority_map |
issuetype.name |
issue_type |
Mapped via type_map |
assignee |
assignee |
Display name or username |
labels |
labels |
Direct copy |
created |
created_at |
ISO 8601 timestamp |
updated |
updated_at |
ISO 8601 timestamp |
resolutiondate |
closed_at |
ISO 8601 timestamp |
| (computed) | external_ref |
URL to Jira issue |
issuelinks |
dependencies |
Mapped to blocks/related |
parent |
dependencies |
Mapped to parent-child |
Status Mapping
Default status mappings (Jira status -> bd status):
| Jira Status | bd Status |
|---|---|
| To Do, Open, Backlog, New | open |
| In Progress, In Development, In Review | in_progress |
| Blocked, On Hold | blocked |
| Done, Closed, Resolved, Complete | closed |
Custom mappings via bd config:
bd config set jira.status_map.backlog "open"
bd config set jira.status_map.in_review "in_progress"
bd config set jira.status_map.on_hold "blocked"
Priority Mapping
Default priority mappings (Jira priority -> bd priority 0-4):
| Jira Priority | bd Priority |
|---|---|
| Highest, Critical, Blocker | 0 (Critical) |
| High, Major | 1 (High) |
| Medium, Normal | 2 (Medium) |
| Low, Minor | 3 (Low) |
| Lowest, Trivial | 4 (Backlog) |
Custom mappings:
bd config set jira.priority_map.urgent "0"
bd config set jira.priority_map.nice_to_have "4"
Issue Type Mapping
Default type mappings (Jira type -> bd type):
| Jira Type | bd Type |
|---|---|
| Bug, Defect | bug |
| Story, Feature, Enhancement | feature |
| Task, Sub-task | task |
| Epic, Initiative | epic |
| Technical Task, Maintenance | chore |
Custom mappings:
bd config set jira.type_map.story "feature"
bd config set jira.type_map.spike "task"
bd config set jira.type_map.tech_debt "chore"
Issue Links & Dependencies
Jira issue links are converted to bd dependencies:
| Jira Link Type | bd Dependency Type |
|---|---|
| Blocks/Is blocked by | blocks |
| Parent (Epic/Story) | parent-child |
| All others | related |
Note: Only links to issues included in the import are preserved. Links to issues outside the query results are ignored.
Re-syncing from Jira
Each imported issue has an external_ref field containing the Jira issue URL. On subsequent imports:
- Issues are matched by
external_reffirst - If matched, the existing bd issue is updated (if Jira is newer)
- If not matched, a new bd issue is created
This enables incremental sync:
# Initial import
python jira2jsonl.py --from-config | bd import
# Later: import only recent changes
python jira2jsonl.py --from-config \
--jql "project = PROJ AND updated >= -7d" \
| bd import
Examples
Example 1: Import Active Sprint
python jira2jsonl.py --url https://company.atlassian.net \
--jql "project = PROJ AND sprint in openSprints()" \
| bd import
bd ready # See what's ready to work on
Example 2: Full Project Migration
# Export all issues
python jira2jsonl.py --from-config > all-issues.jsonl
# Preview import
bd import -i all-issues.jsonl --dry-run
# Import
bd import -i all-issues.jsonl
# View stats
bd stats
Example 3: Sync High Priority Bugs
python jira2jsonl.py --from-config \
--jql "project = PROJ AND type = Bug AND priority in (Highest, High)" \
| bd import
Example 4: Import with Hash IDs
# Use hash IDs for collision-free distributed work
python jira2jsonl.py --from-config --id-mode hash | bd import
Limitations
- Single assignee: Jira supports multiple assignees (watchers), bd supports one
- Custom fields: Only standard fields are mapped; custom fields are ignored
- Attachments: Not imported
- Comments: Not imported (only description)
- Worklogs: Not imported
- Sprints: Sprint metadata not preserved (use labels or JQL filtering)
- Components/Versions: Not mapped to bd (consider using labels)
Troubleshooting
"Authentication failed"
Jira Cloud:
- Verify you're using your email as username
- Create a fresh API token at https://id.atlassian.com/manage-profile/security/api-tokens
- Ensure the token has access to the project
Jira Server/DC:
- Try using a Personal Access Token instead of password
- Check that your account has permission to access the project
"403 Forbidden"
- Check project permissions in Jira
- Verify API token has correct scopes
- Some Jira instances restrict API access by IP
"400 Bad Request"
- Check JQL syntax
- Verify project key exists
- Check for special characters in JQL (escape with backslash)
Rate Limits
Jira Cloud has rate limits. For large imports:
- Add delays between requests (not implemented yet)
- Import in batches using JQL date ranges
- Use the
--fileoption with a manual export
API Rate Limits
- Jira Cloud: ~100 requests/minute (varies by plan)
- Jira Server/DC: Depends on configuration
This script fetches 100 issues per request, so a 1000-issue project requires ~10 API calls.
Export: jsonl2jira.py
Push bd issues to Jira.
Export Quick Start
# Export all issues (create new, update existing)
bd export | python jsonl2jira.py --from-config
# Create only (don't update existing Jira issues)
bd export | python jsonl2jira.py --from-config --create-only
# Dry run (preview what would happen)
bd export | python jsonl2jira.py --from-config --dry-run
# Auto-update bd with new external_refs
bd export | python jsonl2jira.py --from-config --update-refs
Export Modes
Create Only
Only create new Jira issues for bd issues that don't have an external_ref:
bd export | python jsonl2jira.py --from-config --create-only
Create and Update
Create new issues AND update existing ones (matched by external_ref):
bd export | python jsonl2jira.py --from-config
Dry Run
Preview what would happen without making any changes:
bd export | python jsonl2jira.py --from-config --dry-run
Workflow Transitions
Jira often requires workflow transitions to change issue status (you can't just set status=Done). The export script automatically:
- Fetches available transitions for each issue
- Finds a transition that leads to the target status
- Executes the transition
If no valid transition is found, the status change is skipped with a warning.
Reverse Field Mappings
For export, you need mappings from bd → Jira (reverse of import):
# Status: bd status -> Jira status name
bd config set jira.reverse_status_map.open "To Do"
bd config set jira.reverse_status_map.in_progress "In Progress"
bd config set jira.reverse_status_map.blocked "Blocked"
bd config set jira.reverse_status_map.closed "Done"
# Type: bd type -> Jira issue type name
bd config set jira.reverse_type_map.bug "Bug"
bd config set jira.reverse_type_map.feature "Story"
bd config set jira.reverse_type_map.task "Task"
bd config set jira.reverse_type_map.epic "Epic"
bd config set jira.reverse_type_map.chore "Task"
# Priority: bd priority (0-4) -> Jira priority name
bd config set jira.reverse_priority_map.0 "Highest"
bd config set jira.reverse_priority_map.1 "High"
bd config set jira.reverse_priority_map.2 "Medium"
bd config set jira.reverse_priority_map.3 "Low"
bd config set jira.reverse_priority_map.4 "Lowest"
If not configured, sensible defaults are used.
Updating external_ref
After creating a Jira issue, you'll want to link it back to the bd issue:
# Option 1: Auto-update with --update-refs flag
bd export | python jsonl2jira.py --from-config --update-refs
# Option 2: Manual update from script output
bd export | python jsonl2jira.py --from-config | while read line; do
bd_id=$(echo "$line" | jq -r '.bd_id')
ext_ref=$(echo "$line" | jq -r '.external_ref')
bd update "$bd_id" --external-ref="$ext_ref"
done
Export Examples
Example 1: Initial Export to Jira
# First, export all open issues
bd list --status open --json | python jsonl2jira.py --from-config --update-refs
# Now those issues have external_ref set
bd list --status open
Example 2: Sync Changes Back to Jira
# Export issues modified today
bd list --json | python jsonl2jira.py --from-config
Example 3: Preview Before Export
# See what would happen
bd export | python jsonl2jira.py --from-config --dry-run
# If it looks good, run for real
bd export | python jsonl2jira.py --from-config --update-refs
Export Limitations
- Assignee: Not set (requires Jira account ID lookup)
- Dependencies: Not synced to Jira issue links
- Comments: Not exported
- Custom fields: design, acceptance_criteria, notes not exported
- Attachments: Not exported
Bidirectional Sync Workflow
For ongoing synchronization between Jira and bd:
# 1. Pull changes from Jira
python jira2jsonl.py --from-config --jql "project=PROJ AND updated >= -1d" | bd import
# 2. Do local work in bd
bd update bd-xxx --status in_progress
# ... work ...
bd close bd-xxx
# 3. Push changes to Jira
bd export | python jsonl2jira.py --from-config
# 4. Repeat daily/weekly
See Also
- bd README - Main documentation
- GitHub Import Example - Similar import for GitHub Issues
- CONFIG.md - Configuration documentation
- Jira REST API docs