Every sprint planning session has the same bottleneck. Someone describes a feature. Then someone else spends the next ten minutes translating that description into a Jira story — picking the project, writing the summary, formatting the user story, typing out acceptance criteria, estimating points, linking to an epic, adding labels.
The feature took thirty seconds to describe. The ticket took ten minutes to create. That ratio is broken.
Part 6 of my AI Agents series fixes it. You type one sentence. The agent creates a complete, well-structured Jira story in seconds.
What’s New in Part 6: Writing TO Jira
Parts 1 through 5 of this series only read from external systems — fetching Jira tickets, reading work logs, listing projects. Part 6 introduces the first write operation: creating and updating Jira issues via HTTP POST and PUT.
This is the conceptual leap: from an agent that observes to one that acts.
The code difference is surprisingly small. The _jira_get() helper you already know:
def _jira_get(path: str) -> dict | list:
req = urllib.request.Request(url, headers={"Authorization": _auth_header()})
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode("utf-8"))
The new _jira_post() helper:
def _jira_post(path: str, payload: dict) -> dict:
body = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=body, headers={
"Authorization": _auth_header(),
"Accept": "application/json",
"Content-Type": "application/json", # ← the only real addition
}, method="POST")
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode("utf-8"))
Three lines of difference. Same urllib, same auth, same pattern. That’s the entire new concept in Part 6.
The 8 Tools
The agent exposes eight tools, each decorated with @tool. The LLM reads the docstrings and decides which to call — not a hard-coded decision tree.
Read tools (familiar pattern):
- get_current_time — reused from Part 3 for timestamping
- get_jira_projects — lists accessible projects with keys and IDs
- get_issue_types — discovers Story/Bug/Epic/Task IDs (handles both free and paid Jira plans)
- get_epics — fetches epics directly by key instead of JQL (JQL returns 410 Gone on free Jira plans — a gotcha I hit hard)
- get_jira_ticket — reads a ticket back after creation to verify it
Write tools (new in Part 6):
- create_jira_story — the core tool: converts plain English to a full Jira issue via POST
- update_jira_story — updates summary, story points, or labels on existing tickets via PUT
- create_multiple_stories — decomposes a feature into 3–5 sub-stories and creates them all
The ADF Problem: Why Plain Text Gets Rejected
Here’s the first real gotcha in Jira API v3: you cannot send plain text as the description field. The API requires Atlassian Document Format (ADF) — a JSON structure where every paragraph is wrapped in a specific schema.
Send plain text and you get a cryptic HTTP 400 error.
The agent handles this by converting the LLM’s output into ADF automatically:
# Step 1: Strip markdown that LLMs love to generate
for line in description.strip().split("\n"):
line = re.sub(r"^#{1,6}\s*", "", line) # ## Headers
line = re.sub(r"\*\*(.*?)\*\*", r"\1", line) # **bold**
line = re.sub(r"\*(.*?)\*", r"\1", line) # *italic*
if line.strip(): clean_lines.append(line.strip())
# Step 2: Wrap each line in an ADF paragraph
adf_content = [
{"type": "paragraph", "content": [{"type": "text", "text": line}]}
for line in clean_lines
]
# Step 3: Wrap in the ADF document envelope
"description": {"type": "doc", "version": 1, "content": adf_content}
This is the kind of integration detail that takes hours to debug from the Jira docs but is ten lines of code once you know the pattern.
Second gotcha: story points in Jira Cloud use customfield_10016, not a field called story_points. Use the wrong field name and you get a 400 error that says “Field cannot be set” — not the most helpful message when you’re trying to figure out why your points aren’t saving.
The Full Story Creation Workflow
When you type "create a story for adding dark mode to the settings page", here’s what the agent loop actually does:
Iteration 1: Calls get_epics — confirms SCRUM-7/8/9 with their current status. The LLM now knows which epic to link based on the story content.
Iteration 2: LLM reasons: dark mode is a UI feature → not core agent logic → not an integration → closest to SCRUM-8 (Integrations) or it may assign to a general UI epic. It writes the full story: user story format, acceptance criteria, point estimate.
Iteration 3: Calls create_jira_story with the structured content → the tool strips markdown, builds ADF, POSTs to Jira → returns the new ticket key (e.g. SCRUM-12).
Iteration 4: Calls get_jira_ticket("SCRUM-12") → reads the ticket back, extracts ADF description to plain text, confirms everything looks correct.
Final response: Shows you the created ticket with a direct URL.
The whole thing takes about 8 seconds. The ticket in Jira is indistinguishable from one a human wrote.
Bulk Story Creation: Decomposing a Feature
The most useful prompt in the agent’s arsenal:
create 3 stories for the GitLab MR review feature
This triggers create_multiple_stories, which returns a structured decomposition prompt back to the agent:
Feature to decompose: 'GitLab MR review feature'
Please break this into 3-5 individual user stories, then call
create_jira_story for each one. Each story should be independently
deliverable. Use "As a [user] I want [goal] so that [reason]" format
with clear acceptance criteria.
The LLM then reasons about how to split the feature and calls create_jira_story sequentially for each sub-story. For a GitLab MR review feature it typically generates:
fetch_mr_diff tool— reads the code changes (2 points)AI review generation tool— generates the review prose (3 points)post_review_comment tool— publishes to GitLab MR discussion (2 points)Unit tests for review tools— test coverage (3 points)
Four tickets, properly scoped, correctly linked to the right epic. One prompt.
Epic Auto-Mapping: Smarter Than It Looks
At the top of the file, there’s a simple dictionary:
EPIC_LABEL_MAP = {
"SCRUM-7": "agent-core", # AI Agent Core — LLM, tools, agent loop
"SCRUM-8": "integrations", # Integrations — Jira, GitLab, Slack
"SCRUM-9": "testing", # Testing & Quality
}
This is loaded into the system prompt, and the LLM uses it to auto-assign the right epic based on story content. A story about Slack webhooks goes to SCRUM-8. A story about agent memory goes to SCRUM-7. A story about pytest coverage goes to SCRUM-9.
No explicit rule engine. No keyword matching. The LLM reads the epic descriptions in the system prompt and makes the right call based on semantic understanding of the story content.
When it gets it wrong (it occasionally does), you can override with:
create a story for improving agent retry logic under SCRUM-7
Explicit epic key in the prompt overrides the auto-mapping.
The Closed Loop
This is the part I find most satisfying about the series structure. Part 6 closes a loop that started in Part 4:
Part 6 creates Jira stories from plain English → stories land in your backlog.
Parts 4–5 (standup reporter) fetch your in-progress tickets from Jira and write your daily standup. As soon as you move a story to “In Progress,” the standup reporter picks it up automatically.
Part 7 (GitLab agent) creates branches and MRs with the Jira ticket key embedded in the branch name (feature/SCRUM-12-dark-mode), linking code back to the story.
One sentence to create a story. The rest of the workflow runs without you.
Setup
Prerequisites: Python 3.10+, Ollama running locally, a Jira account with API access.
pip install langchain-ollama langchain-core python-dotenv
ollama pull qwen3.5:9b
Get your Jira API token: id.atlassian.com → Security → Create and manage API tokens. Takes two minutes. The token is shown once — copy it immediately.
# .env file
JIRA_BASE_URL=https://yourcompany.atlassian.net
JIRA_EMAIL=you@yourcompany.com
JIRA_API_TOKEN=ATATxxxxxxxxxxxxxxxx
Update the epic map at the top of the file with your actual epic keys:
DEFAULT_PROJECT_KEY = "YOUR_PROJECT"
EPIC_LABEL_MAP = {
"YOUR-1": "your-first-epic-label",
"YOUR-2": "your-second-epic-label",
}
Run:
python jira_user_story_creator_agent.py
The startup screen shows config status. Start with show me my Jira projects to confirm the connection, then show me the epics in YOUR_PROJECT to verify epic keys.
What I Learned Building This
Jira’s free plan has surprising API restrictions. JQL search for epics (issuetype = Epic) returns HTTP 410 Gone on team-managed free projects. The fix — fetching each epic directly by key via /issue/{key} — works on all plan types. Worth knowing before you spend an hour debugging a 410 error.
The system prompt is your contract with the LLM. The agent writes good user stories because the system prompt explicitly defines what a good story looks like — the format, the acceptance criteria count, the point scale. Vague system prompt → vague stories. Specific system prompt → sprint-ready tickets.
Verify after creation, every time. The get_jira_ticket call after create_jira_story isn’t just for the user — it catches edge cases where the ticket created but a field silently failed (labels not applied, epic not linked). The agent can self-correct on the next iteration if the verification shows something is wrong.
create_multiple_stories is a meta-tool. It doesn’t call the Jira API at all — it returns a prompt that drives the agent’s next several iterations. This pattern (a tool that orchestrates other tool calls) is genuinely useful for anything requiring multi-step decomposition. Worth internalizing as a pattern.
What’s Next
The series continues:
- Part 7 (already published): GitLab Developer Workflow Agent — 15 tools covering the complete branch → commit → MR → AI review → merge pipeline. The branch names embed the Jira keys created here.
- Part 8: Slack notifications — posting to your team channel when stories are created or MRs are merged.
- Part 9: Cross-session memory — the agent remembers your preferred story style and point estimation patterns across sessions.
The Pattern, Again
Four parts into real-world agents and the architecture hasn’t changed:
Real data source → Tools → LLM reasons → Real output
For the story creator: Plain English → 8 @tool functions → Ollama/Gemini → Jira backlog
The only thing that changes is what you plug into the boxes. Once you know the loop, you can build a new agent for almost any workflow in an afternoon. That’s the whole point of the series.
Full code in the GitLab repo linked in my profile. Part 7 (GitLab workflow agent) is already up if you want to see where this loop goes next.