From scrum-master
Manage Jira issues, sprints, and boards via the Jira REST API v3 and Agile API. Use this skill whenever the user wants to create, update, transition, search, or manage Jira issues — including epics, stories, tasks, bugs, and sub-tasks. Trigger on any mention of Jira issues, Jira URLs ({instance}/browse/PROJ-123), issue keys, JQL queries, sprints, boards, backlogs, or requests like "create a Jira ticket", "move this to In Progress", "search for bugs in PROJ", "add to sprint", "link these issues", or "bulk create stories".
npx claudepluginhub nexussema/omg-marketplace --plugin scrum-masterThis skill uses the workspace's default tool permissions.
You are working with Jira Cloud via the **REST API v3** for issue management and the **Agile REST API 1.0** for board/sprint operations.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Analyzes BMad project state from catalog CSV, configs, artifacts, and query to recommend next skills or answer questions. Useful for help requests, 'what next', or starting BMad.
You are working with Jira Cloud via the REST API v3 for issue management and the Agile REST API 1.0 for board/sprint operations.
Credentials and instance URL are read from environment variables. Users must set these in their shell profile (see README for setup):
# Read credentials from environment (set in ~/.zshrc, ~/.bashrc, etc.)
ATLASSIAN_EMAIL="${ATLASSIAN_EMAIL}"
ATLASSIAN_API_TOKEN="${ATLASSIAN_API_TOKEN}"
ATLASSIAN_INSTANCE="${ATLASSIAN_INSTANCE:-yourcompany.atlassian.net}"
If the env vars are not available in the subprocess, fall back to reading from the shell profile:
ATLASSIAN_EMAIL="${ATLASSIAN_EMAIL:-$(grep 'export ATLASSIAN_EMAIL' ~/.zshrc ~/.bashrc ~/.bash_profile 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_API_TOKEN="${ATLASSIAN_API_TOKEN:-$(grep 'export ATLASSIAN_API_TOKEN' ~/.zshrc ~/.bashrc ~/.bash_profile 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_INSTANCE="${ATLASSIAN_INSTANCE:-$(grep 'export ATLASSIAN_INSTANCE' ~/.zshrc ~/.bashrc ~/.bash_profile 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//' || echo 'yourcompany.atlassian.net')}"
https://${ATLASSIAN_INSTANCE}/rest/api/3https://${ATLASSIAN_INSTANCE}/rest/agile/1.0-u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN"When the user pastes a Jira URL, the issue key is the last path segment:
https://{instance}/browse/PROJ-123
^^^^^^^^
This is the issue key
ATLASSIAN_EMAIL="${ATLASSIAN_EMAIL:-$(grep 'export ATLASSIAN_EMAIL' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_API_TOKEN="${ATLASSIAN_API_TOKEN:-$(grep 'export ATLASSIAN_API_TOKEN' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_INSTANCE="${ATLASSIAN_INSTANCE:-$(grep 'export ATLASSIAN_INSTANCE' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//' || echo 'yourcompany.atlassian.net')}"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/project" \
| python3 -c "
import json, sys
projects = json.load(sys.stdin)
for p in projects:
print(f\"{p['key']}\t{p['name']}\t{p.get('projectTypeKey','')}\")
"
Before creating an issue, discover valid issue types for a project:
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/createmeta/$PROJECT_KEY/issuetypes" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for it in d.get('issueTypes', d.get('values', [])):
print(f\"{it['id']}\t{it['name']}\t{'subtask' if it.get('subtask') else 'standard'}\")
"
After selecting an issue type, discover required and available fields:
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/createmeta/$PROJECT_KEY/issuetypes/$ISSUE_TYPE_ID" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for f in d.get('fields', d.get('values', [])):
req = 'REQUIRED' if f.get('required') else 'optional'
print(f\"{f['fieldId']}\t{f['name']}\t{req}\t{f.get('schema',{}).get('type','')}\")
"
Use this to discover custom field IDs (e.g., customfield_10014 for Epic Link) before creating issues.
Supports: Epic, Story, Task, Bug, Sub-task.
ATLASSIAN_EMAIL="${ATLASSIAN_EMAIL:-$(grep 'export ATLASSIAN_EMAIL' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_API_TOKEN="${ATLASSIAN_API_TOKEN:-$(grep 'export ATLASSIAN_API_TOKEN' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_INSTANCE="${ATLASSIAN_INSTANCE:-$(grep 'export ATLASSIAN_INSTANCE' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//' || echo 'yourcompany.atlassian.net')}"
python3 -c "
import json
payload = {
'fields': {
'project': {'key': '<PROJECT_KEY>'},
'issuetype': {'name': '<Issue Type>'}, # Epic, Story, Task, Bug, Sub-task
'summary': '<Issue summary>',
'description': {
'type': 'doc',
'version': 1,
'content': [
{
'type': 'paragraph',
'content': [{'type': 'text', 'text': '<Description text>'}]
}
]
}
# Add optional/custom fields as needed:
# 'priority': {'name': 'High'},
# 'labels': ['backend', 'sprint-12'],
# 'assignee': {'accountId': '<account_id>'},
# 'parent': {'key': 'PROJ-100'}, # for Sub-task
# 'customfield_10014': 'PROJ-50', # Epic Link (field ID varies)
}
}
json.dump(payload, open('/tmp/jira_create.json', 'w'))
print('Payload written.')
"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d @/tmp/jira_create.json \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
if 'key' in d:
print(f\"Created: {d['key']}\")
print(f\"URL: https://$ATLASSIAN_INSTANCE/browse/{d['key']}\")
print(f\"ID: {d['id']}\")
else:
print('Error:', json.dumps(d, indent=2))
sys.exit(1)
"
rm -f /tmp/jira_create.json
ISSUE_KEY="PROJ-123"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/$ISSUE_KEY" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
f = d['fields']
print(f\"Key: {d['key']}\")
print(f\"Summary: {f['summary']}\")
print(f\"Type: {f['issuetype']['name']}\")
print(f\"Status: {f['status']['name']}\")
print(f\"Priority: {f.get('priority',{}).get('name','None')}\")
print(f\"Assignee: {f.get('assignee',{}).get('displayName','Unassigned') if f.get('assignee') else 'Unassigned'}\")
print(f\"Reporter: {f.get('reporter',{}).get('displayName','Unknown')}\")
print(f\"Labels: {', '.join(f.get('labels',[])) or 'None'}\")
print(f\"Created: {f['created']}\")
print(f\"Updated: {f['updated']}\")
# Print description if present
if f.get('description'):
print(f\"Description: (ADF document - see raw JSON for full content)\")
"
python3 -c "
import json
payload = {
'fields': {
'summary': '<New summary>',
# 'priority': {'name': 'High'},
# 'labels': ['updated-label'],
# 'description': { ADF document },
}
}
json.dump(payload, open('/tmp/jira_update.json', 'w'))
"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X PUT \
-H "Content-Type: application/json" \
-d @/tmp/jira_update.json \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/$ISSUE_KEY" \
-w "\nHTTP Status: %{http_code}\n"
rm -f /tmp/jira_update.json
A 204 No Content response means success.
First, get available transitions for the issue:
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/$ISSUE_KEY/transitions" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for t in d['transitions']:
print(f\"{t['id']}\t{t['name']}\t→ {t['to']['name']}\")
"
Then execute the transition:
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d "{\"transition\": {\"id\": \"$TRANSITION_ID\"}}" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/$ISSUE_KEY/transitions" \
-w "\nHTTP Status: %{http_code}\n"
A 204 No Content response means the transition was successful.
ATLASSIAN_EMAIL="${ATLASSIAN_EMAIL:-$(grep 'export ATLASSIAN_EMAIL' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_API_TOKEN="${ATLASSIAN_API_TOKEN:-$(grep 'export ATLASSIAN_API_TOKEN' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_INSTANCE="${ATLASSIAN_INSTANCE:-$(grep 'export ATLASSIAN_INSTANCE' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//' || echo 'yourcompany.atlassian.net')}"
python3 -c "
import json
payload = {
'jql': '<JQL query>',
'maxResults': 50,
'fields': ['summary', 'status', 'assignee', 'priority', 'issuetype', 'labels']
}
json.dump(payload, open('/tmp/jira_search.json', 'w'))
"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d @/tmp/jira_search.json \
"https://$ATLASSIAN_INSTANCE/rest/api/3/search" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
print(f\"Total: {d['total']} results\")
for issue in d['issues']:
f = issue['fields']
assignee = f.get('assignee',{}).get('displayName','Unassigned') if f.get('assignee') else 'Unassigned'
print(f\"{issue['key']}\t{f['issuetype']['name']}\t{f['status']['name']}\t{f['summary']}\t{assignee}\")
"
rm -f /tmp/jira_search.json
Common JQL examples:
project = PROJ AND status = "In Progress" — all in-progress issuesproject = PROJ AND issuetype = Bug AND status != Done — open bugsassignee = currentUser() AND sprint in openSprints() — my sprint itemsproject = PROJ AND labels = "backend" ORDER BY priority DESC — by labelproject = PROJ AND created >= -7d — issues created in last 7 daysparent = PROJ-100 — sub-tasks of an epic/storypython3 -c "
import json
payload = {
'body': {
'type': 'doc',
'version': 1,
'content': [
{
'type': 'paragraph',
'content': [{'type': 'text', 'text': '<Comment text>'}]
}
]
}
}
json.dump(payload, open('/tmp/jira_comment.json', 'w'))
"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d @/tmp/jira_comment.json \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/$ISSUE_KEY/comment" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
if 'id' in d:
print(f\"Comment added (ID: {d['id']})\")
else:
print('Error:', json.dumps(d, indent=2))
"
rm -f /tmp/jira_comment.json
# First find the user's accountId
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/user/search?query=<email_or_name>" \
| python3 -c "
import json, sys
users = json.load(sys.stdin)
for u in users:
print(f\"{u['accountId']}\t{u['displayName']}\t{u.get('emailAddress','')}\")
"
# Then assign
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X PUT \
-H "Content-Type: application/json" \
-d "{\"accountId\": \"<accountId>\"}" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/$ISSUE_KEY/assignee" \
-w "\nHTTP Status: %{http_code}\n"
To unassign, use {"accountId": null}.
python3 -c "
import json
payload = {
'type': {'name': '<link_type>'},
'inwardIssue': {'key': '<PROJ-1>'},
'outwardIssue': {'key': '<PROJ-2>'}
}
json.dump(payload, open('/tmp/jira_link.json', 'w'))
"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d @/tmp/jira_link.json \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issueLink" \
-w "\nHTTP Status: %{http_code}\n"
rm -f /tmp/jira_link.json
Common link types: Blocks, is blocked by, Clones, Duplicate, Relates.
To discover available link types:
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issueLinkType" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for lt in d['issueLinkTypes']:
print(f\"{lt['name']}\t{lt['inward']}\t{lt['outward']}\")
"
ATLASSIAN_EMAIL="${ATLASSIAN_EMAIL:-$(grep 'export ATLASSIAN_EMAIL' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_API_TOKEN="${ATLASSIAN_API_TOKEN:-$(grep 'export ATLASSIAN_API_TOKEN' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_INSTANCE="${ATLASSIAN_INSTANCE:-$(grep 'export ATLASSIAN_INSTANCE' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//' || echo 'yourcompany.atlassian.net')}"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/agile/1.0/board" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for b in d.get('values', []):
print(f\"{b['id']}\t{b['name']}\t{b['type']}\")
"
To filter boards by project: append ?projectKeyOrId=PROJ to the URL.
List sprints for a board:
BOARD_ID="<board_id>"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/agile/1.0/board/$BOARD_ID/sprint" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for s in d.get('values', []):
print(f\"{s['id']}\t{s['name']}\t{s['state']}\t{s.get('startDate','N/A')}\t{s.get('endDate','N/A')}\")
"
Get sprint issues:
SPRINT_ID="<sprint_id>"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/agile/1.0/sprint/$SPRINT_ID/issue" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for issue in d.get('issues', []):
f = issue['fields']
assignee = f.get('assignee',{}).get('displayName','Unassigned') if f.get('assignee') else 'Unassigned'
print(f\"{issue['key']}\t{f['issuetype']['name']}\t{f['status']['name']}\t{f['summary']}\t{assignee}\")
"
Move issues to a sprint:
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d "{\"issues\": [\"PROJ-1\", \"PROJ-2\", \"PROJ-3\"]}" \
"https://$ATLASSIAN_INSTANCE/rest/agile/1.0/sprint/$SPRINT_ID/issue" \
-w "\nHTTP Status: %{http_code}\n"
BOARD_ID="<board_id>"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/agile/1.0/board/$BOARD_ID/backlog?maxResults=50" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for issue in d.get('issues', []):
f = issue['fields']
priority = f.get('priority',{}).get('name','None') if f.get('priority') else 'None'
print(f\"{issue['key']}\t{f['issuetype']['name']}\t{priority}\t{f['summary']}\")
"
python3 -c "
import json
payload = {
'issueUpdates': [
{
'fields': {
'project': {'key': '<PROJECT_KEY>'},
'issuetype': {'name': 'Story'},
'summary': 'Story 1 title',
'description': {
'type': 'doc', 'version': 1,
'content': [{'type': 'paragraph', 'content': [{'type': 'text', 'text': 'Description 1'}]}]
}
}
},
{
'fields': {
'project': {'key': '<PROJECT_KEY>'},
'issuetype': {'name': 'Story'},
'summary': 'Story 2 title',
'description': {
'type': 'doc', 'version': 1,
'content': [{'type': 'paragraph', 'content': [{'type': 'text', 'text': 'Description 2'}]}]
}
}
}
]
}
json.dump(payload, open('/tmp/jira_bulk.json', 'w'))
print(f'Payload: {len(payload[\"issueUpdates\"])} issues')
"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d @/tmp/jira_bulk.json \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/bulk" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for issue in d.get('issues', []):
print(f\"Created: {issue['key']} — https://$ATLASSIAN_INSTANCE/browse/{issue['key']}\")
for err in d.get('errors', []):
print(f\"Error: {json.dumps(err)}\")
"
rm -f /tmp/jira_bulk.json
Jira v3 API uses ADF for rich-text fields (description, comment body). All ADF documents share this wrapper:
{
"type": "doc",
"version": 1,
"content": [ ... ]
}
Paragraph:
{"type": "paragraph", "content": [{"type": "text", "text": "Hello world"}]}
Heading (levels 1–6):
{"type": "heading", "attrs": {"level": 2}, "content": [{"type": "text", "text": "Section Title"}]}
Bold / Italic / Code:
{"type": "text", "text": "bold text", "marks": [{"type": "strong"}]}
{"type": "text", "text": "italic text", "marks": [{"type": "em"}]}
{"type": "text", "text": "inline code", "marks": [{"type": "code"}]}
Bullet List:
{
"type": "bulletList",
"content": [
{"type": "listItem", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Item 1"}]}]},
{"type": "listItem", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Item 2"}]}]}
]
}
Ordered List:
{
"type": "orderedList",
"content": [
{"type": "listItem", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Step 1"}]}]},
{"type": "listItem", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Step 2"}]}]}
]
}
Code Block:
{
"type": "codeBlock",
"attrs": {"language": "python"},
"content": [{"type": "text", "text": "print('hello')"}]
}
Table:
{
"type": "table",
"content": [
{
"type": "tableRow",
"content": [
{"type": "tableHeader", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Header 1"}]}]},
{"type": "tableHeader", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Header 2"}]}]}
]
},
{
"type": "tableRow",
"content": [
{"type": "tableCell", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Cell 1"}]}]},
{"type": "tableCell", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Cell 2"}]}]}
]
}
]
}
Link:
{"type": "text", "text": "Click here", "marks": [{"type": "link", "attrs": {"href": "https://example.com"}}]}
Mention (user):
{"type": "mention", "attrs": {"id": "<accountId>", "text": "@User Name"}}
Fetch all issues assigned to the current user in active sprints:
ATLASSIAN_EMAIL="${ATLASSIAN_EMAIL:-$(grep 'export ATLASSIAN_EMAIL' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_API_TOKEN="${ATLASSIAN_API_TOKEN:-$(grep 'export ATLASSIAN_API_TOKEN' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//')}"
ATLASSIAN_INSTANCE="${ATLASSIAN_INSTANCE:-$(grep 'export ATLASSIAN_INSTANCE' ~/.zshrc ~/.bashrc 2>/dev/null | head -1 | sed 's/.*=\"//;s/\".*//' || echo 'yourcompany.atlassian.net')}"
python3 -c "
import json
payload = {
'jql': 'assignee = currentUser() AND sprint in openSprints() ORDER BY status ASC, priority DESC',
'maxResults': 50,
'fields': ['summary', 'status', 'priority', 'issuetype', 'labels', 'parent', 'subtasks']
}
json.dump(payload, open('/tmp/jira_my.json', 'w'))
"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d @/tmp/jira_my.json \
"https://$ATLASSIAN_INSTANCE/rest/api/3/search" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
print(f'My Issues ({d[\"total\"]} total):')
print()
for issue in d['issues']:
f = issue['fields']
prio = f.get('priority',{}).get('name','—') if f.get('priority') else '—'
parent = f.get('parent',{}).get('key','') if f.get('parent') else ''
parent_str = f' (under {parent})' if parent else ''
subtask_count = len(f.get('subtasks', []))
sub_str = f' [{subtask_count} sub-tasks]' if subtask_count else ''
print(f\"{issue['key']}\t{f['issuetype']['name']}\t{f['status']['name']}\t{prio}\t{f['summary']}{parent_str}{sub_str}\")
"
rm -f /tmp/jira_my.json
Variations:
assignee = currentUser() AND status != Doneassignee = currentUser() AND project = PROJ AND status != DoneFetch an issue's sub-tasks to see the full breakdown:
ISSUE_KEY="PROJ-123"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/$ISSUE_KEY?fields=summary,status,subtasks" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
f = d['fields']
print(f\"Parent: {d['key']} — {f['summary']} [{f['status']['name']}]\")
print()
subtasks = f.get('subtasks', [])
if subtasks:
print(f'Sub-tasks ({len(subtasks)}):')
for st in subtasks:
sf = st['fields']
print(f\" {st['key']}\t{sf['status']['name']}\t{sf['summary']}\")
else:
print('No sub-tasks.')
"
After viewing the parent, create sub-tasks referencing it:
python3 -c "
import json
payload = {
'fields': {
'project': {'key': '<PROJECT_KEY>'},
'parent': {'key': '<PARENT_KEY>'},
'issuetype': {'name': 'Sub-task'},
'summary': '<Sub-task summary>',
'description': {
'type': 'doc',
'version': 1,
'content': [
{
'type': 'heading', 'attrs': {'level': 3},
'content': [{'type': 'text', 'text': 'Acceptance Criteria'}]
},
{
'type': 'bulletList',
'content': [
{'type': 'listItem', 'content': [{'type': 'paragraph', 'content': [{'type': 'text', 'text': 'Criterion 1'}]}]},
{'type': 'listItem', 'content': [{'type': 'paragraph', 'content': [{'type': 'text', 'text': 'Criterion 2'}]}]}
]
}
]
}
}
}
json.dump(payload, open('/tmp/jira_subtask.json', 'w'))
print('Payload written.')
"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d @/tmp/jira_subtask.json \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
if 'key' in d:
print(f\"Created sub-task: {d['key']}\")
print(f\"URL: https://$ATLASSIAN_INSTANCE/browse/{d['key']}\")
else:
print('Error:', json.dumps(d, indent=2))
sys.exit(1)
"
rm -f /tmp/jira_subtask.json
Add a Confluence page URL (or any external link) as a web link on a Jira issue:
ISSUE_KEY="PROJ-123"
LINK_URL="https://yourcompany.atlassian.net/wiki/spaces/PROJ/pages/12345/Page-Title"
LINK_TITLE="Architecture Document"
python3 -c "
import json
payload = {
'object': {
'url': '$LINK_URL',
'title': $(python3 -c "import json; print(json.dumps('$LINK_TITLE'))"),
'icon': {
'url16x16': 'https://yourcompany.atlassian.net/favicon.ico'
}
}
}
json.dump(payload, open('/tmp/jira_remotelink.json', 'w'))
"
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d @/tmp/jira_remotelink.json \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/$ISSUE_KEY/remotelink" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
if 'id' in d:
print(f\"Link added to {sys.argv[1]}: {sys.argv[2]}\")
else:
print('Error:', json.dumps(d, indent=2))
" "$ISSUE_KEY" "$LINK_TITLE"
rm -f /tmp/jira_remotelink.json
curl -s -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
"https://$ATLASSIAN_INSTANCE/rest/api/3/issue/$ISSUE_KEY/remotelink" \
| python3 -c "
import json, sys
links = json.load(sys.stdin)
if links:
for l in links:
obj = l.get('object', {})
print(f\"{l['id']}\t{obj.get('title','')}\t{obj.get('url','')}\")
else:
print('No remote links.')
"
Custom fields have instance-specific IDs (e.g., customfield_10014). Always discover before using:
customfield_XXXXX in create/update payloadsCommon custom fields (IDs vary by instance):
customfield_10014 (typically) — set to epic's issue keycustomfield_10016 (typically) — numeric valuecustomfield_10020 (typically) — sprint ID (number)Created: PROJ-123 — https://instance/browse/PROJ-123PROJ-123: transitioned to "In Progress"$ATLASSIAN_INSTANCE env var (defaults to yourcompany.atlassian.net)/rest/api/3) — uses ADF for rich text/rest/agile/1.0) — boards, sprints, backlog204 No Content = success for update, transition, assign operationsparent.key in the create payload