Two fixes for real-world Jira Cloud usage: 1. Add fields=*all to API request - v3 search/jql endpoint returns only issue IDs by default, causing empty title/description imports 2. Add adf_to_text() converter for Atlassian Document Format - API v3 returns rich text fields as ADF JSON instead of plain text/HTML Also documented: - Silent auth failure gotcha (200 with empty results vs 401) - ADF format explanation with conversion examples
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
- Silent auth failure: The Jira API may return HTTP 200 with empty results instead of 401. Check for
X-Seraph-Loginreason: AUTHENTICATED_FAILEDheader in responses.
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.
Jira API v3 Notes
This script uses the Jira REST API v3 /rest/api/3/search/jql endpoint. The older /rest/api/3/search endpoint was deprecated (returns HTTP 410 Gone). Two important considerations:
Explicit Field Selection
The v3 search endpoint returns only issue IDs by default. The script explicitly requests fields=*all to retrieve all fields. Without this parameter, you'll get issues with no title, description, or other metadata.
Atlassian Document Format (ADF)
Jira API v3 returns rich text fields (like description) in Atlassian Document Format - a JSON structure rather than plain text or HTML. The script automatically converts ADF to markdown:
ADF input:
{"type": "doc", "content": [{"type": "heading", "attrs": {"level": 3}, "content": [{"type": "text", "text": "Overview"}]}]}
Converted output:
### Overview
Supported ADF node types: paragraph, heading, bulletList, orderedList, listItem, codeBlock, blockquote, hardBreak, rule, inlineCard, mention, and text nodes.
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