| name | jira-automation |
| description | Complete Jira automation toolkit using REST API - create, update, search, transition issues, manage sprints, add comments, link issues, and more |
Jira Automation Skill
When to Use This Skill
Use this skill when you need to:
Issue Management:
- Create single or multiple Jira issues from specifications
- Update existing issues (fields, status, assignee, etc.)
- Search and filter issues using JQL (Jira Query Language)
- Get detailed issue information
- Delete issues (with caution)
Workflow & Status:
- Transition issues through workflow states (To Do → In Progress → Done)
- Get available transitions for an issue
- Track issue status across projects
Collaboration:
- Add comments to issues
- Mention users in comments with @username
- Update issue descriptions with findings
Agile & Sprint Management:
- Create and update sprints
- Link issues to epics
- Get board and sprint information
- Manage sprint issues and backlog
Issue Relationships:
- Link related issues (blocks, relates to, duplicates, etc.)
- Remove issue links
- Create parent-child relationships
Project & Version Management:
- Get all projects and their details
- Create and manage project versions
- Get project-specific issues
Time Tracking:
- Add work logs to issues
- Track time spent on tasks
Prerequisites
Before using this skill, ensure you have:
Skill Workflow
Step 1: Prepare Task Definitions
Structure your tasks as a Python dictionary with this format:
EPIC_TASKS = {
"EPIC-KEY": [
{
"summary": "Task title (required)",
"description": "Task description (required)",
"labels": ["label1", "label2"],
"priority": "Major",
"timeEstimate": "2h"
},
]
}
Required fields:
summary: Short task title
description: Detailed task description (supports markdown)
Optional fields:
labels: Array of label strings
priority: Jira priority name (default: "Major")
timeEstimate: Time estimate in Jira format (e.g., "30m", "2h", "1d")
Step 2: Create the Python Script
Use this template as the base for your bulk creation script:
"""
Bulk create Jira tasks for epics.
Usage:
python create-jira-tasks.py --email YOUR_EMAIL --api-token YOUR_API_TOKEN
"""
import argparse
import sys
import requests
from requests.auth import HTTPBasicAuth
JIRA_BASE_URL = "https://your-company.atlassian.net"
JIRA_API_URL = f"{JIRA_BASE_URL}/rest/api/3"
PROJECT_KEY = "YOUR_PROJECT_KEY"
EPIC_TASKS = {
}
def create_jira_task(auth, epic_key, task):
"""Create a single Jira task under the specified epic."""
payload = {
"fields": {
"project": {"key": PROJECT_KEY},
"summary": task["summary"],
"description": {
"type": "doc",
"version": 1,
"content": [{
"type": "paragraph",
"content": [{"type": "text", "text": task["description"]}]
}]
},
"issuetype": {"name": "Task"},
"parent": {"key": epic_key},
"labels": task.get("labels", []),
"priority": {: task.get(, )}
}
}
task:
payload[][] = {
: task[]
}
response = requests.post(
,
auth=auth,
headers={: },
json=payload
)
response.status_code == :
result = response.json()
()
result
:
()
()
()
():
parser = argparse.ArgumentParser(description=)
parser.add_argument(, required=, =)
parser.add_argument(, required=, =)
parser.add_argument(, =)
parser.add_argument(, action=, =)
args = parser.parse_args()
auth = HTTPBasicAuth(args.email, args.api_token)
response = requests.get(, auth=auth)
response.status_code != :
()
sys.exit()
user_info = response.json()
()
()
epics_to_process = [args.epic] args.epic EPIC_TASKS.keys()
total_created =
total_failed =
epic_key epics_to_process:
epic_key EPIC_TASKS:
()
tasks = EPIC_TASKS[epic_key]
()
()
args.dry_run:
task tasks:
()
()
task tasks:
result = create_jira_task(auth, epic_key, task)
result:
total_created +=
:
total_failed +=
()
args.dry_run:
()
:
()
total_failed > :
()
__name__ == :
main()
Step 3: Windows Encoding Fix
IMPORTANT FOR WINDOWS: Remove emoji characters from print statements to avoid encoding errors.
Replace emojis with ASCII prefixes:
- ✅ →
[OK]
- ❌ →
[ERROR]
- ⚠️ →
[WARN]
- 📋 →
[EPIC]
- 🔍 →
[DRY-RUN]
Step 4: Execute the Script
Dry run (preview only):
python create-jira-tasks.py --email your.email@company.com --api-token YOUR_TOKEN --dry-run
Create tasks for specific epic:
python create-jira-tasks.py --email your.email@company.com --api-token YOUR_TOKEN --epic DP01-21
Create all tasks:
python create-jira-tasks.py --email your.email@company.com --api-token YOUR_TOKEN
Best Practices
1. Always Dry Run First
Run with --dry-run to verify task definitions before creating them in Jira.
2. Test with One Epic
Use --epic EPIC-KEY to test with a single epic before bulk creating all tasks.
3. Validate Epic Keys Exist
Ensure all parent epic keys exist in Jira before running the script. The script will fail if parent epic doesn't exist.
4. Use Meaningful Labels
Add labels for filtering and organization:
Track-1, Track-2, Track-3 for project tracks
frontend, backend, database for component types
Day-1-90, Day-91-180 for MVP phasing
5. Set Realistic Time Estimates
Use Jira time format:
- Minutes:
30m, 45m
- Hours:
1h, 2h, 4h
- Days:
1d, 2d (1 day = 8 hours by default)
6. Structure Task Descriptions
Include in each task description:
- Autonomy Level: HIGH/MEDIUM/LOW (for Claude Code execution)
- Claude Code Prompt: Ready-to-use prompt for implementation
- Deliverables: Specific files or features to create
- Validation: How to verify the task is complete
7. Handle Errors Gracefully
The script continues even if individual tasks fail. Review the summary to identify failed tasks.
Common Issues and Solutions
Issue: Authentication Failed (401)
Cause: Wrong email or expired API token
Solution:
Issue: Epic Not Found (404)
Cause: Parent epic key doesn't exist
Solution: Create the epic in Jira first, or verify the epic key is correct
Issue: Unicode Encoding Error (Windows)
Cause: Emoji characters in print statements
Solution: Replace all emojis with ASCII equivalents (see Step 3)
Issue: Time Estimate Not Set
Cause: Invalid time format
Solution: Use Jira format: 30m, 2h, 1d (not 30 minutes, 2 hours)
Issue: Tasks Created in Wrong Project
Cause: Incorrect PROJECT_KEY
Solution: Update PROJECT_KEY constant to match your Jira project
Example: Epic Tasking Guide to Jira Tasks
This skill was used to create 74 tasks from the Epic Tasking Guide:
python scripts/create-jira-tasks.py \
--email clay.campbell@vividcg.com \
--api-token YOUR_TOKEN
Customization Options
Add Custom Fields
To add custom Jira fields, update the payload["fields"] dictionary:
payload["fields"]["customfield_10001"] = "Custom value"
payload["fields"]["assignee"] = {"accountId": "user-account-id"}
payload["fields"]["duedate"] = "2025-12-31"
Filter by Labels
Create tasks only for specific labels:
if args.label_filter:
tasks = [t for t in tasks if args.label_filter in t.get("labels", [])]
Batch Creation with Rate Limiting
Add rate limiting for large batches:
import time
for task in tasks:
result = create_jira_task(auth, epic_key, task)
time.sleep(0.5)
Integration with Claude Code
When breaking down epics for Claude Code execution:
- Use this skill to create the task structure
- Add autonomy levels to descriptions (HIGH/MEDIUM/LOW)
- Include Claude Code prompts in each task description
- Link related tasks using Jira issue links
- Track progress as Claude Code completes tasks
Security Notes
- ⚠️ Never commit API tokens to version control
- ⚠️ Use environment variables for sensitive data
- ⚠️ Rotate tokens regularly (every 90 days recommended)
- ⚠️ Restrict token permissions to minimum required scope
Success Criteria
You've successfully used this skill when:
- ✅ All tasks are created in Jira under correct epics
- ✅ Task descriptions include autonomy levels and prompts
- ✅ Labels and priorities are set correctly
- ✅ Time estimates are realistic and useful for planning
- ✅ No duplicate tasks were created
- ✅ Error handling caught and reported any failures
Advanced Jira Operations
Beyond bulk task creation, this skill covers all Jira REST API operations. Here are code examples for common workflows:
1. Search Issues with JQL
def search_issues(auth, jql, max_results=50):
"""Search Jira issues using JQL (Jira Query Language)."""
params = {
"jql": jql,
"maxResults": max_results,
"fields": "summary,status,assignee,priority,created"
}
response = requests.get(
f"{JIRA_API_URL}/search",
auth=auth,
params=params
)
if response.status_code == 200:
data = response.json()
print(f"[OK] Found {data['total']} issues")
for issue in data['issues']:
print(f" {issue['key']}: {issue['fields']['summary']}")
return data['issues']
else:
print(f"[ERROR] Search failed: {response.status_code}")
return []
2. Get Issue Details
def get_issue(auth, issue_key):
"""Get complete details of a specific issue."""
response = requests.get(
f"{JIRA_API_URL}/issue/{issue_key}",
auth=auth
)
if response.status_code == 200:
issue = response.json()
print(f"[OK] {issue_key}: {issue['fields']['summary']}")
print(f" Status: {issue['fields']['status']['name']}")
print(f" Assignee: {issue['fields']['assignee']['displayName'] if issue['fields']['assignee'] else 'Unassigned'}")
return issue
else:
print(f"[ERROR] Failed to get issue: {response.status_code}")
return None
3. Update Issue Fields
def update_issue(auth, issue_key, fields):
"""Update fields on an existing issue."""
payload = {"fields": fields}
response = requests.put(
f"{JIRA_API_URL}/issue/{issue_key}",
auth=auth,
headers={"Content-Type": "application/json"},
json=payload
)
if response.status_code == 204:
print(f"[OK] Updated {issue_key}")
return True
else:
print(f"[ERROR] Failed to update: {response.status_code}")
print(f" Response: {response.text}")
return False
4. Transition Issue (Change Status)
def get_transitions(auth, issue_key):
"""Get available transitions for an issue."""
response = requests.get(
f"{JIRA_API_URL}/issue/{issue_key}/transitions",
auth=auth
)
if response.status_code == 200:
transitions = response.json()['transitions']
print(f"[OK] Available transitions for {issue_key}:")
for t in transitions:
print(f" {t['id']}: {t['name']}")
return transitions
else:
print(f"[ERROR] Failed to get transitions: {response.status_code}")
return []
def transition_issue(auth, issue_key, transition_id):
"""Transition an issue to a new status."""
payload = {
"transition": {"id": transition_id}
}
response = requests.post(
f"{JIRA_API_URL}/issue/{issue_key}/transitions",
auth=auth,
headers={"Content-Type": "application/json"},
json=payload
)
if response.status_code == 204:
print(f"[OK] Transitioned {issue_key}")
return True
else:
()
5. Add Comments
def add_comment(auth, issue_key, comment_text):
"""Add a comment to an issue."""
payload = {
"body": {
"type": "doc",
"version": 1,
"content": [{
"type": "paragraph",
"content": [{
"type": "text",
"text": comment_text
}]
}]
}
}
response = requests.post(
f"{JIRA_API_URL}/issue/{issue_key}/comment",
auth=auth,
headers={"Content-Type": "application/json"},
json=payload
)
if response.status_code == 201:
print(f"[OK] Added comment to {issue_key}")
return True
else:
print(f"[ERROR] Failed to add comment: {response.status_code}")
return False
6. Link Issues
def link_issues(auth, inward_issue, outward_issue, link_type="Relates"):
"""Create a link between two issues."""
payload = {
"type": {"name": link_type},
"inwardIssue": {"key": inward_issue},
"outwardIssue": {"key": outward_issue}
}
response = requests.post(
f"{JIRA_API_URL}/issueLink",
auth=auth,
headers={"Content-Type": "application/json"},
json=payload
)
if response.status_code == 201:
print(f"[OK] Linked {inward_issue} {link_type} {outward_issue}")
return True
else:
print(f"[ERROR] Failed to link issues: {response.status_code}")
return False
7. Link Issue to Epic
def link_to_epic(auth, issue_key, epic_key):
"""Link an issue to an epic (parent)."""
payload = {
"fields": {
"parent": {"key": epic_key}
}
}
response = requests.put(
f"{JIRA_API_URL}/issue/{issue_key}",
auth=auth,
headers={"Content-Type": "application/json"},
json=payload
)
if response.status_code == 204:
print(f"[OK] Linked {issue_key} to epic {epic_key}")
return True
else:
print(f"[ERROR] Failed to link to epic: {response.status_code}")
return False
8. Add Work Log (Time Tracking)
def add_worklog(auth, issue_key, time_spent, comment=""):
"""Log time spent on an issue."""
payload = {
"timeSpent": time_spent,
"comment": {
"type": "doc",
"version": 1,
"content": [{
"type": "paragraph",
"content": [{
"type": "text",
"text": comment
}]
}]
} if comment else None
}
response = requests.post(
f"{JIRA_API_URL}/issue/{issue_key}/worklog",
auth=auth,
headers={"Content-Type": "application/json"},
json=payload
)
if response.status_code == 201:
print(f"[OK] Logged {time_spent} on {issue_key}")
return True
else:
print(f"[ERROR] Failed to log work: {response.status_code}")
return False
9. Get All Projects
def get_all_projects(auth):
"""Get list of all accessible Jira projects."""
response = requests.get(
f"{JIRA_API_URL}/project",
auth=auth
)
if response.status_code == 200:
projects = response.json()
print(f"[OK] Found {len(projects)} projects:")
for project in projects:
print(f" {project['key']}: {project['name']}")
return projects
else:
print(f"[ERROR] Failed to get projects: {response.status_code}")
return []
10. Batch Create Issues
def batch_create_issues(auth, issues):
"""Create multiple issues in a single API call."""
payload = {
"issueUpdates": [
{"fields": issue} for issue in issues
]
}
response = requests.post(
f"{JIRA_API_URL}/issue/bulk",
auth=auth,
headers={"Content-Type": "application/json"},
json=payload
)
if response.status_code == 201:
results = response.json()
print(f"[OK] Created {len(results['issues'])} issues")
for issue in results['issues']:
print(f" {issue['key']}")
return results
else:
print(f"[ERROR] Batch creation failed: {response.status_code}")
return None
11. Create Sprint
def create_sprint(auth, board_id, sprint_name, start_date=None, end_date=None):
"""Create a new sprint for an agile board."""
payload = {
"name": sprint_name,
"originBoardId": board_id,
}
if start_date:
payload["startDate"] = start_date
if end_date:
payload["endDate"] = end_date
response = requests.post(
f"{JIRA_BASE_URL}/rest/agile/1.0/sprint",
auth=auth,
headers={"Content-Type": "application/json"},
json=payload
)
if response.status_code == 201:
sprint = response.json()
print(f"[OK] Created sprint: {sprint['name']} (ID: {sprint['id']})")
return sprint
else:
print(f"[ERROR] Failed to create sprint: {response.status_code}")
return None
12. Get Board Issues
def get_board_issues(auth, board_id, jql_filter=""):
"""Get issues from a specific agile board."""
params = {"maxResults": 100}
if jql_filter:
params["jql"] = jql_filter
response = requests.get(
f"{JIRA_BASE_URL}/rest/agile/1.0/board/{board_id}/issue",
auth=auth,
params=params
)
if response.status_code == 200:
data = response.json()
print(f"[OK] Found {data['total']} issues on board")
return data['issues']
else:
print(f"[ERROR] Failed to get board issues: {response.status_code}")
return []
Common JQL Query Examples
Jira Query Language (JQL) is powerful for filtering issues. Here are common queries:
"project = DP01"
"assignee = currentUser()"
"project = DP01 AND status = 'In Progress'"
"created >= -7d"
"project = DP01 AND issuetype = Bug AND priority in (High, Highest)"
"labels = Track-3-Platform"
"duedate < now() AND status != Done"
"updated >= -1d"
"project = DP01 AND assignee = currentUser() AND status in ('To Do', 'In Progress') AND labels = urgent ORDER BY priority DESC"
"'Epic Link' = DP01-21"
"sprint in openSprints() AND assignee is EMPTY"
"issueFunction in linkedIssuesOf('project = DP01', 'blocks')"
Integration with Claude Code Workflows
Automated Task Status Updates
When Claude Code completes a task, automatically update Jira:
def complete_claude_task(auth, issue_key, time_spent, implementation_notes):
"""Mark a Claude Code task as complete in Jira."""
add_worklog(auth, issue_key, time_spent, "Implementation completed by Claude Code")
add_comment(auth, issue_key, f"Implementation complete.\n\n{implementation_notes}")
transitions = get_transitions(auth, issue_key)
done_transition = next((t for t in transitions if t['name'].lower() == 'done'), None)
if done_transition:
transition_issue(auth, issue_key, done_transition['id'])
print(f"[OK] Task {issue_key} marked as complete")
else:
print(f"[WARN] Could not find 'Done' transition for {issue_key}")
Sprint Planning Automation
Automate sprint creation and issue assignment:
def setup_sprint(auth, board_id, sprint_name, epic_key, num_days=14):
"""Create sprint and add epic issues to it."""
from datetime import datetime, timedelta
start_date = datetime.now().isoformat() + "Z"
end_date = (datetime.now() + timedelta(days=num_days)).isoformat() + "Z"
sprint = create_sprint(auth, board_id, sprint_name, start_date, end_date)
if not sprint:
return
jql = f"'Epic Link' = {epic_key} AND status = 'To Do'"
issues = search_issues(auth, jql)
for issue in issues:
move_to_sprint(auth, issue['key'], sprint['id'])
print(f"[OK] Sprint '{sprint_name}' created with {len(issues)} issues")
Related Skills
- brainstorming - Use before this skill to refine epic breakdown
- test-driven-development - Use after task creation for implementation
- finishing-a-development-branch - Use when completing tasks
Version History