AI Jira Linear Integration: Bidirectional Ticket Management for AI Agents
TL;DR — AI Jira and Linear integration enables bidirectional ticket management for AI agents via MCP servers. Autonomi: "Linear redesigned its API specifically for agentic workflows in 2025. Jira MCP servers enable GitHub Copilot and Claude to read and update tickets. The bidirectional pattern matters because unidirectional sync creates stale project boards." CodeWithSeb: "Say implement fix for ENG-4521 and Claude reads the Jira ticket, finds the bug, writes the fix, runs tests, and commits. Under ninety seconds." PADISO: "Issue tracking systems have traditionally been human-centric tools. But when you introduce AI agents, the entire dynamic changes." Linear: AgentSession with 6 states (pending, active, error, awaitingInput, complete, stale), Agent Activities for progress, Agent Plans for checklists. Key patterns: MCP servers (jira_get_issue, linear_create_issue), issue-driven development (ticket → code → PR → status), bidirectional sync (Jira ↔ Linear), agent sessions (visible lifecycle), status mapping. Integrate with GitHub, Nango, webhooks, and Slack.
PADISO describes the paradigm shift: "Issue tracking systems like Linear and Jira have traditionally been human-centric tools. Product managers create tickets, engineers update status, QA comments on reproduction steps. But when you introduce AI agents into this workflow, the entire dynamic changes."
Autonomi identifies the key architectural decision: "The bidirectional pattern matters because unidirectional sync (reading tickets but not updating them) creates stale project boards. When an agent completes a task and the ticket stays 'In Progress,' the project manager has no visibility. Bidirectional sync means the project board reflects reality in real time, whether updates come from humans or agents."
AI Jira/Linear Integration Architecture
(REST API + MCP)"] Linear["Linear
(GraphQL API + MCP)"] end subgraph MCP["MCP Servers"] JiraMCP["Jira MCP
jira_get_issue
jira_transition_issue
jira_create_issue
jira_add_comment
jira_search"] LinearMCP["Linear MCP
linear_search_issues
linear_create_issue
linear_update_issue
linear_create_comment"] end subgraph Agent["AI Agent Workflow"] Read["Read Ticket
(description, criteria, comments)"] Analyze["Analyze Codebase
(find relevant files)"] Implement["Implement Fix
(follow acceptance criteria)"] Test["Run Tests
(verify reproduction steps)"] Commit["Commit + Push
(reference ticket)"] PR["Open PR
(GitHub MCP)"] Update["Update Ticket Status
(transition to In Review)"] Comment["Add Comment
(link PR + commit SHA)"] end subgraph LinearAgent["Linear Agent System"] Session["AgentSession
(pending to active to complete)"] Activity["Agent Activities
(thoughts, tool calls, responses)"] Plan["Agent Plan
(evolving checklist)"] Webhook["AgentSession Webhook
(mentioned, delegated, prompted)"] end subgraph Sync["Bidirectional Sync"] JiraSync["Jira to Linear
(epics to projects, issues to issues)"] LinearSync["Linear to Jira
(status mapping, comment sync)"] StatusMap["Status Mapping
(To Do to Backlog, Done to Done)"] end Jira --> JiraMCP Linear --> LinearMCP JiraMCP --> Read LinearMCP --> Read Read --> Analyze Analyze --> Implement Implement --> Test Test --> Commit Commit --> PR PR --> Update Update --> Comment Linear --> Session Session --> Activity Session --> Plan Session --> Webhook Webhook --> Read Jira --> JiraSync JiraSync --> Linear Linear --> LinearSync LinearSync --> Jira JiraSync --> StatusMap LinearSync --> StatusMap
MCP Tool Comparison
| Tool | Jira MCP | Linear MCP | Use Case |
|---|---|---|---|
| Get issue | jira_get_issue |
linear_get_issue |
Read ticket details, criteria, comments |
| Search issues | jira_search (JQL) |
linear_search_issues (GraphQL) |
Find duplicates, query backlog |
| Create issue | jira_create_issue |
linear_create_issue |
AI agent creates tickets from requests |
| Update status | jira_transition_issue |
linear_update_issue |
Move ticket through workflow |
| Add comment | jira_add_comment |
linear_create_comment |
Link PRs, commit SHAs, test results |
| Manage sub-tasks | jira_create_subtask |
linear_create_sub_issue |
Break down epics |
| Get team info | jira_get_project |
linear_get_teams |
Understand team structure |
| Assign | jira_assign_issue |
linear_update_issue (assignee) |
Route to right person |
Jira vs Linear for AI Agents
| Aspect | Jira | Linear |
|---|---|---|
| API type | REST API | GraphQL API |
| MCP server | Fragmented ecosystem | Production-grade (Zalab Inc) |
| Agent API | Via MCP servers | Native agent interaction system |
| Agent sessions | Not native | AgentSession with 6 states |
| Agent activities | Not native | Thoughts, tool calls, responses |
| Agent plans | Not native | Evolving checklist |
| Response speed | Slower (REST, larger payloads) | Faster (GraphQL, structured) |
| Enterprise features | Custom fields, complex workflows | Simpler, opinionated |
| Rate limits | 2,000 requests/hour | Higher (GraphQL efficient) |
| Best for | Enterprise with complex workflows | Modern, fast-moving teams |
Implementation
import httpx
import json
import re
class JiraLinearIntegration:
"""AI agent integration for Jira and Linear project management."""
def __init__(self, jira_config: dict, linear_config: dict, audit):
self.jira = JiraClient(jira_config)
self.linear = LinearClient(linear_config)
self.audit = audit
async def issue_driven_development(self, tenant_id: str, user_id: str,
ticket_key: str, platform: str = "jira"):
"""Execute issue-driven development: ticket to code to PR to status."""
# 1. Read ticket
if platform == "jira":
ticket = await self.jira.get_issue(ticket_key)
else:
ticket = await self.linear.get_issue(ticket_key)
# 2. Extract context
context = {
"key": ticket["key"],
"title": ticket["title"],
"description": ticket["description"],
"acceptance_criteria": ticket.get("acceptance_criteria", []),
"comments": ticket.get("comments", []),
"linked_issues": ticket.get("linked_issues", []),
}
# 3. Create branch
branch_name = f"fix/{ticket_key.lower()}-{self._slugify(ticket['title'])}"
# 4. Implementation done by AI agent
# 5. After PR is created, update ticket
pr_url = "https://github.com/company/repo/pull/123"
commit_sha = "abc123def456"
if platform == "jira":
await self.jira.transition_issue(ticket_key, "In Review")
await self.jira.add_comment(ticket_key,
f"PR created: {pr_url}\nCommit: {commit_sha}")
else:
await self.linear.update_issue(ticket_key, status="In Review")
await self.linear.create_comment(ticket_key,
f"PR created: {pr_url}\nCommit: {commit_sha}")
# 6. Audit log
await self.audit.log(
tenant_id=tenant_id,
user_id=user_id,
action="issue_driven_dev_complete",
entity_type="ticket",
entity_id=ticket_key,
after={"platform": platform, "branch": branch_name,
"pr_url": pr_url, "status": "In Review"},
)
return {"ticket": ticket_key, "branch": branch_name,
"pr_url": pr_url, "status": "In Review"}
async def create_ticket_from_request(self, tenant_id: str, user_id: str,
title: str, description: str,
platform: str = "jira",
project_key: str = None,
team_id: str = None) -> dict:
"""AI agent creates a ticket from a user request."""
# Check for duplicates first
if platform == "jira":
existing = await self.jira.search(
f'project = {project_key} AND summary ~ "{title}"'
)
else:
existing = await self.linear.search_issues(
query=title, team_id=team_id
)
if existing:
return {"status": "duplicate_found",
"existing_ticket": existing[0]["key"]}
# Create new ticket
if platform == "jira":
ticket = await self.jira.create_issue(
project_key=project_key, summary=title,
description=description, issue_type="Task",
)
else:
ticket = await self.linear.create_issue(
team_id=team_id, title=title, description=description,
)
await self.audit.log(
tenant_id=tenant_id, user_id=user_id,
action="ticket_created_by_agent",
entity_type="ticket", entity_id=ticket["key"],
after={"title": title, "platform": platform},
)
return {"status": "created", "ticket": ticket["key"]}
async def bidirectional_sync(self, tenant_id: str):
"""Sync tickets between Jira and Linear bidirectionally."""
# 1. Get recent Jira changes
jira_changes = await self.jira.get_recent_changes()
for change in jira_changes:
linear_status = self._map_status_jira_to_linear(change["status"])
linear_issue = await self.linear.find_by_external_id(change["key"])
if linear_issue:
await self.linear.update_issue(
linear_issue["id"], status=linear_status)
# 2. Get recent Linear changes
linear_changes = await self.linear.get_recent_changes()
for change in linear_changes:
jira_status = self._map_status_linear_to_jira(change["status"])
jira_issue = await self.jira.find_by_external_id(change["id"])
if jira_issue:
await self.jira.transition_issue(
jira_issue["key"], jira_status)
await self.audit.log(
tenant_id=tenant_id, action="bidirectional_sync",
entity_type="sync",
after={"jira_changes": len(jira_changes),
"linear_changes": len(linear_changes)},
)
def _map_status_jira_to_linear(self, jira_status: str) -> str:
STATUS_MAP = {
"To Do": "Backlog", "In Progress": "Started",
"In Review": "In Review", "Done": "Done", "Blocked": "Triage",
}
return STATUS_MAP.get(jira_status, "Triage")
def _map_status_linear_to_jira(self, linear_status: str) -> str:
STATUS_MAP = {
"Backlog": "To Do", "Triage": "To Do",
"Started": "In Progress", "In Review": "In Review",
"Done": "Done", "Canceled": "Done",
}
return STATUS_MAP.get(linear_status, "To Do")
def _slugify(self, text: str) -> str:
return re.sub(r'[^a-z0-9]+', '-', text.lower()).strip('-')[:40]
class JiraClient:
"""Jira Cloud REST API client."""
def __init__(self, config: dict):
self.client = httpx.AsyncClient(
base_url=f"https://{config['base_url']}",
auth=(config["email"], config["api_token"]),
headers={"Accept": "application/json"},
)
async def get_issue(self, key: str) -> dict:
response = await self.client.get(f"/rest/api/3/issue/{key}")
data = response.json()
return {
"key": data["key"],
"title": data["fields"]["summary"],
"description": data["fields"].get("description", ""),
"status": data["fields"]["status"]["name"],
"comments": [c["body"] for c in data["fields"]
.get("comment", {}).get("comments", [])],
"linked_issues": [l["outwardIssue"]["key"] for l
in data["fields"].get("issuelinks", [])],
}
async def transition_issue(self, key: str, status: str):
transitions = await self.client.get(
f"/rest/api/3/issue/{key}/transitions")
transition_id = None
for t in transitions.json()["transitions"]:
if t["name"] == status:
transition_id = t["id"]
break
await self.client.post(
f"/rest/api/3/issue/{key}/transitions",
json={"transition": {"id": transition_id}})
async def create_issue(self, project_key: str, summary: str,
description: str, issue_type: str) -> dict:
response = await self.client.post("/rest/api/3/issue", json={
"fields": {
"project": {"key": project_key},
"summary": summary,
"description": description,
"issuetype": {"name": issue_type},
},
})
return response.json()
async def add_comment(self, key: str, comment: str):
await self.client.post(
f"/rest/api/3/issue/{key}/comment",
json={"body": comment})
async def search(self, jql: str) -> list:
response = await self.client.post(
"/rest/api/3/search", json={"jql": jql})
return response.json().get("issues", [])
class LinearClient:
"""Linear GraphQL API client."""
def __init__(self, config: dict):
self.client = httpx.AsyncClient(
base_url="https://api.linear.app/graphql",
headers={"Authorization": config["api_key"]},
)
async def get_issue(self, key: str) -> dict:
query = """
query($id: String!) {
issue(id: $id) {
id title description state { name }
comments { body }
}
}
"""
response = await self.client.post("", json={
"query": query, "variables": {"id": key}})
data = response.json()["data"]["issue"]
return {
"key": data["id"],
"title": data["title"],
"description": data.get("description", ""),
"status": data["state"]["name"],
"comments": [c["body"] for c in data.get("comments", [])],
}
async def create_issue(self, team_id: str, title: str,
description: str) -> dict:
mutation = """
mutation($input: IssueCreateInput!) {
issueCreate(input: $input) { issue { id } }
}
"""
response = await self.client.post("", json={
"query": mutation,
"variables": {"input": {
"teamId": team_id, "title": title,
"description": description}},
})
return response.json()["data"]["issueCreate"]["issue"]
async def update_issue(self, issue_id: str, **kwargs):
mutation = """
mutation($id: String!, $input: IssueUpdateInput!) {
issueUpdate(id: $id, input: $input) { success }
}
"""
await self.client.post("", json={
"query": mutation,
"variables": {"id": issue_id, "input": kwargs},
})
async def create_comment(self, issue_id: str, body: str):
mutation = """
mutation($input: CommentCreateInput!) {
commentCreate(input: $input) { success }
}
"""
await self.client.post("", json={
"query": mutation,
"variables": {"input": {"issueId": issue_id, "body": body}},
})
async def search_issues(self, query: str, team_id: str = None) -> list:
q = """
query($filter: IssueFilter) {
issues(filter: $filter) { nodes { id title } }
}
"""
issue_filter = {"title": {"contains": query}}
if team_id:
issue_filter["team"] = {"id": {"eq": team_id}}
response = await self.client.post("", json={
"query": q, "variables": {"filter": issue_filter},
})
return response.json()["data"]["issues"]["nodes"]
AI Jira/Linear Integration Checklist
- [ ] Set up Jira Cloud API access (email + API token)
- [ ] Set up Linear API access (personal API key)
- [ ] Install Jira MCP server for AI agent integration
- [ ] Install Linear MCP server (Zalab Inc production-grade)
- [ ] Configure MCP server with correct API credentials
- [ ] Expose MCP tools: get_issue, create_issue, transition_issue, add_comment, search
- [ ] Implement issue-driven development workflow (ticket to code to PR to status)
- [ ] Read full ticket context: description, acceptance criteria, comments, linked issues
- [ ] Use acceptance criteria as implementation checklist
- [ ] Create branch with ticket reference (fix/ENG-4521-description)
- [ ] Reference ticket in commit messages
- [ ] Open PR with description referencing ticket
- [ ] Transition ticket to In Review after PR creation
- [ ] Add comment with PR URL and commit SHA
- [ ] Implement duplicate detection before creating new tickets
- [ ] Search existing tickets before creating new ones
- [ ] Implement bidirectional sync between Jira and Linear
- [ ] Map statuses: To Do to Backlog, In Progress to Started, Done to Done
- [ ] Handle Jira epics to Linear projects sync
- [ ] Handle Jira sub-tasks to Linear sub-issues sync
- [ ] Prevent JQL injection with strict epic key validation
- [ ] Handle Jira rate limits (2,000 requests/hour) with backoff
- [ ] Handle Linear rate limits with backoff
- [ ] Set up webhooks for ticket status changes in both systems
- [ ] Use Linear AgentSession for agent lifecycle tracking
- [ ] Emit Agent Activities (thoughts, tool calls, responses) for visibility
- [ ] Use Agent Plans for evolving checklists during execution
- [ ] Handle AgentSession webhooks (mentioned, delegated, prompted)
- [ ] Use issueRepositorySuggestions API for repo matching
- [ ] Integrate with GitHub knowledge base for code context
- [ ] Use Nango for OAuth management
- [ ] Set up webhook data sync for real-time updates
- [ ] Notify Slack on ticket status changes
- [ ] Log all ticket operations in audit logs
- [ ] Apply RBAC to ticket operations
- [ ] Apply multi-tenant project isolation
- [ ] Apply zero-trust architecture to API access
- [ ] Apply OWASP LLM Top 10 security controls
- [ ] Set up platform monitoring for integrations
- [ ] Monitor: ticket creation rate, sync latency, API errors, agent session states
- [ ] Consider action approval for ticket creation
- [ ] Consider AI incident response for sync failures
- [ ] Test: issue-driven development end-to-end (ticket to code to PR to status)
- [ ] Test: duplicate detection prevents redundant tickets
- [ ] Test: bidirectional sync updates both systems correctly
- [ ] Test: status mapping handles all workflow states
- [ ] Test: rate limit handling with backoff
- [ ] Test: webhook delivery for status changes
- [ ] Document MCP tool usage and integration setup
- [ ] Choose: Linear for modern teams, Jira for enterprise with complex workflows
FAQ
What is AI Jira and Linear integration?
AI Jira and Linear integration connects AI agents to project management tools via MCP servers and APIs, enabling bidirectional ticket management. Autonomi: "Linear redesigned its API specifically for agentic workflows in 2025. Jira MCP servers enable GitHub Copilot and Claude to read and update tickets." CodeWithSeb: "Say implement fix for ENG-4521 and Claude reads the Jira ticket, finds the bug, writes the fix, runs tests, and commits. Under ninety seconds." Key capabilities: (1) Read tickets — get issue details, acceptance criteria, comments, linked issues. (2) Create tickets — AI agent creates issues from user requests or detected bugs. (3) Update status — transition tickets (To Do to In Progress to In Review to Done). (4) Add comments — link PRs, commit SHAs, test results. (5) Search issues — find duplicates before creating new ones. (6) Manage sub-tasks — break down epics into actionable items.
How do MCP servers connect AI agents to Jira and Linear?
MCP (Model Context Protocol) servers expose Jira and Linear APIs as tools that AI agents can call. PADISO: "Linear and Jira both have MCP servers available. The Linear Issue Tracker MCP Server by Zalab Inc is production-grade and handles searching, creating, updating issues, managing comments, and fetching team info." For Jira: (1) jira_get_issue — read ticket details, description, acceptance criteria, comments. (2) jira_transition_issue — update ticket status. (3) jira_create_issue — create new tickets. (4) jira_add_comment — add comments with PR links or test results. (5) jira_search — JQL-based search for issues. For Linear: (1) linear_search_issues — search issues. (2) linear_create_issue — create new issues. (3) linear_update_issue — update status, assignee, priority. (4) linear_create_comment — add comments. CodeWithSeb: "Linear GraphQL-based API means responses are typically faster and more structured than Jira REST API."
What is issue-driven development with AI agents?
Issue-driven development with AI agents is a workflow where Jira/Linear tickets drive AI coding agents end-to-end. CodeWithSeb: "Claude reads the ticket, understands the requirement, finds the relevant code, implements the change, runs tests, and commits." Flow: (1) Read ticket via MCP — jira_get_issue returns title, description, acceptance criteria, comments, linked issues. (2) Analyze codebase — find relevant files based on ticket context. (3) Create branch — fix/ENG-4521-description. (4) Implement fix — follow acceptance criteria as checklist. (5) Run tests — verify fix addresses reproduction steps. (6) Commit and push — reference ticket in commit message. (7) Open PR via GitHub MCP — description references ticket. (8) Transition ticket — jira_transition_issue to In Review. (9) Add comment — link PR in ticket comments. Autonomi: "Port documented the complete end-to-end flow: Jira ticket creation triggers GitHub issue generation, which triggers coding agent task execution, which writes status back to Jira."
How does Linear agent interaction system work?
Linear provides a native agent interaction system with Agent Sessions and Agent Activities. AgentSession tracks the lifecycle of an agent run with 6 states: pending, active, error, awaitingInput, complete, stale — visible to users in the Linear UI. An AgentSession is created automatically when an agent is mentioned or delegated an issue. AgentSession webhooks notify your agent when mentioned, delegated, or when a user provides additional prompts. Agents communicate progress by emitting Agent Activities — thoughts, tool calls, prompts for clarification, final responses, or errors. Agent Plans provide a session-level checklist that evolves during execution — agents can add, modify, or remove steps as they discover new tasks. The issueRepositorySuggestions API uses an LLM to return ranked repository matches for a given issue.
How do you handle bidirectional sync between Jira and Linear?
Bidirectional sync ensures ticket updates in one system reflect in the other. Autonomi: "Bidirectional sync means the project board reflects reality in real time, whether updates come from humans or agents. Unidirectional sync creates stale project boards." Linear offers native Jira Sync: connect Jira spaces to Linear teams so new issues sync in both directions. Jira epics automatically sync as Linear projects. Implementation patterns: (1) Webhook-based — listen for ticket changes via webhooks, update corresponding ticket. (2) Adapter pattern — reusable adapter for extension to other PM tools. (3) Status mapping — To Do to Backlog, In Progress to Started, Done to Done. (4) JQL injection prevention — strict epic key validation. (5) Rate limit handling — Jira Cloud has 2,000 requests per hour limit with backoff.
Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →