How to Integrate AI Agents with Jira

Complete guide to connecting AI agents with Jira Cloud. Learn to create, update, and query issues using LangChain and CrewAI, with real use cases for sprint planning, status reporting, and code review automation.

Before You Start

Prerequisites: Jira Cloud account, Atlassian API token, Basic understanding of AI agent concepts

Works with: LangChain, CrewAI

Engineering teams waste significant time on project management overhead: manually creating tickets from discussion notes, updating statuses, writing sprint reports, and transitioning issues as code moves through review. An AI agent integrated with Jira can handle the entire administrative layer of project management — letting engineers focus on engineering.

This guide covers the full integration from API setup to working Python code with real sprint planning and status reporting use cases.

What AI Agents Can Do With Jira Access#

The Jira REST API exposes almost everything in the Jira data model to external agents:

Issue operations

  • Create new issues with properly structured fields: summary, description, type, priority, labels, story points
  • Read issues with full detail including comments, attachments, and change history
  • Update any field on an existing issue (status, assignee, priority, custom fields)
  • Add comments — public or internal — to existing issues
  • Delete or archive issues (use with caution)

Query and reporting

  • Execute JQL queries to retrieve precisely scoped sets of issues
  • Build sprint reports from current sprint data
  • Identify bottlenecks: issues in progress longer than a threshold, unassigned issues, blocked tickets
  • Generate velocity reports and burndown data

Workflow management

  • Transition issue status through the configured workflow (e.g., To Do → In Progress → In Review → Done)
  • Create and manage sprints programmatically
  • Link issues (blocks, is blocked by, relates to, duplicates)
  • Set epic links and parent issues for hierarchy

Integration hooks

  • Process incoming webhooks from Jira when issues change
  • Fire external actions when issue status transitions occur

For a broader view of how agents integrate across engineering toolchains, see AI agent workflow examples.


Authentication Setup: Atlassian API Token#

Jira Cloud uses Atlassian API tokens for programmatic access. These are personal tokens tied to a specific user account.

Step 1: Generate an API Token#

  1. Log in to id.atlassian.com/manage-profile/security/api-tokens
  2. Click Create API token
  3. Label it descriptively (e.g., "Sprint Planning Agent")
  4. Copy the token immediately — it will not be shown again

Step 2: Find Your Jira Cloud Base URL#

Your base URL is the domain where your Jira instance lives, in the format:

https://{your-subdomain}.atlassian.net

Step 3: Store Credentials#

Create a .env file:

JIRA_BASE_URL=https://yourcompany.atlassian.net
JIRA_EMAIL=agent@yourcompany.com
JIRA_API_TOKEN=your_api_token_here
OPENAI_API_KEY=sk-...

Step 4: Verify the Connection#

curl -u "your@email.com:your_api_token" \
  -H "Content-Type: application/json" \
  "https://yourcompany.atlassian.net/rest/api/3/myself"

A successful response returns your user profile, confirming the credentials work.

Dedicated Service Account#

For production agents, create a dedicated Atlassian account (e.g., jira-agent@yourcompany.com) and use its API token rather than a real person's credentials. Add this account to the relevant Jira projects with the minimum required role.


Building Jira Tools for LangChain#

Installation#

pip install langchain langchain-openai jira python-dotenv

Jira Tool Implementations#

import os
from jira import JIRA, JIRAError
from langchain.tools import tool
from dotenv import load_dotenv
import json

load_dotenv()

jira_client = JIRA(
    server=os.getenv("JIRA_BASE_URL"),
    basic_auth=(os.getenv("JIRA_EMAIL"), os.getenv("JIRA_API_TOKEN"))
)


@tool
def jql_search(jql_query: str, max_results: int = 20) -> str:
    """
    Search Jira issues using JQL (Jira Query Language).
    Examples:
      - 'project = PROJ AND status = "In Progress"'
      - 'project = PROJ AND sprint in openSprints() AND assignee is EMPTY'
      - 'project = PROJ AND created >= -7d AND status = "To Do"'
    Returns issue keys, summaries, statuses, and assignees.
    """
    try:
        issues = jira_client.search_issues(jql_query, maxResults=max_results,
                                           fields="summary,status,assignee,priority,story_points,labels")
        if not issues:
            return f"No issues found for JQL: {jql_query}"

        results = []
        for issue in issues:
            f = issue.fields
            assignee = f.assignee.displayName if f.assignee else "Unassigned"
            results.append(
                f"{issue.key}: {f.summary} | Status: {f.status.name} | "
                f"Assignee: {assignee} | Priority: {f.priority.name if f.priority else 'None'}"
            )
        return f"Found {len(issues)} issue(s):\n" + "\n".join(results)
    except JIRAError as e:
        return f"JQL search failed: {e.text}"


@tool
def create_issue(project_key: str, summary: str, description: str,
                  issue_type: str = "Story", priority: str = "Medium",
                  assignee_email: str = None, labels: str = None,
                  story_points: int = None) -> str:
    """
    Create a new Jira issue.
    project_key: The Jira project key (e.g., 'PROJ', 'BACKEND')
    issue_type: 'Story', 'Task', 'Bug', 'Epic', 'Sub-task'
    labels: Comma-separated list of labels to apply
    story_points: Estimated story points (integer)
    """
    try:
        issue_dict = {
            "project": {"key": project_key},
            "summary": summary,
            "description": {
                "type": "doc",
                "version": 1,
                "content": [{"type": "paragraph", "content": [{"type": "text", "text": description}]}]
            },
            "issuetype": {"name": issue_type},
            "priority": {"name": priority}
        }

        if assignee_email:
            users = jira_client.search_users(query=assignee_email)
            if users:
                issue_dict["assignee"] = {"accountId": users[0].accountId}

        if labels:
            issue_dict["labels"] = [l.strip() for l in labels.split(",")]

        if story_points is not None:
            issue_dict["story_points"] = story_points

        new_issue = jira_client.create_issue(fields=issue_dict)
        return f"Issue created: {new_issue.key} — '{summary}' in project {project_key}"
    except JIRAError as e:
        return f"Failed to create issue: {e.text}"


@tool
def update_issue_status(issue_key: str, target_status: str) -> str:
    """
    Transition a Jira issue to a new status.
    Common statuses: 'To Do', 'In Progress', 'In Review', 'Done', 'Blocked'
    The exact status names depend on the project's workflow configuration.
    """
    try:
        issue = jira_client.issue(issue_key)
        transitions = jira_client.transitions(issue)

        # Find the transition that leads to the target status
        target_transition = None
        for t in transitions:
            if t["to"]["name"].lower() == target_status.lower():
                target_transition = t["id"]
                break

        if not target_transition:
            available = [t["to"]["name"] for t in transitions]
            return f"Status '{target_status}' not found. Available transitions: {available}"

        jira_client.transition_issue(issue_key, target_transition)
        return f"Issue {issue_key} transitioned to '{target_status}'"
    except JIRAError as e:
        return f"Failed to transition {issue_key}: {e.text}"


@tool
def add_comment(issue_key: str, comment_text: str) -> str:
    """Add a comment to a Jira issue."""
    try:
        jira_client.add_comment(issue_key, comment_text)
        return f"Comment added to {issue_key}"
    except JIRAError as e:
        return f"Failed to add comment: {e.text}"


@tool
def get_issue_detail(issue_key: str) -> str:
    """
    Fetch full detail for a single Jira issue including description, comments, and history.
    """
    try:
        issue = jira_client.issue(issue_key, expand="renderedFields,changelog")
        f = issue.fields

        comments = []
        for comment in f.comment.comments[-3:]:  # Last 3 comments
            comments.append(f"  [{comment.author.displayName}]: {comment.body[:200]}")

        return (
            f"Issue: {issue_key}\n"
            f"Summary: {f.summary}\n"
            f"Status: {f.status.name}\n"
            f"Assignee: {f.assignee.displayName if f.assignee else 'Unassigned'}\n"
            f"Priority: {f.priority.name if f.priority else 'None'}\n"
            f"Labels: {', '.join(f.labels) if f.labels else 'none'}\n"
            f"Description: {str(f.description)[:500] if f.description else 'None'}\n"
            f"Recent comments:\n" + "\n".join(comments)
        )
    except JIRAError as e:
        return f"Failed to fetch issue {issue_key}: {e.text}"

Use Case 1: Sprint Planning Agent#

This agent takes meeting notes and creates structured Jira tickets from them:

from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [jql_search, create_issue, add_comment]

sprint_planning_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a sprint planning agent that converts meeting notes into Jira issues.

When given meeting notes:
1. Extract each distinct action item or feature discussed
2. For each item, determine:
   - Issue type: Story (user-facing feature), Task (internal work), Bug (defect)
   - Priority: Critical/High/Medium/Low based on business impact discussed
   - Story points: Estimate 1, 2, 3, 5, 8, or 13 based on complexity signals in the notes
   - Assignee: Map any mentioned person names to the project
   - Labels: Relevant labels from the discussion (frontend, backend, api, database, etc.)
3. Create each issue in the specified project
4. After creating all issues, run a JQL search to confirm they appear in the project
5. Provide a summary of what was created

Be precise with issue titles — use the format '[Action] [Subject]', e.g. 'Add OAuth2 login flow' or 'Fix checkout page 500 error'"""),
    ("human", "Create Jira issues in project {project_key} from these meeting notes:\n\n{meeting_notes}"),
    ("placeholder", "{agent_scratchpad}"),
])

sprint_agent = create_tool_calling_agent(llm, tools, sprint_planning_prompt)
sprint_executor = AgentExecutor(agent=sprint_agent, tools=tools, verbose=True, max_iterations=15)

meeting_notes = """
Sprint Planning Notes — Feb 25, 2026

Sarah mentioned the checkout page is throwing a 500 error for about 5% of users when
they have items from multiple sellers. High priority — losing revenue.

Tom needs to build out the new reporting dashboard for enterprise customers. This
was promised to customers for Q1. Probably 2-3 weeks of work. Medium priority.

We decided to add Google SSO to the login page. Engineering estimated 3-5 days.
Assign to the frontend team.

There's a performance issue with the search API — it takes 4+ seconds for
complex queries. Needs investigation before we can estimate properly.
"""

result = sprint_executor.invoke({
    "project_key": "PROJ",
    "meeting_notes": meeting_notes
})
print(result["output"])

This agent applies tool calling in AI agents to chain multiple Jira API operations in sequence — creating issues, checking results, and reporting back.


Use Case 2: Automated Status Update Agent#

An agent that generates daily project status updates by querying Jira and formatting a report:

status_report_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a project status reporting agent for engineering teams.

When asked for a status report for a project:
1. Query the current sprint for: completed issues, in-progress issues, blocked issues
2. Identify any issues that have been 'In Progress' for more than 5 days (potential blockers)
3. Count total story points completed vs remaining in the sprint
4. Format a concise status report suitable for a Slack post or email to stakeholders

Format:
## Sprint Status: [Project] — [Date]
**Completed this sprint**: [count] issues ([X] points)
**In Progress**: [list key issues]
**Blocked**: [list blocked issues with reason if available]
**At Risk**: [issues in progress for 5+ days]
**Sprint Progress**: [completed points] / [total points] ([%])
"""),
    ("human", "Generate a status report for project {project_key}"),
    ("placeholder", "{agent_scratchpad}"),
])

status_agent = create_tool_calling_agent(llm, [jql_search, get_issue_detail], status_report_prompt)
status_executor = AgentExecutor(agent=status_agent, tools=[jql_search, get_issue_detail], verbose=False)

report = status_executor.invoke({"project_key": "BACKEND"})
print(report["output"])

Use Case 3: CrewAI for Code Review to Jira Automation#

This CrewAI crew processes a GitHub PR webhook and links it back to Jira:

from crewai import Agent, Task, Crew

pr_analyst = Agent(
    role="Pull Request Analyst",
    goal="Analyze PR content and identify the associated Jira issue",
    backstory="Expert at reading PR descriptions and branch names to extract Jira issue context",
    tools=[get_issue_detail],
    llm="gpt-4o"
)

jira_updater = Agent(
    role="Jira Project Manager",
    goal="Keep Jira issues synchronized with development progress",
    backstory="Specialist in maintaining accurate project tracking as code moves through review",
    tools=[update_issue_status, add_comment],
    llm="gpt-4o"
)

analyze_pr_task = Task(
    description="""Analyze this pull request:
    Branch: {branch_name}
    PR Title: {pr_title}
    PR Description: {pr_description}

    Extract the Jira issue key from the branch name or PR title (format: PROJ-123).
    Fetch the issue details from Jira to understand the original requirement.
    Report the issue key, current status, and whether the PR appears to address the requirement.""",
    agent=pr_analyst,
    expected_output="Jira issue key, current status, and alignment assessment between PR and issue"
)

update_jira_task = Task(
    description="""Based on the PR analysis, update the Jira issue:
    - If PR is opened: transition status to 'In Review', add comment noting the PR
    - If PR is merged: transition status to 'Done', add comment with merge confirmation

    Always add a comment documenting what the agent did and why.""",
    agent=jira_updater,
    expected_output="Confirmation of Jira updates made"
)

pr_crew = Crew(
    agents=[pr_analyst, jira_updater],
    tasks=[analyze_pr_task, update_jira_task],
    verbose=True
)

# Triggered by GitHub webhook payload
result = pr_crew.kickoff(inputs={
    "branch_name": "feature/PROJ-456-add-oauth-login",
    "pr_title": "[PROJ-456] Add Google OAuth2 login flow",
    "pr_description": "Implements the OAuth2 login flow requested in PROJ-456. Adds Google SSO button to login page.",
    "pr_event": "merged"
})

For more multi-agent coordination patterns, see the multi-agent systems tutorial.


JQL Query Reference for Common Agent Tasks#

JQL is the query language your agents will use most. Here are practical queries for common agent scenarios:

-- All unassigned open issues in a project
project = PROJ AND status != Done AND assignee is EMPTY

-- Current sprint issues that are blocked
project = PROJ AND sprint in openSprints() AND status = Blocked

-- Issues in progress for more than 5 days
project = PROJ AND status = "In Progress" AND updated <= -5d

-- All bugs created in the last 7 days
project = PROJ AND issuetype = Bug AND created >= -7d ORDER BY priority DESC

-- Issues with no story points assigned
project = PROJ AND sprint in openSprints() AND "Story Points" is EMPTY

-- High priority issues not yet started
project = PROJ AND priority in (Critical, High) AND status = "To Do"

-- Issues completed in the last sprint
project = PROJ AND sprint in lastSprint() AND status = Done

These queries can be dynamically constructed by your agent based on the user's request, enabling natural language queries like "show me all blocked tickets in the current sprint" to translate directly into JQL.


Rate Limits and Batching#

Jira Cloud's rate limiting is managed through Atlassian's throttling system. Key practices:

Use batch fetching: When your agent needs to process many issues, use maxResults=100 in JQL queries rather than fetching individual issues one by one.

# Efficient: one API call for 100 issues
issues = jira_client.search_issues(jql_query, maxResults=100)

# Inefficient: 100 separate API calls
for key in issue_keys:
    issue = jira_client.issue(key)  # Avoid this pattern for bulk operations

Implement backoff: The jira Python library raises JIRAError with status 429 on rate limit hits. Wrap calls with retry logic:

import time
from jira import JIRAError

def jira_with_retry(fn, *args, max_retries=5, **kwargs):
    for attempt in range(max_retries):
        try:
            return fn(*args, **kwargs)
        except JIRAError as e:
            if e.status_code == 429:
                wait = 2 ** attempt
                print(f"Rate limited. Waiting {wait}s...")
                time.sleep(wait)
            else:
                raise
    raise Exception("Max retries exceeded")

Next Steps#

With your Jira agent operational, expand its capabilities: