Jira CLI
Command-line tool for managing Jira issues via af jira.
Setup
Add the following environment variables to your project's .env file (preferred names first; legacy JIRA_* names are still accepted and fall back to these when unset):
ATLASSIAN_BASE_URL— Your Atlassian instance URL (e.g.,https://company.atlassian.net)ATLASSIAN_EMAIL— Your Atlassian account emailATLASSIAN_API_TOKEN— API token from https://id.atlassian.com/manage-profile/security/api-tokens
Project Defaults
If the project instructions specify a default Jira project key (e.g., Jira project: PROJ), use it as the default for --project, list, types, and JQL queries. Explicit user input always overrides the default.
Quick Reference
Run bare af jira (no subcommand) for the full command and flag reference. Note af jira --help is intercepted by af's router and prints only a short stub.
Read Operations
af jira get <issue-key>— Get issue details (renders a "Custom Fields" section when any are set)af jira list <project> [--limit N] [--show-field a,b,c]— List project issues;--show-fieldadds custom-field columnsaf jira search "<jql>" [--show-field a,b,c]— Search with JQLaf jira projects— List projectsaf jira types <project>— List issue typesaf jira fields [--project <key> --type <name>] [--refresh] [--verbose]— List custom fields; with--project/--typemarks required fields and shows allowed values (create-scoped)af jira editmeta <issue-key>— Editable fields and allowed values for an existing issue (the edit-scoped twin offields) — run this beforeaf jira update --field ...af jira transitions <issue-key>— List available transitions, expanded with screen metadata: which transitions present a screen and what fields they require (this output tells you which--resolution/--fieldargs thetransitioncall needs)af jira comment <issue-key>— List commentsaf jira remote-link <issue-key>— List remote linksaf jira versions <project>— List all versions in a projectaf jira version <version-id>— Get version details
Write Operations
af jira create --project <key> --type <type> --summary "<text>" [--description "<text>"] [--priority <name>] [--labels a,b,c] [--parent <key>] [--estimate <time>] [--fix-version <v1,v2>] [--affected-version <v>] [--field name=value]... [--field-json '<json>']af jira update <issue-key> [--summary "<text>"] [--description "<text>"] [--priority <name>] [--labels a,b,c] [--estimate <time>] [--remaining <time>] [--fix-version <v1,v2>] [--affected-version <v>] [--parent <issue-key>] [--clear-parent] [--field name=value]... [--field-json '<json>']af jira transition <issue-key> --to "<status>" [--resolution <name>] [--comment "<text>"] [--field name=value]...— Screen fields, resolution, and a transition comment in one callaf jira assign <issue-key> --to <email>(use--to noneto unassign)af jira comment <issue-key> --add "<text>" [--visibility <name>] [--internal] [--public]af jira comment edit <issue-key> <comment-id> --body "<text>" [--visibility <name>]af jira comment delete <issue-key> <comment-id>af jira attach <issue-key> <file>— Attach a file (images, PDFs, etc.)af jira move <issue-key> --to-project <KEY> [--type <name>]— Move to another project (async;--typedefaults to the current issue type)af jira delete <issue-key>af jira watch <issue-key>/af jira unwatch <issue-key>/af jira vote <issue-key>— All act as the current user
--parent sets the parent (subtask parent or epic — parent is the canonical field that replaced Epic Link); --clear-parent detaches it and wins if both are given. --clear-parent is provisional: behavior varies by project type.
--visibility <name> restricts a comment to a project role by default; prefix with group: to pick a group (--visibility "group:Developers") or role: to force a role. --internal / --public are JSM only — they route the comment through the Service Desk API to add an internal note or a public reply.
Worklog Operations
af jira worklog list <issue-key>— List worklogsaf jira worklog add <issue-key> --time <duration> [--comment "<text>"] [--started "<timestamp>"]—--timerequired (e.g.2h,30m);--starteddefaults to now and uses Jira's formatyyyy-MM-ddTHH:mm:ss.SSS+0000af jira worklog update <issue-key> <worklog-id> [--time <duration>] [--comment "<text>"] [--started "<timestamp>"]af jira worklog delete <issue-key> <worklog-id>
Bulk Operations (JQL-selected, async)
af jira bulk delete --jql "<query>"af jira bulk transition --jql "<query>" --to "<status>"af jira bulk edit --jql "<query>" --field-json '<json>'
All three select issues with --jql (required) and run through Jira's asynchronous bulk endpoints, polled to completion. The selection is a single unpaginated search capped at 1000 issues — a JQL matching more than 1000 is silently truncated, so narrow the JQL (and re-run) rather than assuming everything matched was processed.
- Bulk transition refuses screens. If the target transition presents a screen or has required fields, the command errors out and points you at single-issue
af jira transition, which can supply--resolution/--field. Bulk transition is for no-screen transitions only. bulk edit --field-jsonis NOT the same shape ascreate/update --field-json. It takes Jira's bulk-editeditedFieldsInputpayload verbatim, not a plain map of custom fields. Consult the Jira bulk edit API for the shape.
Agile Operations (Jira Software)
af jira boards [--project <key>]— List boardsaf jira sprints --board <id> [--state future,active,closed]— List a board's sprints (--boardrequired)af jira sprint add <issue-key> --sprint <sprint-id>— Move an issue into a sprintaf jira sprint remove <issue-key>— Move an issue to the backlogaf jira rank <issue-key> --above <key>/--below <key>— Reorder in the backlog (at least one is required; if both are passed,--abovewins and--belowis ignored)
Link Operations
af jira link <issue-key> --to <issue-key> [--type "<name>"]— Link two issues (default type: "Blocks")af jira unlink <issue-key> --from <issue-key>— Remove a linkaf jira remote-link <issue-key> --url "<url>" --title "<text>"— Add a remote linkaf jira remote-link <issue-key> --remove <link-id>— Remove a remote link
Version Operations
af jira version-create --project <key> --name "<text>" [--description "<text>"] [--start-date YYYY-MM-DD] [--release-date YYYY-MM-DD] [--released]af jira version-update <id> [--name "<text>"] [--description "<text>"] [--start-date YYYY-MM-DD] [--release-date YYYY-MM-DD] [--released] [--unreleased]af jira version-delete <id> [--move-fix-issues-to <id>] [--move-affected-issues-to <id>]
Output Formats
- Default: Markdown
- JSON: Add
--jsonflag
Common Workflows
View my assigned issues
af jira search "assignee = currentUser() AND status != Done ORDER BY priority DESC"
Start working on an issue
af jira get PROJ-123
af jira transition PROJ-123 --to "In Progress"
af jira comment PROJ-123 --add "Starting work"
Complete an issue
Close it with a resolution — a bare --to "Done" leaves the issue resolved with no resolution. transition carries the resolution and the comment in the same call:
af jira transition PROJ-123 --to Done --resolution Fixed --comment "Shipped in v1.2"
Transition through a screen
af jira transitions shows which transitions present a screen and what fields they require. Supply those with repeatable --field:
# See what the transition needs
af jira transitions PROJ-123
# Supply screen fields alongside the resolution
af jira transition PROJ-123 --to Done --resolution "Won't Do" \
--comment "Superseded by PROJ-456" \
--field customfield_10050='{"id":"3"}'
transition --field has different semantics from create/update --field: the value is JSON-parsed when it parses (so --field customfield_10050='{"id":"3"}' sends an object) and kept as a raw string otherwise. It is passed straight through to the transition screen — no alias resolution, no schema-driven encoding.
Create a bug with details
af jira create --project PROJ --type Bug --summary "Login fails on Safari" \
--description "Users cannot log in using Safari 17. Error: 'Invalid session'" \
--priority High --labels safari,auth,urgent
Create a task with time estimate and version
af jira create --project PROJ --type Task --summary "Implement auth" \
--estimate "4h" --fix-version "v1.0.0"
Create a subtask
af jira create --project PROJ --type Sub-task --summary "Write unit tests" \
--parent PROJ-123
Link issues
# Block another issue
af jira link PROJ-123 --to PROJ-456
# Use a specific link type
af jira link PROJ-123 --to PROJ-456 --type "Relates"
# Remove a link
af jira unlink PROJ-123 --from PROJ-456
Remote links
# List remote links
af jira remote-link PROJ-123
# Add a remote link (e.g., design doc, PR, external resource)
af jira remote-link PROJ-123 --url "https://example.com/doc" --title "Design Doc"
# Remove a remote link
af jira remote-link PROJ-123 --remove 10042
Manage versions
# List versions
af jira versions PROJ
# Create a version
af jira version-create --project PROJ --name "v1.0.0" --release-date 2024-06-01
# Mark a version as released
af jira version-update 12345 --released
Update time tracking
af jira update PROJ-123 --estimate "8h" --remaining "2h"
Log work
Estimates (--estimate/--remaining on update) are projections; worklogs are the record of time actually spent.
# Log 2h against an issue (--started defaults to now)
af jira worklog add PROJ-123 --time 2h --comment "Debugging the session bug"
# Log against a specific start time
af jira worklog add PROJ-123 --time 90m --started "2024-06-01T09:00:00.000+0000"
# Review, correct, remove
af jira worklog list PROJ-123
af jira worklog update PROJ-123 10015 --time 3h --comment "Also covered the fix"
af jira worklog delete PROJ-123 10015
Reparent an issue / move it under an epic
# Put the issue under an epic (or change a subtask's parent)
af jira update PROJ-123 --parent PROJ-100
# Detach it
af jira update PROJ-123 --clear-parent
Manage comments
# Add a comment restricted to a project role, or to a group
af jira comment PROJ-123 --add "Internal note" --visibility Developers
af jira comment PROJ-123 --add "Internal note" --visibility "group:jira-developers"
# JSM: internal note vs. public reply to the customer
af jira comment PROJ-123 --add "Escalating to platform" --internal
af jira comment PROJ-123 --add "We've deployed a fix" --public
# Edit or delete (get comment ids from `af jira comment PROJ-123`)
af jira comment edit PROJ-123 10045 --body "Corrected: shipped in v1.3"
af jira comment delete PROJ-123 10045
Sprints, boards, and backlog ranking (Jira Software)
# Find the board, then its sprints
af jira boards --project PROJ
af jira sprints --board 42 --state active,future
# Move an issue into a sprint, or back to the backlog
af jira sprint add PROJ-123 --sprint 108
af jira sprint remove PROJ-123
# Reorder the backlog
af jira rank PROJ-123 --above PROJ-456
af jira rank PROJ-123 --below PROJ-456
Bulk operations over a JQL selection
# Close out a batch (no-screen transitions only — see below)
af jira bulk transition --jql "project = PROJ AND status = 'In Review' AND updated < -30d" \
--to "In Progress"
# Delete a batch
af jira bulk delete --jql "project = PROJ AND type = Task AND labels = scratch"
# Bulk edit takes Jira's editedFieldsInput payload, NOT the create/update --field-json shape
af jira bulk edit --jql "project = PROJ AND labels = migrate" --field-json '<editedFieldsInput>'
bulk edit passes the --field-json object straight to Jira's bulk edit API as editedFieldsInput, and its top-level keys become the selectedActions. Look the shape up in Jira's bulk edit API docs — it is not the plain {"customfield_10050": ...} map that create/update --field-json accept, so don't copy one into the other.
If the bulk transition target presents a screen or has required fields, the command refuses and tells you to use single-issue af jira transition (which can pass --resolution/--field). Loop over the keys yourself in that case.
Move an issue to another project
# --to-project is required; --type defaults to the current issue type
af jira move PROJ-123 --to-project OPS
# Change the issue type as part of the move
af jira move PROJ-123 --to-project OPS --type Task
This runs through the asynchronous bulk-move API and reports the polled task id and status.
Watch or vote
af jira watch PROJ-123
af jira unwatch PROJ-123
af jira vote PROJ-123
All three act as the current user (the account behind ATLASSIAN_EMAIL).
Custom fields
Custom fields are instance-specific (Story Points, Sprint, Severity, and any custom selectors configured by the Jira admin). Reference them by alias (from af.json), by display name (case-insensitive, must be unambiguous), or by raw customfield_<digits> id — in that precedence order.
# Discover fields — lists every custom field in the instance
af jira fields
# Project-and-type scope marks required fields with ✓ and lists allowed values
af jira fields --project PROJ --type Story
# Bust the cache (instance catalog is cached 24h, createmeta 1h)
af jira fields --refresh
# Create with required custom fields
af jira create --project PROJ --type Story --summary "Redesign login" \
--field storyPoints=5 --field severity=High
# Update a custom field
af jira update PROJ-123 --field storyPoints=8
# Clear a custom field (empty value after `=`)
af jira update PROJ-123 --field severity=
# JSON escape hatch for complex shapes (cascading selects, raw overrides)
af jira create --project PROJ --type Story --summary "X" \
--field-json '{"customfield_10050":{"value":"A","child":{"value":"A1"}}}'
# Show custom fields as extra columns when listing
af jira list PROJ --show-field storyPoints,severity
Encoding rules (applied per --field):
number→ numeric;string/date/datetime→ raw stringoption→{value: "..."}; multi-select → array of thoseuser→ resolves email/name to{accountId}viafindUser; multi-user splits on commasversion→{name: "..."}; multi-version array- sprint → numeric sprint ID only
- Epic Link → issue key string
- Empty value after
=→null(clears the field) - Unknown schema types pass through raw and warn to stderr — reach for
--field-jsonif that doesn't encode correctly
These rules apply to create --field and update --field only. transition --field is a different flag with different semantics (raw pass-through, JSON-parsed when parseable), and bulk edit --field-json takes Jira's editedFieldsInput payload rather than the plain custom-field map that create/update --field-json take.
Optional aliases live in af.json and make commands more ergonomic:
{
"jira": {
"customFields": {
"storyPoints": { "id": "customfield_10016" },
"sprint": { "id": "customfield_10020", "type": "sprint" },
"severity": { "id": "customfield_10099" }
}
}
}
The CLI does not pre-validate required fields on create. Run af jira fields --project PROJ --type <type> first to see what Jira will require; otherwise the error message from Jira's 400 surfaces unchanged.
Attach files to an issue
# Attach a screenshot
af jira attach PROJ-123 ./screenshot.png
# Attach multiple files
for f in ./audit/*.png; do
af jira attach PROJ-123 "$f"
done
Search examples
# My open issues
af jira search "assignee = currentUser() AND status != Done"
# Recent bugs in project
af jira search "project = PROJ AND type = Bug ORDER BY created DESC" --limit 10
# Unassigned issues
af jira search "project = PROJ AND assignee IS EMPTY"
# Issues updated this week
af jira search "project = PROJ AND updated >= -7d"
# High priority blockers
af jira search "priority = Highest AND status != Done"
Tips
- Discover valid values first: Run
af jira transitions <key>before transitioning (it shows screen fields too),af jira types <project>before creating,af jira fields --project <key> --type <type>before creating an issue that requires custom fields, andaf jira editmeta <key>before updating one —fieldsis create-scoped,editmetais the edit-scoped view of an existing issue - Close issues with a resolution:
af jira transition <key> --to Done --resolution Fixed. A bare--to Doneleaves the issue with no resolution - Use
--jsonfor scripting: Pipe output tojqfor automation - Quote JQL queries: Always wrap JQL in double quotes to handle spaces
- Time format: Use
"2h","1d","30m"for estimates and forworklog --time - Clearing flags:
--fix-version ""clears versions;--field name=clears a custom field;--clear-parentdetaches a parent - Alias custom fields in
af.jsonfor nicer--fieldergonomics; otherwise use the display name (case-insensitive, must be unambiguous) or the rawcustomfield_<digits>id - Dry-run bulk ops: run the
--jqlthroughaf jira searchfirst to see exactly which issuesaf jira bulkwill hit — bulk delete is not reversible
Error Handling
- Errors print to stderr
- With
--json:{"error": "message"} - Exit codes:
0success,1error