Atlassian Jira Expert
JQL query writing, custom workflow design, automation rules, dashboard configuration, and Jira best practices for software and project teams.
What this skill does
Organize your team's work by instantly setting up custom Jira projects, automation rules, and visual dashboards tailored to your process. You can request specific data like overdue tasks or team capacity without needing to learn complex search codes or configuration details. Use it whenever you need to launch a new initiative or get clear answers from your existing project boards.
name: “jira-expert” description: Atlassian Jira expert for creating and managing projects, planning, product discovery, JQL queries, workflows, custom fields, automation, reporting, and all Jira features. Use for Jira project setup, configuration, advanced search, dashboard creation, workflow design, and technical Jira operations.
Atlassian Jira Expert
Master-level expertise in Jira configuration, project management, JQL, workflows, automation, and reporting. Handles all technical and operational aspects of Jira.
Quick Start — Most Common Operations
Create a project:
mcp jira create_project --name "My Project" --key "MYPROJ" --type scrum --lead "[email protected]"
Run a JQL query:
mcp jira search_issues --jql "project = MYPROJ AND status != Done AND dueDate < now()" --maxResults 50
For full command reference, see Atlassian MCP Integration. For JQL functions, see JQL Functions Reference. For report templates, see Reporting Templates.
Workflows
Project Creation
- Determine project type (Scrum, Kanban, Bug Tracking, etc.)
- Create project with appropriate template
- Configure project settings:
- Name, key, description
- Project lead and default assignee
- Notification scheme
- Permission scheme
- Set up issue types and workflows
- Configure custom fields if needed
- Create initial board/backlog view
- HANDOFF TO: Scrum Master for team onboarding
Workflow Design
- Map out process states (To Do → In Progress → Done)
- Define transitions and conditions
- Add validators, post-functions, and conditions
- Configure workflow scheme
- Validate: Deploy to a test project first; verify all transitions, conditions, and post-functions behave as expected before associating with production projects
- Associate workflow with project
- Test workflow with sample issues
JQL Query Building
Basic Structure: field operator value
Common Operators:
=, !=: equals, not equals~, !~: contains, not contains>, <, >=, <=: comparisonin, not in: list membershipis empty, is not emptywas, was in, was notchanged
Powerful JQL Examples:
Find overdue issues:
dueDate < now() AND status != Done
Sprint burndown issues:
sprint = 23 AND status changed TO "Done" DURING (startOfSprint(), endOfSprint())
Find stale issues:
updated < -30d AND status != Done
Cross-project epic tracking:
"Epic Link" = PROJ-123 ORDER BY rank
Velocity calculation:
sprint in closedSprints() AND resolution = Done
Team capacity:
assignee in (user1, user2) AND sprint in openSprints()
Dashboard Creation
- Create new dashboard (personal or shared)
- Add relevant gadgets:
- Filter Results (JQL-based)
- Sprint Burndown
- Velocity Chart
- Created vs Resolved
- Pie Chart (status distribution)
- Arrange layout for readability
- Configure automatic refresh
- Share with appropriate teams
- HANDOFF TO: Senior PM or Scrum Master for use
Automation Rules
- Define trigger (issue created, field changed, scheduled)
- Add conditions (if applicable)
- Define actions:
- Update field
- Send notification
- Create subtask
- Transition issue
- Post comment
- Test automation with sample data
- Enable and monitor
Advanced Features
Custom Fields
When to Create:
- Track data not in standard fields
- Capture process-specific information
- Enable advanced reporting
Field Types: Text, Numeric, Date, Select (single/multi/cascading), User picker
Configuration:
- Create custom field
- Configure field context (which projects/issue types)
- Add to appropriate screens
- Update search templates if needed
Issue Linking
Link Types:
- Blocks / Is blocked by
- Relates to
- Duplicates / Is duplicated by
- Clones / Is cloned by
- Epic-Story relationship
Best Practices:
- Use Epic linking for feature grouping
- Use blocking links to show dependencies
- Document link reasons in comments
Permissions & Security
Permission Schemes:
- Browse Projects
- Create/Edit/Delete Issues
- Administer Projects
- Manage Sprints
Security Levels:
- Define confidential issue visibility
- Control access to sensitive data
- Audit security changes
Bulk Operations
Bulk Change:
- Use JQL to find target issues
- Select bulk change operation
- Choose fields to update
- Validate: Preview all changes before executing; confirm the JQL filter matches only intended issues — bulk edits are difficult to reverse
- Execute and confirm
- Monitor background task
Bulk Transitions:
- Move multiple issues through workflow
- Useful for sprint cleanup
- Requires appropriate permissions
- Validate: Run the JQL filter and review results in small batches before applying at scale
JQL Functions Reference
Tip: Save frequently used queries as named filters instead of re-running complex JQL ad hoc. See Best Practices for performance guidance.
Date: startOfDay(), endOfDay(), startOfWeek(), endOfWeek(), startOfMonth(), endOfMonth(), startOfYear(), endOfYear()
Sprint: openSprints(), closedSprints(), futureSprints()
User: currentUser(), membersOf("group")
Advanced: issueHistory(), linkedIssues(), issuesWithFixVersions()
Reporting Templates
Tip: These JQL snippets can be saved as shared filters or wired directly into Dashboard gadgets (see Dashboard Creation).
| Report | JQL |
|---|---|
| Sprint Report | project = PROJ AND sprint = 23 |
| Team Velocity | assignee in (team) AND sprint in closedSprints() AND resolution = Done |
| Bug Trend | type = Bug AND created >= -30d |
| Blocker Analysis | priority = Blocker AND status != Done |
Decision Framework
When to Escalate to Atlassian Admin:
- Need new project permission scheme
- Require custom workflow scheme across org
- User provisioning or deprovisioning
- License or billing questions
- System-wide configuration changes
When to Collaborate with Scrum Master:
- Sprint board configuration
- Backlog prioritization views
- Team-specific filters
- Sprint reporting needs
When to Collaborate with Senior PM:
- Portfolio-level reporting
- Cross-project dashboards
- Executive visibility needs
- Multi-project dependencies
Handoff Protocols
FROM Senior PM:
- Project structure requirements
- Workflow and field needs
- Reporting requirements
- Integration needs
TO Senior PM:
- Cross-project metrics
- Issue trends and patterns
- Workflow bottlenecks
- Data quality insights
FROM Scrum Master:
- Sprint board configuration requests
- Workflow optimization needs
- Backlog filtering requirements
- Velocity tracking setup
TO Scrum Master:
- Configured sprint boards
- Velocity reports
- Burndown charts
- Team capacity views
Best Practices
Data Quality:
- Enforce required fields with field validation rules
- Use consistent issue key naming conventions per project type
- Schedule regular cleanup of stale/orphaned issues
Performance:
- Avoid leading wildcards in JQL (
~on large text fields is expensive) - Use saved filters instead of re-running complex JQL ad hoc
- Limit dashboard gadgets to reduce page load time
- Archive completed projects rather than deleting to preserve history
Governance:
- Document rationale for custom workflow states and transitions
- Version-control permission/workflow schemes before making changes
- Require change management review for org-wide scheme updates
- Run permission audits after user role changes
Atlassian MCP Integration
Primary Tool: Jira MCP Server
Key Operations with Example Commands:
Create a project:
mcp jira create_project --name "My Project" --key "MYPROJ" --type scrum --lead "[email protected]"
Execute a JQL query:
mcp jira search_issues --jql "project = MYPROJ AND status != Done AND dueDate < now()" --maxResults 50
Update an issue field:
mcp jira update_issue --issue "MYPROJ-42" --field "status" --value "In Progress"
Create a sprint:
mcp jira create_sprint --board 10 --name "Sprint 5" --startDate "2024-06-01" --endDate "2024-06-14"
Create a board filter:
mcp jira create_filter --name "Open Blockers" --jql "priority = Blocker AND status != Done" --shareWith "project-team"
Integration Points:
- Pull metrics for Senior PM reporting
- Configure sprint boards for Scrum Master
- Create documentation pages for Confluence Expert
- Support template creation for Template Creator
Related Skills
- Confluence Expert (
project-management/confluence-expert/) — Documentation complements Jira workflows - Atlassian Admin (
project-management/atlassian-admin/) — Permission and user management for Jira projects
Jira Automation Reference
Comprehensive guide to Jira automation rules: triggers, conditions, actions, smart values, and production-ready recipes.
Rule Structure
Every automation rule follows this pattern:
TRIGGER → [CONDITION(s)] → ACTION(s)- Trigger: The event that starts the rule (required, exactly one)
- Condition: Filters to narrow when the rule fires (optional, multiple allowed)
- Action: What the rule does (required, one or more)
Triggers
Issue Triggers
| Trigger | Fires When | Use For |
|---|---|---|
| Issue created | New issue is created | Auto-assignment, notifications, SLA start |
| Issue transitioned | Status changes | Workflow automation, notifications |
| Issue updated | Any field changes | Field sync, cascading updates |
| Issue commented | Comment is added | Auto-responses, SLA tracking |
| Issue assigned | Assignee changes | Workload notifications |
| Issue linked | Link is added/removed | Dependency tracking |
| Issue deleted | Issue is deleted | Cleanup, audit logging |
Sprint & Board Triggers
| Trigger | Fires When |
|---|---|
| Sprint started | Sprint is activated |
| Sprint completed | Sprint is closed |
| Issue moved between sprints | Issue is moved |
| Backlog item moved to sprint | Item is pulled into sprint |
Scheduled Triggers
| Trigger | Fires When |
|---|---|
| Scheduled | Cron-based (daily, weekly, custom) |
| Issue stale | No updates for X days |
Version Triggers
| Trigger | Fires When |
|---|---|
| Version created | New version added |
| Version released | Version is released |
Conditions
Issue Conditions
| Condition | Matches When |
|---|---|
| Issue fields condition | Field matches value (e.g., priority = High) |
| JQL condition | Issue matches JQL query |
| Related issues condition | Linked/sub-task issues match criteria |
| User condition | Actor matches (reporter, assignee, group) |
| Advanced compare | Complex field comparisons |
Condition Operators
Field = value # Exact match
Field != value # Not equal
Field > value # Greater than (numeric/date)
Field is empty # Field has no value
Field is not empty # Field has a value
Field changed # Field was modified in this event
Field changed to # Field changed to specific value
Field changed from # Field changed from specific valueActions
Issue Actions
| Action | Does |
|---|---|
| Edit issue | Update any field on the current issue |
| Transition issue | Move to a new status |
| Assign issue | Change assignee |
| Comment on issue | Add a comment |
| Create issue | Create a new linked issue |
| Create sub-tasks | Create child issues |
| Clone issue | Duplicate the issue |
| Delete issue | Remove the issue |
| Link issues | Add issue links |
| Log work | Add time tracking entry |
Notification Actions
| Action | Does |
|---|---|
| Send email | Send custom email to users/groups |
| Send Slack message | Post to Slack channel (requires integration) |
| Send Microsoft Teams message | Post to Teams (requires integration) |
| Send web request | HTTP call to external service |
Lookup & Branch Actions
| Action | Does |
|---|---|
| Lookup issues (JQL) | Find issues matching JQL, iterate over them |
| Create branch | Branch logic (if/then/else) |
| For each | Loop over found issues |
Smart Values
Smart values are dynamic placeholders that resolve at runtime.
Issue Smart Values
{{issue.key}} # PROJ-123
{{issue.summary}} # Issue title
{{issue.description}} # Full description
{{issue.status.name}} # Current status
{{issue.priority.name}} # Priority level
{{issue.assignee.displayName}} # Assignee name
{{issue.reporter.displayName}} # Reporter name
{{issue.issuetype.name}} # Issue type
{{issue.project.key}} # Project key
{{issue.created}} # Creation date
{{issue.updated}} # Last update date
{{issue.fixVersions}} # Fix versions
{{issue.labels}} # Labels array
{{issue.components}} # Components arrayTransition Smart Values
{{transition.from_status}} # Previous status
{{transition.to_status}} # New status
{{transition.transitionName}} # Transition nameUser Smart Values
{{initiator.displayName}} # Who triggered the rule
{{initiator.emailAddress}} # Their email
{{initiator.accountId}} # Their account IDDate Smart Values
{{now}} # Current timestamp
{{now.plusDays(7)}} # 7 days from now
{{now.minusHours(24)}} # 24 hours ago
{{issue.created.plusBusinessDays(3)}} # 3 business days after creationConditional Smart Values
{{#if issue.priority.name == "High"}}
This is high priority
{{/if}}
{{#if issue.assignee}}
Assigned to {{issue.assignee.displayName}}
{{else}}
Unassigned
{{/if}}Production-Ready Recipes
1. Auto-Assign by Component
Trigger: Issue created
Condition: Issue has component
Action: Edit issue
- Assignee = Component lead
Rule Logic:
IF component = "Backend" → assign to @backend-lead
IF component = "Frontend" → assign to @frontend-lead
IF component = "DevOps" → assign to @devops-lead2. SLA Warning — Stale Issues
Trigger: Scheduled (daily at 9am)
Condition: JQL = "status != Done AND updated <= -5d AND priority in (High, Highest)"
Action:
- Add comment: "⚠️ This {{issue.priority.name}} issue hasn't been updated in 5+ days."
- Send Slack: "#engineering-alerts: {{issue.key}} is stale ({{issue.assignee.displayName}})"3. Auto-Close Resolved Issues After 7 Days
Trigger: Scheduled (daily)
Condition: JQL = "status = Resolved AND updated <= -7d"
Action:
- Transition: Resolved → Closed
- Comment: "Auto-closed after 7 days in Resolved status."4. Sprint Spillover Notification
Trigger: Sprint completed
Condition: Issue status != Done
Action:
- Comment: "Spilled over from Sprint {{sprint.name}}. Reason needs review."
- Add label: "spillover"
- Send email to: {{issue.assignee.emailAddress}}5. Sub-Task Completion → Parent Transition
Trigger: Issue transitioned (to Done)
Condition: Issue is sub-task AND all sibling sub-tasks are Done
Action (on parent):
- Transition: In Progress → In Review
- Comment: "All sub-tasks completed. Ready for review."6. Bug Priority Escalation
Trigger: Scheduled (every 4 hours)
Condition: JQL = "type = Bug AND priority = High AND status = Open AND created <= -24h"
Action:
- Edit: priority = Highest
- Comment: "⚡ Auto-escalated: High-priority bug open for 24+ hours."
- Send email to: project lead7. Auto-Link Duplicate Detection
Trigger: Issue created
Condition: JQL finds issues with similar summary (fuzzy)
Action:
- Comment: "Possible duplicate of {{lookupIssues.first.key}}: {{lookupIssues.first.summary}}"
- Add label: "possible-duplicate"8. Release Notes Generator
Trigger: Version released
Condition: None
Action:
- Lookup: JQL = "fixVersion = {{version.name}} AND status = Done"
- Create Confluence page:
Title: "Release Notes — {{version.name}}"
Content: List of resolved issues with types and summaries9. Workload Balancer — Round-Robin Assignment
Trigger: Issue created
Condition: Issue type = Story AND assignee is empty
Action:
- Lookup: JQL = "assignee in (dev1, dev2, dev3) AND sprint in openSprints() AND status != Done"
- Assign to team member with fewest open issues10. Blocker Notification Chain
Trigger: Issue updated (priority changed to Blocker)
Action:
- Send email to: project lead, scrum master
- Send Slack: "#blockers: 🚨 {{issue.key}} marked as Blocker by {{initiator.displayName}}"
- Comment: "Blocker escalated. Notified: PM + SM."
- Edit: Add label "blocker-active"Best Practices
- Name rules descriptively — "Auto-assign Backend bugs to @dev-lead" not "Rule 1"
- Add conditions before actions — prevent unintended execution
- Use JQL conditions for precision — field conditions can miss edge cases
- Test in a sandbox project first — automation mistakes can be destructive
- Set rate limits — avoid infinite loops (Rule A triggers Rule B triggers Rule A)
- Monitor rule execution — check Automation audit log weekly
- Document business rules — explain WHY the rule exists, not just WHAT it does
- Use branches (if/else) over separate rules — reduces rule count, easier to maintain
- Disable before deleting — observe for a week to ensure no side effects
- Version your automation — export rules as JSON backup before major changes
Jira Workflows Reference
Comprehensive guide to Jira workflow design, transitions, conditions, validators, and post-functions.
Default Workflows
Simplified Workflow
Open → In Progress → DoneSoftware Development Workflow
Backlog → Selected for Development → In Progress → In Review → Done
↑___________________________| (reopen)Bug Tracking Workflow
Open → In Progress → Fixed → Verified → Closed
↑ | |
|____Reopened________|________|Custom Workflow Design
Design Principles
- Mirror your actual process — don't force teams into artificial states
- Minimize statuses — each status must represent a distinct work state where the item waits for a different action
- Clear ownership — every status should have an obvious responsible party
- Allow rework — always provide paths back for rejected/reopened items
- Separate "waiting" from "working" — distinguish "In Review" (waiting) from "Reviewing" (actively working)
Status Categories
Jira maps every status to one of four categories that drive board columns and JQL:
| Category | Meaning | JQL | Examples |
|---|---|---|---|
To Do |
Not started | statusCategory = "To Do" |
Backlog, Open, New |
In Progress |
Active work | statusCategory = "In Progress" |
In Progress, In Review, Testing |
Done |
Completed | statusCategory = Done |
Done, Closed, Released |
Undefined |
Legacy/unused | — | Avoid using |
Recommended Statuses by Team Type
Engineering Team:
Backlog → Ready → In Progress → Code Review → QA → DoneSupport Team:
New → Triaged → In Progress → Waiting on Customer → Resolved → ClosedDesign Team:
Backlog → Research → Design → Review → Approved → HandoffTransitions
Transition Properties
| Property | Description |
|---|---|
| Name | Display name on the button (e.g., "Start Work") |
| Screen | Form shown during transition (optional) |
| Conditions | Who can trigger this transition |
| Validators | Rules that must pass before transition executes |
| Post-functions | Actions executed after transition completes |
Common Transition Patterns
Start Work:
Trigger: "Start Work" button
Condition: Assignee only
Validator: Issue must have assignee
Post-function: Set "In Progress" resolution to NoneSubmit for Review:
Trigger: "Submit for Review" button
Condition: Assignee or project admin
Validator: All sub-tasks must be Done
Post-function: Add comment "Submitted for review by {user}"Approve:
Trigger: "Approve" button
Condition: Must be in "Reviewers" group
Validator: Must add comment
Post-function: Set resolution to "Done", fire eventConditions
Built-in Conditions
| Condition | Use When |
|---|---|
| Only Assignee | Only assigned user can transition |
| Only Reporter | Only creator can transition |
| Permission Condition | User must have specific permission |
| Group Condition | User must be in specified group |
| Sub-Task Blocking | All sub-tasks must be resolved |
| Previous Status | Issue must have been in a specific status |
| User Is In Role | User must have project role (Developer, Admin) |
Combining Conditions
- AND logic: Add multiple conditions to one transition — ALL must pass
- OR logic: Create parallel transitions with different conditions
Validators
Built-in Validators
| Validator | Checks |
|---|---|
| Required Field | Specific field must be populated |
| Field Has Been Modified | Field must change during transition |
| Regular Expression | Field must match regex pattern |
| Permission Validator | User must have permission |
| Previous Status Validator | Issue was in a required status |
Common Validator Patterns
# Require comment on rejection
Validator: Comment Required
When: Transition = "Reject"
# Require fix version before release
Validator: Required Field = "Fix Version/s"
When: Transition = "Release"
# Require time logged before closing
Validator: Field Required = "Time Spent" (must be > 0)
When: Transition = "Close"Post-Functions
Built-in Post-Functions
| Post-Function | Action |
|---|---|
| Set Field Value | Assign a value to any field |
| Update Issue Field | Change assignee, priority, etc. |
| Create Comment | Add automated comment |
| Fire Event | Trigger notification event |
| Assign to Lead | Assign to project lead |
| Assign to Reporter | Assign back to creator |
| Clear Field | Remove field value |
| Copy Value | Copy field from parent/linked issue |
Post-Function Execution Order
Post-functions execute in defined order. Standard sequence:
- Set issue status (automatic, always first)
- Add comment (if configured)
- Update fields
- Generate change history (automatic, always last)
- Fire event (triggers notifications)
Important: "Generate change history" and "Fire event" must always be last — reorder if you add custom post-functions.
Workflow Schemes
What They Do
- Map issue types to workflows within a project
- One workflow scheme per project
- Different issue types can use different workflows
Configuration Pattern
Project: MYPROJ
Workflow Scheme: "Engineering Workflow Scheme"
Bug → Bug Tracking Workflow
Story → Development Workflow
Task → Simple Workflow
Epic → Epic Workflow
Sub-task → Sub-task Workflow (inherits parent transitions)Best Practices
- Start simple, add complexity only when needed — a 5-status workflow beats a 15-status one
- Name transitions as actions — "Start Work" not "In Progress" (the status is "In Progress", the action is "Start Work")
- Use screens sparingly — only show a screen when you need data from the user during transition
- Test with real users — workflows that look good on paper may confuse the team
- Document your workflow — add descriptions to statuses and transitions
- Use global transitions carefully — a "Cancel" transition from any status is convenient but can bypass important gates
- Audit quarterly — remove statuses with <5% usage
Jira Automation Examples
Auto-Assignment Rules
Auto-assign by component
Trigger: Issue created Conditions:
- Component is not EMPTY Actions:
- Assign issue to component lead
Auto-assign to reporter for feedback
Trigger: Issue transitioned to "Waiting for Feedback" Actions:
- Assign issue to reporter
- Add comment: "Please provide additional information"
Round-robin assignment
Trigger: Issue created Conditions:
- Project = ABC
- Assignee is EMPTY Actions:
- Assign to next team member in rotation (use smart value)
Status Sync Rules
Sync subtask status to parent
Trigger: Issue transitioned Conditions:
- Issue type = Sub-task
- Transition is to "Done"
- Parent issue exists
- All subtasks are Done Actions:
- Transition parent issue to "Done"
Sync parent to subtasks
Trigger: Issue transitioned Conditions:
- Issue type has subtasks
- Transition is to "Cancelled" Actions:
- For each: Sub-tasks
- Transition issue to "Cancelled"
Epic progress tracking
Trigger: Issue transitioned Conditions:
- Epic link is not EMPTY
- Transition is to "Done" Actions:
- Add comment to epic: "{{issue.key}} completed"
- Update epic custom field "Progress"
Notification Rules
Slack notification for high-priority bugs
Trigger: Issue created Conditions:
- Issue type = Bug
- Priority IN (Highest, High) Actions:
- Send Slack message to #engineering:
🚨 High Priority Bug Created {{issue.key}}: {{issue.summary}} Reporter: {{issue.reporter.displayName}} Priority: {{issue.priority.name}} {{issue.url}}
Email assignee when mentioned
Trigger: Issue commented Conditions:
- Comment contains @mention of assignee Actions:
- Send email to {{issue.assignee.emailAddress}}:
Subject: You were mentioned in {{issue.key}} Body: {{comment.author.displayName}} mentioned you: {{comment.body}}
SLA breach warning
Trigger: Scheduled - Every hour Conditions:
- Status != Done
- SLA time remaining < 2 hours Actions:
- Send email to {{issue.assignee}}
- Add comment: "⚠️ SLA expires in <2 hours"
- Set priority to Highest
Field Automation Rules
Auto-set due date
Trigger: Issue created Conditions:
- Issue type = Bug
- Priority = Highest Actions:
- Set due date to {{now.plusDays(1)}}
Clear assignee when in backlog
Trigger: Issue transitioned Conditions:
- Transition is to "Backlog"
- Assignee is not EMPTY Actions:
- Assign issue to Unassigned
- Add comment: "Returned to backlog, assignee cleared"
Auto-populate sprint field
Trigger: Issue transitioned Conditions:
- Transition is to "In Progress"
- Sprint is EMPTY Actions:
- Add issue to current sprint
Set fix version based on component
Trigger: Issue created Conditions:
- Component = "Mobile App" Actions:
- Set fix version to "Mobile v2.0"
Escalation Rules
Auto-escalate stale issues
Trigger: Scheduled - Daily at 9:00 AM Conditions:
- Status = "Waiting for Response"
- Updated < -7 days Actions:
- Add comment: "@{{issue.reporter}} This issue needs attention"
- Send email to project lead
- Add label: "needs-attention"
Escalate overdue critical issues
Trigger: Scheduled - Every hour Conditions:
- Priority IN (Highest, High)
- Due date < now()
- Status != Done Actions:
- Transition to "Escalated"
- Assign to project manager
- Send Slack notification
Auto-close inactive issues
Trigger: Scheduled - Daily at 10:00 AM Conditions:
- Status = "Waiting for Customer"
- Updated < -30 days Actions:
- Transition to "Closed"
- Add comment: "Auto-closed due to inactivity"
- Send email to reporter
Sprint Automation Rules
Move incomplete work to next sprint
Trigger: Sprint closed Conditions:
- Issue status != Done Actions:
- Add issue to next sprint
- Add comment: "Moved from {{sprint.name}}"
Auto-remove completed items from active sprint
Trigger: Issue transitioned Conditions:
- Transition is to "Done"
- Sprint IN openSprints() Actions:
- Remove issue from sprint
- Add comment: "Removed from active sprint (completed)"
Sprint start notification
Trigger: Sprint started Actions:
- Send Slack message to #team:
🚀 Sprint {{sprint.name}} Started! Goal: {{sprint.goal}} Committed: {{sprint.issuesCount}} issues
Approval Workflow Rules
Request approval for large stories
Trigger: Issue created Conditions:
- Issue type = Story
- Story points >= 13 Actions:
- Transition to "Pending Approval"
- Assign to product owner
- Send email notification
Auto-approve small bugs
Trigger: Issue created Conditions:
- Issue type = Bug
- Priority IN (Low, Lowest) Actions:
- Transition to "Approved"
- Add comment: "Auto-approved (low-priority bug)"
Require security review
Trigger: Issue transitioned Conditions:
- Transition is to "Ready for Release"
- Labels contains "security" Actions:
- Transition to "Security Review"
- Assign to security-team
- Send email to [email protected]
Integration Rules
Create GitHub issue
Trigger: Issue transitioned Conditions:
- Transition is to "In Progress"
- Labels contains "needs-tracking" Actions:
- Send webhook to GitHub API:
{ "title": "{{issue.key}}: {{issue.summary}}", "body": "{{issue.description}}", "assignee": "{{issue.assignee.name}}" }
Update Confluence page
Trigger: Issue transitioned Conditions:
- Issue type = Epic
- Transition is to "Done" Actions:
- Send webhook to Confluence:
- Update epic status page
- Add completion date
Quality & Testing Rules
Require test cases for features
Trigger: Issue transitioned Conditions:
- Issue type = Story
- Transition is to "Ready for QA"
- Custom field "Test Cases" is EMPTY Actions:
- Transition back to "In Progress"
- Add comment: "❌ Test cases required before QA"
Auto-create test issue
Trigger: Issue transitioned Conditions:
- Issue type = Story
- Transition is to "Ready for QA" Actions:
- Create linked issue:
- Type: Test
- Summary: "Test: {{issue.summary}}"
- Link type: "tested by"
- Assignee: QA team
Flag regression bugs
Trigger: Issue created Conditions:
- Issue type = Bug
- Affects version is in released versions Actions:
- Add label: "regression"
- Set priority to High
- Add comment: "🚨 Regression in released version"
Documentation Rules
Require documentation for features
Trigger: Issue transitioned Conditions:
- Issue type = Story
- Labels contains "customer-facing"
- Transition is to "Done"
- Custom field "Documentation Link" is EMPTY Actions:
- Reopen issue
- Add comment: "📝 Documentation required for customer-facing feature"
Auto-create doc task
Trigger: Issue transitioned Conditions:
- Issue type = Epic
- Transition is to "In Progress" Actions:
- Create subtask:
- Type: Task
- Summary: "Documentation for {{issue.summary}}"
- Assignee: {{issue.assignee}}
Time Tracking Rules
Log work reminder
Trigger: Issue transitioned Conditions:
- Transition is to "Done"
- Time spent is EMPTY Actions:
- Add comment: "⏱️ Reminder: Please log your time"
Warn on high time spent
Trigger: Work logged Conditions:
- Time spent > original estimate * 1.5 Actions:
- Add comment: "⚠️ Time spent exceeds estimate by 50%"
- Send notification to assignee and project manager
Advanced Conditional Rules
Conditional assignee based on priority
Trigger: Issue created Conditions:
- Issue type = Bug Actions:
- If: Priority = Highest
- Assign to on-call engineer
- Else if: Priority = High
- Assign to team lead
- Else:
- Assign to next available team member
Multi-step approval flow
Trigger: Issue transitioned Conditions:
- Transition is to "Request Approval"
- Budget estimate > $10,000 Actions:
- If: Budget > $50,000
- Assign to CFO
- Send email to executive team
- Else if: Budget > $10,000
- Assign to Director
- Add comment: "Director approval required"
- Add label: "pending-approval"
Smart Value Examples
Dynamic assignee based on component
{{issue.components.first.lead.accountId}}Days since created
{{issue.created.diff(now).days}}Conditional message
{{#if(issue.priority.name == "Highest")}}
🚨 CRITICAL
{{else}}
ℹ️ Normal priority
{{/}}List all subtasks
{{#issue.subtasks}}
- {{key}}: {{summary}} ({{status.name}})
{{/}}Calculate completion percentage
{{issue.subtasks.filter(item => item.status.statusCategory.key == "done").size.divide(issue.subtasks.size).multiply(100).round()}}%Best Practices
- Test in sandbox - Always test rules on test project first
- Start simple - Begin with basic rules, add complexity incrementally
- Use conditions wisely - Narrow scope to reduce unintended triggers
- Monitor audit log - Check automation execution history regularly
- Limit actions - Keep rules focused, don't chain too many actions
- Name clearly - Use descriptive names: "Auto-assign bugs to component lead"
- Document rules - Add description explaining purpose and owner
- Review regularly - Audit rules quarterly, disable unused ones
- Handle errors - Add error handling for webhooks and integrations
- Performance - Avoid scheduled rules that query large datasets hourly
JQL Query Examples
Sprint Queries
Current sprint issues:
sprint IN openSprints() ORDER BY rankIssues in specific sprint:
sprint = "Sprint 23" ORDER BY priority DESCAll sprint work (current and backlog):
project = ABC AND issuetype IN (Story, Bug, Task)
ORDER BY sprint DESC, rankUnscheduled stories:
project = ABC AND issuetype = Story AND sprint IS EMPTY
AND status != Done ORDER BY priority DESCSpillover from last sprint:
sprint IN closedSprints() AND sprint NOT IN (latestReleasedVersion())
AND status != Done ORDER BY created DESCSprint completion rate:
sprint = "Sprint 23" AND status = DoneUser & Team Queries
My open issues:
assignee = currentUser() AND status != Done
ORDER BY priority DESC, created ASCUnassigned in my project:
project = ABC AND assignee IS EMPTY AND status != Done
ORDER BY priority DESCIssues I'm watching:
watcher = currentUser() AND status != DoneTeam workload:
assignee IN membersOf("engineering-team") AND status IN ("In Progress", "In Review")
ORDER BY assignee, priority DESCIssues I reported that are still open:
reporter = currentUser() AND status != Done ORDER BY created DESCIssues commented on by me:
comment ~ currentUser() AND status != DoneDate Range Queries
Created today:
created >= startOfDay() ORDER BY created DESCUpdated in last 7 days:
updated >= -7d ORDER BY updated DESCCreated this week:
created >= startOfWeek() AND created <= endOfWeek()Created this month:
created >= startOfMonth() AND created <= endOfMonth()Not updated in 30 days:
status != Done AND updated <= -30d ORDER BY updated ASCResolved yesterday:
resolved >= startOfDay(-1d) AND resolved < startOfDay()Due this week:
duedate >= startOfWeek() AND duedate <= endOfWeek() AND status != DoneOverdue:
duedate < now() AND status != Done ORDER BY duedate ASCStatus & Workflow Queries
In Progress issues:
project = ABC AND status = "In Progress" ORDER BY assigneeBlocked issues:
project = ABC AND labels = blocked AND status != DoneIssues in review:
project = ABC AND status IN ("Code Review", "QA Review", "Pending Approval")
ORDER BY updated ASCReady for development:
project = ABC AND status = "Ready" AND sprint IS EMPTY
ORDER BY priority DESCRecently done:
project = ABC AND status = Done AND resolved >= -7d
ORDER BY resolved DESCStatus changed today:
status CHANGED AFTER startOfDay() ORDER BY updated DESCLong-running in progress:
status = "In Progress" AND status CHANGED BEFORE -14d
ORDER BY status CHANGED ASCPriority & Type Queries
High priority bugs:
issuetype = Bug AND priority IN (Highest, High) AND status != Done
ORDER BY priority DESC, created ASCCritical blockers:
priority = Highest AND status != Done ORDER BY created ASCAll epics:
issuetype = Epic ORDER BY status, priority DESCStories without acceptance criteria:
issuetype = Story AND "Acceptance Criteria" IS EMPTY AND status = BacklogTechnical debt:
labels = tech-debt AND status != Done ORDER BY priority DESCComplex Multi-Condition Queries
My team's sprint work:
sprint IN openSprints()
AND assignee IN membersOf("engineering-team")
AND status != Done
ORDER BY assignee, priority DESCBugs created this month, not in sprint:
issuetype = Bug
AND created >= startOfMonth()
AND sprint IS EMPTY
AND status != Done
ORDER BY priority DESC, created DESCHigh-priority work needing attention:
project = ABC
AND priority IN (Highest, High)
AND status IN ("In Progress", "In Review")
AND updated <= -3d
ORDER BY priority DESC, updated ASCStale issues:
project = ABC
AND status NOT IN (Done, Cancelled)
AND (assignee IS EMPTY OR updated <= -30d)
ORDER BY created ASCEpic progress:
"Epic Link" = ABC-123 ORDER BY status, rankComponent & Version Queries
Issues in component:
project = ABC AND component = "Frontend" AND status != DoneIssues without component:
project = ABC AND component IS EMPTY AND status != DoneTarget version:
fixVersion = "v2.0" ORDER BY status, priority DESCReleased versions:
fixVersion IN releasedVersions() ORDER BY fixVersion DESCLabel & Text Search Queries
Issues with label:
labels = urgent AND status != DoneMultiple labels (AND):
labels IN (frontend, bug) AND status != DoneSearch in summary:
summary ~ "authentication" ORDER BY created DESCSearch in summary and description:
text ~ "API integration" ORDER BY created DESCIssues with empty description:
description IS EMPTY AND issuetype = StoryPerformance-Optimized Queries
Good - Specific project first:
project = ABC AND status = "In Progress" AND assignee = currentUser()Bad - User filter first:
assignee = currentUser() AND status = "In Progress" AND project = ABCGood - Use functions:
sprint IN openSprints() AND status != DoneBad - Hardcoded sprint:
sprint = "Sprint 23" AND status != DoneGood - Specific date:
created >= 2024-01-01 AND created <= 2024-01-31Bad - Relative with high cost:
created >= -365d AND created <= -335dReporting Queries
Velocity calculation:
sprint = "Sprint 23" AND status = DoneThen sum story points
Bug rate:
project = ABC AND issuetype = Bug AND created >= startOfMonth()Average cycle time:
project = ABC AND resolved >= startOfMonth()
AND resolved <= endOfMonth()Calculate time from In Progress to Done
Stories delivered this quarter:
project = ABC AND issuetype = Story
AND resolved >= startOfYear() AND resolved <= endOfQuarter()Team capacity:
assignee IN membersOf("engineering-team")
AND sprint IN openSprints()Sum original estimates
Notification & Watching Queries
Issues I need to review:
status = "Pending Review" AND assignee = currentUser()Issues assigned to me, high priority:
assignee = currentUser() AND priority IN (Highest, High)
AND status != DoneIssues created by me, not resolved:
reporter = currentUser() AND status != Done
ORDER BY created DESCAdvanced Functions
Issues changed from status:
status WAS "In Progress" AND status = "Done"
AND status CHANGED AFTER startOfWeek()Assignee changed:
assignee CHANGED BY currentUser() AFTER -7dIssues re-opened:
status WAS Done AND status != Done ORDER BY updated DESCLinked issues:
issue IN linkedIssues("ABC-123") ORDER BY issuetypeParent epic:
parent = ABC-123 ORDER BY rankSaved Filter Examples
Daily Standup Filter:
assignee = currentUser() AND sprint IN openSprints()
AND status != Done ORDER BY priority DESCTeam Sprint Board Filter:
project = ABC AND sprint IN openSprints() ORDER BY rankBugs Dashboard Filter:
project = ABC AND issuetype = Bug AND status != Done
ORDER BY priority DESC, created ASCTech Debt Backlog:
project = ABC AND labels = tech-debt AND status = Backlog
ORDER BY priority DESCNeeds Triage:
project = ABC AND status = "To Triage"
AND created >= -7d ORDER BY created ASC #!/usr/bin/env python3
"""
JQL Query Builder
Pattern-matching JQL builder from natural language descriptions. Maps common
phrases to JQL operators and constructs valid queries with syntax validation.
Usage:
python jql_query_builder.py "high priority bugs in PROJECT assigned to me"
python jql_query_builder.py "overdue tasks in PROJ" --format json
python jql_query_builder.py --patterns
"""
import argparse
import json
import re
import sys
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Pattern Library
# ---------------------------------------------------------------------------
PATTERN_LIBRARY = {
"my_open_bugs": {
"phrases": ["my open bugs", "my bugs", "bugs assigned to me"],
"jql": 'assignee = currentUser() AND type = Bug AND status != Done',
"description": "All open bugs assigned to current user",
},
"high_priority_bugs": {
"phrases": ["high priority bugs", "critical bugs", "urgent bugs", "p1 bugs"],
"jql": 'type = Bug AND priority in (Highest, High) AND status != Done',
"description": "High and highest priority open bugs",
},
"my_open_tasks": {
"phrases": ["my open tasks", "my tasks", "tasks assigned to me", "my work"],
"jql": 'assignee = currentUser() AND status != Done',
"description": "All open issues assigned to current user",
},
"unassigned_issues": {
"phrases": ["unassigned", "unassigned issues", "no assignee"],
"jql": 'assignee is EMPTY AND status != Done',
"description": "Issues with no assignee",
},
"recently_created": {
"phrases": ["recently created", "new issues", "created this week", "recent"],
"jql": 'created >= -7d ORDER BY created DESC',
"description": "Issues created in the last 7 days",
},
"recently_updated": {
"phrases": ["recently updated", "updated this week", "recent changes"],
"jql": 'updated >= -7d ORDER BY updated DESC',
"description": "Issues updated in the last 7 days",
},
"overdue": {
"phrases": ["overdue", "past due", "missed deadline", "overdue tasks"],
"jql": 'duedate < now() AND status != Done',
"description": "Issues past their due date",
},
"due_this_week": {
"phrases": ["due this week", "due soon", "upcoming deadlines"],
"jql": 'duedate >= startOfWeek() AND duedate <= endOfWeek() AND status != Done',
"description": "Issues due this week",
},
"blocked_issues": {
"phrases": ["blocked", "blocked issues", "impediments"],
"jql": 'status = Blocked OR status = Impediment',
"description": "Issues in blocked or impediment status",
},
"in_progress": {
"phrases": ["in progress", "being worked on", "active work"],
"jql": 'status = "In Progress"',
"description": "Issues currently in progress",
},
"sprint_issues": {
"phrases": ["current sprint", "this sprint", "active sprint"],
"jql": 'sprint in openSprints()',
"description": "Issues in the current active sprint",
},
"backlog": {
"phrases": ["backlog", "backlog items", "not started"],
"jql": 'sprint is EMPTY AND status = "To Do" ORDER BY priority DESC',
"description": "Issues in the backlog not assigned to a sprint",
},
"stories_without_estimates": {
"phrases": ["no estimates", "unestimated", "missing estimates", "no story points"],
"jql": 'type = Story AND (storyPoints is EMPTY OR storyPoints = 0) AND status != Done',
"description": "Stories missing story point estimates",
},
"epics_in_progress": {
"phrases": ["active epics", "epics in progress", "open epics"],
"jql": 'type = Epic AND status != Done ORDER BY priority DESC',
"description": "Epics that are not yet completed",
},
"done_this_week": {
"phrases": ["done this week", "completed this week", "resolved this week"],
"jql": 'status changed to Done DURING (startOfWeek(), now())',
"description": "Issues completed during the current week",
},
"created_vs_resolved": {
"phrases": ["created vs resolved", "issue flow", "throughput"],
"jql": 'created >= -30d ORDER BY created DESC',
"description": "Issues created in the last 30 days for flow analysis",
},
"my_reported_issues": {
"phrases": ["my reported", "reported by me", "i created", "i reported"],
"jql": 'reporter = currentUser() ORDER BY created DESC',
"description": "Issues reported by current user",
},
"stale_issues": {
"phrases": ["stale", "stale issues", "not updated", "abandoned"],
"jql": 'updated <= -30d AND status != Done ORDER BY updated ASC',
"description": "Issues not updated in 30+ days",
},
"subtasks_without_parent": {
"phrases": ["orphan subtasks", "subtasks no parent", "loose subtasks"],
"jql": 'type = Sub-task AND parent is EMPTY',
"description": "Subtasks missing parent issues",
},
"high_priority_unassigned": {
"phrases": ["high priority unassigned", "urgent unassigned", "critical no owner"],
"jql": 'priority in (Highest, High) AND assignee is EMPTY AND status != Done',
"description": "High priority issues with no assignee",
},
"bugs_by_component": {
"phrases": ["bugs by component", "component bugs"],
"jql": 'type = Bug AND status != Done ORDER BY component ASC',
"description": "Open bugs organized by component",
},
"resolved_recently": {
"phrases": ["resolved recently", "recently resolved", "fixed this month"],
"jql": 'resolved >= -30d ORDER BY resolved DESC',
"description": "Issues resolved in the last 30 days",
},
}
# Keyword-to-JQL fragment mapping for dynamic query building
KEYWORD_FRAGMENTS = {
# Issue types
"bug": ("type", "= Bug"),
"bugs": ("type", "= Bug"),
"story": ("type", "= Story"),
"stories": ("type", "= Story"),
"task": ("type", "= Task"),
"tasks": ("type", "= Task"),
"epic": ("type", "= Epic"),
"epics": ("type", "= Epic"),
"subtask": ("type", "= Sub-task"),
"sub-task": ("type", "= Sub-task"),
# Statuses
"open": ("status", "!= Done"),
"closed": ("status", "= Done"),
"done": ("status", "= Done"),
"resolved": ("status", "= Done"),
"todo": ("status", '= "To Do"'),
# Priorities
"critical": ("priority", "= Highest"),
"highest": ("priority", "= Highest"),
"high": ("priority", "in (Highest, High)"),
"medium": ("priority", "= Medium"),
"low": ("priority", "in (Low, Lowest)"),
"lowest": ("priority", "= Lowest"),
# Assignee
"me": ("assignee", "= currentUser()"),
"mine": ("assignee", "= currentUser()"),
"unassigned": ("assignee", "is EMPTY"),
# Time
"overdue": ("duedate", "< now()"),
"today": ("duedate", "= now()"),
}
PROJECT_PATTERN = re.compile(r'\b([A-Z]{2,10})\b')
ASSIGNEE_PATTERN = re.compile(r'assigned\s+to\s+(\w+)', re.IGNORECASE)
LABEL_PATTERN = re.compile(r'label[s]?\s*[=:]\s*["\']?(\w+)["\']?', re.IGNORECASE)
COMPONENT_PATTERN = re.compile(r'component[s]?\s*[=:]\s*["\']?(\w+)["\']?', re.IGNORECASE)
DATE_RANGE_PATTERN = re.compile(r'last\s+(\d+)\s+(day|week|month)s?', re.IGNORECASE)
SPRINT_NAME_PATTERN = re.compile(r'sprint\s+["\']?(\w[\w\s]*\w)["\']?', re.IGNORECASE)
# Words to exclude from project matching
EXCLUDED_WORDS = {
"AND", "OR", "NOT", "IN", "IS", "TO", "BY", "ON", "DO", "BE",
"THE", "ALL", "MY", "NO", "OF", "AT", "AS", "IF", "IT",
"BUG", "BUGS", "TASK", "TASKS", "STORY", "EPIC", "DONE",
"HIGH", "LOW", "MEDIUM", "JQL",
}
# ---------------------------------------------------------------------------
# Query Builder
# ---------------------------------------------------------------------------
def find_matching_pattern(description: str) -> Optional[Dict[str, Any]]:
"""Check if description matches a known pattern exactly."""
desc_lower = description.lower().strip()
for pattern_name, pattern_data in PATTERN_LIBRARY.items():
for phrase in pattern_data["phrases"]:
if phrase in desc_lower or desc_lower in phrase:
return {
"pattern_name": pattern_name,
"jql": pattern_data["jql"],
"description": pattern_data["description"],
"match_type": "exact_pattern",
}
return None
def build_jql_from_description(description: str) -> Dict[str, Any]:
"""Build JQL query from natural language description."""
# First try exact pattern match
pattern_match = find_matching_pattern(description)
if pattern_match:
# Augment with project if mentioned
project = _extract_project(description)
if project:
pattern_match["jql"] = f'project = {project} AND {pattern_match["jql"]}'
return pattern_match
# Dynamic query building
clauses = []
used_fields = set()
desc_lower = description.lower()
# Extract project
project = _extract_project(description)
if project:
clauses.append(f"project = {project}")
used_fields.add("project")
# Extract keyword-based fragments
for keyword, (field, fragment) in KEYWORD_FRAGMENTS.items():
if keyword in desc_lower.split() and field not in used_fields:
clauses.append(f"{field} {fragment}")
used_fields.add(field)
# Extract explicit assignee
assignee_match = ASSIGNEE_PATTERN.search(description)
if assignee_match and "assignee" not in used_fields:
assignee = assignee_match.group(1)
if assignee.lower() in ("me", "myself"):
clauses.append("assignee = currentUser()")
else:
clauses.append(f'assignee = "{assignee}"')
used_fields.add("assignee")
# Extract labels
label_match = LABEL_PATTERN.search(description)
if label_match:
clauses.append(f'labels = "{label_match.group(1)}"')
# Extract component
component_match = COMPONENT_PATTERN.search(description)
if component_match:
clauses.append(f'component = "{component_match.group(1)}"')
# Extract date ranges
date_match = DATE_RANGE_PATTERN.search(description)
if date_match:
amount = date_match.group(1)
unit = date_match.group(2).lower()
unit_char = {"day": "d", "week": "w", "month": "m"}.get(unit, "d")
clauses.append(f"created >= -{amount}{unit_char}")
# Extract sprint reference
sprint_match = SPRINT_NAME_PATTERN.search(description)
if sprint_match:
sprint_name = sprint_match.group(1).strip()
if sprint_name.lower() in ("current", "active", "open"):
clauses.append("sprint in openSprints()")
else:
clauses.append(f'sprint = "{sprint_name}"')
# Default: if no status clause and not looking for done items
if "status" not in used_fields and "done" not in desc_lower and "closed" not in desc_lower:
clauses.append("status != Done")
if not clauses:
return {
"jql": "",
"description": "Could not build query from description",
"match_type": "no_match",
"error": "No recognizable patterns found in description",
}
jql = " AND ".join(clauses)
# Add ORDER BY for common scenarios
if "recent" in desc_lower or "latest" in desc_lower:
jql += " ORDER BY created DESC"
elif "priority" in desc_lower or "urgent" in desc_lower:
jql += " ORDER BY priority DESC"
return {
"jql": jql,
"description": f"Dynamic query from: {description}",
"match_type": "dynamic",
"clauses_used": len(clauses),
}
def _extract_project(description: str) -> Optional[str]:
"""Extract project key from description."""
# Look for IN/in PROJECT pattern
in_project = re.search(r'\bin\s+([A-Z]{2,10})\b', description)
if in_project and in_project.group(1) not in EXCLUDED_WORDS:
return in_project.group(1)
# Look for standalone project keys
for match in PROJECT_PATTERN.finditer(description):
word = match.group(1)
if word not in EXCLUDED_WORDS:
return word
return None
def validate_jql_syntax(jql: str) -> Dict[str, Any]:
"""Basic JQL syntax validation."""
issues = []
if not jql.strip():
return {"valid": False, "issues": ["Empty query"]}
# Check balanced quotes
single_quotes = jql.count("'")
double_quotes = jql.count('"')
if single_quotes % 2 != 0:
issues.append("Unbalanced single quotes")
if double_quotes % 2 != 0:
issues.append("Unbalanced double quotes")
# Check balanced parentheses
open_parens = jql.count("(")
close_parens = jql.count(")")
if open_parens != close_parens:
issues.append(f"Unbalanced parentheses: {open_parens} open, {close_parens} close")
# Check for known JQL operators
valid_operators = {"=", "!=", ">", "<", ">=", "<=", "~", "!~", "in", "not in", "is", "is not", "was", "was not", "changed"}
jql_upper = jql.upper()
# Check AND/OR placement
if jql_upper.strip().startswith("AND") or jql_upper.strip().startswith("OR"):
issues.append("Query cannot start with AND/OR")
if jql_upper.strip().endswith("AND") or jql_upper.strip().endswith("OR"):
issues.append("Query cannot end with AND/OR")
# Check ORDER BY syntax
order_match = re.search(r'ORDER\s+BY\s+(\w+)(?:\s+(ASC|DESC))?', jql, re.IGNORECASE)
if "ORDER" in jql_upper and not order_match:
issues.append("Invalid ORDER BY syntax")
return {
"valid": len(issues) == 0,
"issues": issues,
"query_length": len(jql),
}
# ---------------------------------------------------------------------------
# Output Formatting
# ---------------------------------------------------------------------------
def format_text_output(result: Dict[str, Any]) -> str:
"""Format results as readable text report."""
lines = []
lines.append("=" * 60)
lines.append("JQL QUERY BUILDER RESULTS")
lines.append("=" * 60)
lines.append("")
if "error" in result:
lines.append(f"ERROR: {result['error']}")
return "\n".join(lines)
lines.append(f"Match Type: {result.get('match_type', 'unknown')}")
lines.append(f"Description: {result.get('description', '')}")
lines.append("")
lines.append("GENERATED JQL")
lines.append("-" * 30)
lines.append(result.get("jql", ""))
lines.append("")
validation = result.get("validation", {})
if validation:
lines.append("VALIDATION")
lines.append("-" * 30)
lines.append(f"Valid: {'Yes' if validation.get('valid') else 'No'}")
if validation.get("issues"):
for issue in validation["issues"]:
lines.append(f" - {issue}")
if result.get("pattern_name"):
lines.append("")
lines.append(f"Matched Pattern: {result['pattern_name']}")
return "\n".join(lines)
def format_patterns_output(output_format: str) -> str:
"""Format available patterns list."""
if output_format == "json":
patterns = {}
for name, data in PATTERN_LIBRARY.items():
patterns[name] = {
"description": data["description"],
"phrases": data["phrases"],
"jql": data["jql"],
}
return json.dumps(patterns, indent=2)
lines = []
lines.append("=" * 60)
lines.append("AVAILABLE JQL PATTERNS")
lines.append("=" * 60)
lines.append("")
for name, data in PATTERN_LIBRARY.items():
lines.append(f" {name}")
lines.append(f" Description: {data['description']}")
lines.append(f" Phrases: {', '.join(data['phrases'])}")
lines.append(f" JQL: {data['jql']}")
lines.append("")
lines.append(f"Total patterns: {len(PATTERN_LIBRARY)}")
return "\n".join(lines)
def format_json_output(result: Dict[str, Any]) -> Dict[str, Any]:
"""Format results as JSON."""
return result
# ---------------------------------------------------------------------------
# CLI Interface
# ---------------------------------------------------------------------------
def main() -> int:
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="Build JQL queries from natural language descriptions"
)
parser.add_argument(
"description",
nargs="?",
help="Natural language description of the query",
)
parser.add_argument(
"--format",
choices=["text", "json"],
default="text",
help="Output format (default: text)",
)
parser.add_argument(
"--patterns",
action="store_true",
help="List all available query patterns",
)
args = parser.parse_args()
try:
if args.patterns:
print(format_patterns_output(args.format))
return 0
if not args.description:
parser.error("description is required unless --patterns is used")
# Build query
result = build_jql_from_description(args.description)
# Validate
if result.get("jql"):
result["validation"] = validate_jql_syntax(result["jql"])
# Output results
if args.format == "json":
output = format_json_output(result)
print(json.dumps(output, indent=2))
else:
output = format_text_output(result)
print(output)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
Workflow Validator
Validates Jira workflow definitions (JSON input) for anti-patterns and common
issues. Checks for dead-end states, orphan states, missing transitions, circular
paths, and produces a health score with severity-rated findings.
Usage:
python workflow_validator.py workflow.json
python workflow_validator.py workflow.json --format json
"""
import argparse
import json
import sys
from typing import Any, Dict, List, Optional, Set, Tuple
# ---------------------------------------------------------------------------
# Validation Configuration
# ---------------------------------------------------------------------------
MAX_RECOMMENDED_STATES = 10
REQUIRED_TERMINAL_STATES = {"done", "closed", "resolved", "completed"}
SEVERITY_WEIGHTS = {
"error": 20,
"warning": 10,
"info": 3,
}
# ---------------------------------------------------------------------------
# Validation Rules
# ---------------------------------------------------------------------------
def check_state_count(states: List[str]) -> List[Dict[str, str]]:
"""Check if the workflow has too many states."""
findings = []
count = len(states)
if count > MAX_RECOMMENDED_STATES:
findings.append({
"rule": "state_count",
"severity": "warning",
"message": f"Workflow has {count} states (recommended max: {MAX_RECOMMENDED_STATES}). "
f"Complex workflows slow teams down and increase error rates.",
})
elif count < 2:
findings.append({
"rule": "state_count",
"severity": "error",
"message": f"Workflow has only {count} state(s). A minimum of 2 states is required.",
})
if count > 15:
findings[-1]["severity"] = "error"
return findings
def check_dead_end_states(
states: List[str],
transitions: List[Dict[str, str]],
terminal_states: Set[str],
) -> List[Dict[str, str]]:
"""Find states with no outgoing transitions that are not terminal."""
findings = []
outgoing = set()
for t in transitions:
outgoing.add(t.get("from", "").lower())
for state in states:
state_lower = state.lower()
if state_lower not in outgoing and state_lower not in terminal_states:
findings.append({
"rule": "dead_end_state",
"severity": "error",
"message": f"State '{state}' has no outgoing transitions and is not a terminal state. "
f"Issues will get stuck here.",
})
return findings
def check_orphan_states(
states: List[str],
transitions: List[Dict[str, str]],
initial_state: Optional[str],
) -> List[Dict[str, str]]:
"""Find states with no incoming transitions (except the initial state)."""
findings = []
incoming = set()
for t in transitions:
incoming.add(t.get("to", "").lower())
initial_lower = (initial_state or "").lower()
for state in states:
state_lower = state.lower()
if state_lower not in incoming and state_lower != initial_lower:
findings.append({
"rule": "orphan_state",
"severity": "warning",
"message": f"State '{state}' has no incoming transitions and is not the initial state. "
f"This state may be unreachable.",
})
return findings
def check_missing_terminal_state(states: List[str]) -> List[Dict[str, str]]:
"""Check that at least one terminal/done state exists."""
findings = []
states_lower = {s.lower() for s in states}
has_terminal = bool(states_lower & REQUIRED_TERMINAL_STATES)
if not has_terminal:
findings.append({
"rule": "missing_terminal_state",
"severity": "error",
"message": f"No terminal state found. Expected one of: {', '.join(sorted(REQUIRED_TERMINAL_STATES))}. "
f"Issues cannot be marked as complete.",
})
return findings
def check_duplicate_transition_names(
transitions: List[Dict[str, str]],
) -> List[Dict[str, str]]:
"""Check for duplicate transition names from the same state."""
findings = []
seen = {}
for t in transitions:
name = t.get("name", "").lower()
from_state = t.get("from", "").lower()
key = (from_state, name)
if key in seen:
findings.append({
"rule": "duplicate_transition",
"severity": "warning",
"message": f"Duplicate transition name '{t.get('name', '')}' from state '{t.get('from', '')}'. "
f"This can confuse users selecting transitions.",
})
else:
seen[key] = True
return findings
def check_missing_transitions(
states: List[str],
transitions: List[Dict[str, str]],
) -> List[Dict[str, str]]:
"""Check for states referenced in transitions but not defined."""
findings = []
defined_states = {s.lower() for s in states}
for t in transitions:
from_state = t.get("from", "").lower()
to_state = t.get("to", "").lower()
if from_state and from_state not in defined_states:
findings.append({
"rule": "undefined_state_reference",
"severity": "error",
"message": f"Transition references undefined source state '{t.get('from', '')}'.",
})
if to_state and to_state not in defined_states:
findings.append({
"rule": "undefined_state_reference",
"severity": "error",
"message": f"Transition references undefined target state '{t.get('to', '')}'.",
})
return findings
def check_circular_paths(
states: List[str],
transitions: List[Dict[str, str]],
terminal_states: Set[str],
) -> List[Dict[str, str]]:
"""Detect circular paths that have no exit to a terminal state."""
findings = []
# Build adjacency list
adjacency = {}
for state in states:
adjacency[state.lower()] = set()
for t in transitions:
from_state = t.get("from", "").lower()
to_state = t.get("to", "").lower()
if from_state in adjacency:
adjacency[from_state].add(to_state)
# Find strongly connected components using iterative DFS
def can_reach_terminal(start: str) -> bool:
visited = set()
stack = [start]
while stack:
node = stack.pop()
if node in terminal_states:
return True
if node in visited:
continue
visited.add(node)
for neighbor in adjacency.get(node, set()):
stack.append(neighbor)
return False
# Check each non-terminal state
for state in states:
state_lower = state.lower()
if state_lower not in terminal_states:
if not can_reach_terminal(state_lower):
findings.append({
"rule": "circular_no_exit",
"severity": "error",
"message": f"State '{state}' cannot reach any terminal state. "
f"Issues entering this state will never be resolved.",
})
return findings
def check_self_transitions(transitions: List[Dict[str, str]]) -> List[Dict[str, str]]:
"""Check for transitions that go from a state to itself."""
findings = []
for t in transitions:
if t.get("from", "").lower() == t.get("to", "").lower():
findings.append({
"rule": "self_transition",
"severity": "info",
"message": f"State '{t.get('from', '')}' has a self-transition '{t.get('name', '')}'. "
f"Ensure this is intentional (e.g., for triggering automation).",
})
return findings
# ---------------------------------------------------------------------------
# Main Validation
# ---------------------------------------------------------------------------
def validate_workflow(data: Dict[str, Any]) -> Dict[str, Any]:
"""Run all validations on a workflow definition."""
states = data.get("states", [])
transitions = data.get("transitions", [])
initial_state = data.get("initial_state", states[0] if states else None)
if not states:
return {
"health_score": 0,
"grade": "invalid",
"findings": [{"rule": "no_states", "severity": "error", "message": "No states defined in workflow"}],
"summary": {"errors": 1, "warnings": 0, "info": 0},
}
# Determine terminal states
states_lower = {s.lower() for s in states}
terminal_states = states_lower & REQUIRED_TERMINAL_STATES
# Custom terminal states from input
custom_terminals = data.get("terminal_states", [])
for ct in custom_terminals:
terminal_states.add(ct.lower())
# Run all checks
all_findings = []
all_findings.extend(check_state_count(states))
all_findings.extend(check_dead_end_states(states, transitions, terminal_states))
all_findings.extend(check_orphan_states(states, transitions, initial_state))
all_findings.extend(check_missing_terminal_state(states))
all_findings.extend(check_duplicate_transition_names(transitions))
all_findings.extend(check_missing_transitions(states, transitions))
all_findings.extend(check_circular_paths(states, transitions, terminal_states))
all_findings.extend(check_self_transitions(transitions))
# Calculate health score
summary = {"errors": 0, "warnings": 0, "info": 0}
penalty = 0
for finding in all_findings:
severity = finding["severity"]
summary[severity] = summary.get(severity, 0) + 1
penalty += SEVERITY_WEIGHTS.get(severity, 0)
health_score = max(0, 100 - penalty)
if health_score >= 90:
grade = "excellent"
elif health_score >= 75:
grade = "good"
elif health_score >= 55:
grade = "fair"
else:
grade = "poor"
return {
"health_score": health_score,
"grade": grade,
"findings": all_findings,
"summary": summary,
"workflow_info": {
"state_count": len(states),
"transition_count": len(transitions),
"initial_state": initial_state,
"terminal_states": sorted(terminal_states),
},
}
# ---------------------------------------------------------------------------
# Output Formatting
# ---------------------------------------------------------------------------
def format_text_output(result: Dict[str, Any]) -> str:
"""Format results as readable text report."""
lines = []
lines.append("=" * 60)
lines.append("WORKFLOW VALIDATION REPORT")
lines.append("=" * 60)
lines.append("")
# Health summary
lines.append("HEALTH SUMMARY")
lines.append("-" * 30)
lines.append(f"Health Score: {result['health_score']}/100")
lines.append(f"Grade: {result['grade'].title()}")
lines.append("")
# Workflow info
info = result.get("workflow_info", {})
if info:
lines.append("WORKFLOW INFO")
lines.append("-" * 30)
lines.append(f"States: {info.get('state_count', 0)}")
lines.append(f"Transitions: {info.get('transition_count', 0)}")
lines.append(f"Initial State: {info.get('initial_state', 'N/A')}")
lines.append(f"Terminal States: {', '.join(info.get('terminal_states', []))}")
lines.append("")
# Summary
summary = result.get("summary", {})
lines.append("FINDINGS SUMMARY")
lines.append("-" * 30)
lines.append(f"Errors: {summary.get('errors', 0)}")
lines.append(f"Warnings: {summary.get('warnings', 0)}")
lines.append(f"Info: {summary.get('info', 0)}")
lines.append("")
# Detailed findings
findings = result.get("findings", [])
if findings:
lines.append("DETAILED FINDINGS")
lines.append("-" * 30)
for i, finding in enumerate(findings, 1):
severity = finding["severity"].upper()
lines.append(f"{i}. [{severity}] {finding['message']}")
lines.append(f" Rule: {finding['rule']}")
lines.append("")
else:
lines.append("No issues found. Workflow looks healthy!")
return "\n".join(lines)
def format_json_output(result: Dict[str, Any]) -> Dict[str, Any]:
"""Format results as JSON."""
return result
# ---------------------------------------------------------------------------
# CLI Interface
# ---------------------------------------------------------------------------
def main() -> int:
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="Validate Jira workflow definitions for anti-patterns"
)
parser.add_argument(
"workflow_file",
help="JSON file containing workflow definition (states, transitions)",
)
parser.add_argument(
"--format",
choices=["text", "json"],
default="text",
help="Output format (default: text)",
)
args = parser.parse_args()
try:
with open(args.workflow_file, "r") as f:
data = json.load(f)
result = validate_workflow(data)
if args.format == "json":
print(json.dumps(format_json_output(result), indent=2))
else:
print(format_text_output(result))
return 0
except FileNotFoundError:
print(f"Error: File '{args.workflow_file}' not found", file=sys.stderr)
return 1
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in '{args.workflow_file}': {e}", file=sys.stderr)
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
Install this Skill
Skills give your AI agent a consistent, structured approach to this task — better output than a one-off prompt.
npx skills add alirezarezvani/claude-skills --skill project-management/jira-expert Community skill by @alirezarezvani. Need a walkthrough? See the install guide →
Works with
Prefer no terminal? Download the ZIP and place it manually.
Details
- Category
- Productivity
- License
- MIT
- Author
- @alirezarezvani
- Source
- GitHub →
- Source file
-
show path
project-management/jira-expert/SKILL.md
People who install this also use
Scrum Master Expert
Sprint planning, agile ceremonies facilitation, backlog management, and velocity tracking — an expert Scrum Master in your workflow.
@alirezarezvani
Product Manager Toolkit
RICE prioritization, customer interview frameworks, PRD writing, and product discovery workflows — a complete PM toolkit in one skill.
@alirezarezvani