Jira Tools
Team Context (load before any Jira call)
Private team values live in ~/OneDrive/work/contexts/jira-data.yaml.
- Read the ENTIRE file once per session. No partial reads; risks missing
my_team (pointer to default team key) or mixing cross-team values.
- Resolve team: user-named team, else top-level
my_team field.
- Substitute placeholders from resolved team:
{cloud_id}, {project_key},
{team_field}, {team_id}, {sprint_field}, {refinement_sprint_id},
{active_sprint_board_id}.
- File missing: ask user. Do not guess. End turn.
Active (current) sprint is NOT constant
{refinement_sprint_id} is stable; the active sprint id rotates every sprint.
Never hardcode the active sprint id. Resolve it at runtime.
{active_sprint_board_id} is the constant agile board; sprints are its
children. Resolve the active sprint from the board in one call (verified):
acli jira board list-sprints --id {active_sprint_board_id} --state active --json
Read id from the single active sprint. Flag is --id (the board id), not
--board. The command is board list-sprints; there is no
acli jira sprint list.
Do NOT use the old search+view path (customfield_10020 is rejected in a
search --fields list and stripped from search results). The board endpoint
replaces it.
Tool Priority
Prefer MCP (mcp__atlassian__*) for everything. Supports markdown
(contentFormat: "markdown") and full ADF (contentFormat: "adf"). Pass
cloudId: "{cloud_id}" on all MCP calls.
Use acli only when MCP cannot do the job, or not available (bulk ops, advanced
JQL export, edge cases).
MCP Create/Edit Notes
contentFormat: "adf" required when description contains acceptance criteria
or any checklist. Jira markdown renders - [ ] as literal text in bullets,
not checkboxes. Only taskList/taskItem ADF nodes produce real checkboxes.
contentFormat: "markdown" for simple descriptions (headings, bold, lists,
links, no checklists).
contentFormat: "adf" for full control (checkboxes, panels, complex layouts).
Pass ADF JSON as the description string.
Always set team and sprint via additional_fields:
"additional_fields": {
"{team_field}": "{team_id}",
"{sprint_field}": {refinement_sprint_id}
}
Epic link: "parent": "{project_key}-NNN".
API response body is unreliable for verifying rendering. Create/edit/get
responses echo description as markdown even when sent or requested as ADF
(including responseContentFormat: "adf"); ADF features (checkboxes, expand,
panels) appear flat in the echo but render correctly in Jira. Verify by
opening the issue URL.
expand IS valid in issue ADF. editJiraIssue INVALID_INPUT almost always
means a structural error in a sibling node, not expand. Most common:
paragraph-wrapped taskItem.content (see ADF Reference).
ADF Reference (for contentFormat: "adf")
Pass ADF JSON as the description value.
Canonical docs:
https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/.
Per-node spec at .../document/nodes/<nodeName>/.
Debugging a node
Fetch the node spec for allowed content, attrs, marks:
defuddle parse \
"https://developer.atlassian.com/cloud/jira/platform/apis/document/nodes/<nodeName>/" \
--md
Still rejected: isolate by replacing the suspect subtree with a minimal valid
node (e.g. taskList -> bulletList). Success means the suspect is the
cause. MCP errors usually name the offending node.
MCP-stricter-than-spec traps:
taskItem.content: bare inline nodes, NOT paragraph-wrapped. Same rule
inside expand. See
atlassian/atlassian-mcp-server#25
(also confirms markdown - [ ] does NOT auto-convert to taskList; send
ADF).
tableCell / tableHeader / panel / expand: block-node children only;
wrap raw text in paragraph.
ADF Node Types
Use inside description.content:
Paragraph:
{ "type": "paragraph", "content": [{ "type": "text", "text": "..." }] }
Heading:
{ "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "..." }] }
Code block:
{ "type": "codeBlock", "attrs": { "language": "tsx" }, "content": [{ "type": "text", "text": "..." }] }
Bullet list:
{ "type": "bulletList", "content": [{ "type": "listItem", "content": [{ "type": "paragraph", "content": [...] }] }] }
Ordered list:
{ "type": "orderedList", "content": [/* same as bulletList */] }
Bold text:
{ "type": "text", "text": "...", "marks": [{ "type": "strong" }] }
Inline code:
{ "type": "text", "text": "...", "marks": [{ "type": "code" }] }
Task list (checkboxes):
{ "type": "taskList", "attrs": { "localId": "ac-1" }, "content": [{ "type": "taskItem", "attrs": { "localId": "ac-1-1", "state": "TODO" }, "content": [{ "type": "text", "text": "..." }] }] }
Acceptance criteria MUST use taskList/taskItem, not bulletList.
taskItem.content MUST be bare text nodes; paragraph-wrapping is rejected
with INVALID_INPUT (see Debugging trap above).
Expand (collapsible section):
{ "type": "expand", "attrs": { "title": "..." }, "content": [<block nodes>] }
Children: block nodes (paragraph, bulletList, heading, etc.). Use to
hide long context (findings, traces) below the fold.
Inline smart link (renders as Jira card / Notion preview / any URL):
{ "type": "inlineCard", "attrs": { "url": "https://{cloud_id}/browse/{project_key}-NNN" } }
Use inside a paragraph content array. Works for any URL.
Panel (info/warning/error/success callout):
{
"type": "panel",
"attrs": { "panelType": "info" },
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Note text here" }]
}
]
}
panelType: info, note, warning, error, success.
Table:
{
"type": "table",
"attrs": { "isNumberColumnEnabled": false, "layout": "default" },
"content": [
{
"type": "tableRow",
"content": [
{
"type": "tableHeader",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Header" }]
}
]
}
]
},
{
"type": "tableRow",
"content": [
{
"type": "tableCell",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Cell" }]
}
]
}
]
}
]
}
Header row: tableHeader. Data rows: tableCell. Cell content: block nodes
(wrap text in paragraph).
Horizontal rule: { "type": "rule" }
Link mark:
{ "type": "text", "text": "click here", "marks": [{ "type": "link", "attrs": { "href": "https://..." } }] }
Issue Linking (Blocks, Relates, etc.)
Use MCP mcp__atlassian__createIssueLink. Available link types:
| Name |
Inward (passive) |
Outward (active) |
| Blocks |
is blocked by |
blocks |
| Relates |
relates to |
relates to |
| Duplicate |
is duplicated by |
duplicates |
| Fixes |
is fixed by |
fixes |
| Improves |
is improved by |
improves |
Directionality for "Blocks": inwardIssue = blocker, outwardIssue = blocked.
A blocks B:
inwardIssue: "{project_key}-A", outwardIssue: "{project_key}-B", type: "Blocks"
Subtasks
MCP create with issueTypeName: "Sub-task" and parent: "{project_key}-NNN".
Same parent field as epic linking; issue type determines subtask vs. epic
child.
Sprint Field
{sprint_field} takes a plain integer, not an object:
"{sprint_field}": {refinement_sprint_id}
NOT { "id": ... } (errors with "Number value expected as the Sprint id").
Active (current) sprint vs refinement
jira-data.yaml holds refinement_sprint_id and active_sprint_board_id. The
current/active sprint id is NOT in the yaml and changes every sprint. To target
the active sprint (user says "current sprint", "this sprint", "not refinement"),
resolve it live from the board in one call:
acli jira board list-sprints --id {active_sprint_board_id} --state active --json
then read id from the single active sprint. Use that integer as
{sprint_field} in the create JSON.
Parent / Epic Linking
MCP create: "parent": "{project_key}-NNN". MCP edit
(mcp__atlassian__editJiraIssue):
fields: {"parent": {"key": "{project_key}-NNN"}}.
acli Fallback Reference
Use acli jira workitem subcommands only when MCP cannot do the job.
Quick Reference
Substitute {project_key} before running.
| Operation |
Command |
| View |
acli jira workitem view {project_key}-N |
| View (all) |
acli jira workitem view {project_key}-N --fields '*all' --json |
| Search |
acli jira workitem search --jql "project = {project_key} AND ..." --limit 20 |
| Create |
acli jira workitem create --project {project_key} --type Task --summary "..." |
| Create ADF |
acli jira workitem create --from-json file.json |
| Edit |
acli jira workitem edit --key {project_key}-N --summary "New title" |
| Transition |
acli jira workitem transition --key {project_key}-N --status "In Progress" |
| Comment |
acli jira workitem comment create --key {project_key}-N --body "..." |
acli pitfalls (verified)
acli jira workitem edit prompts y/N; pass --yes for non-interactive runs.
- No
acli jira me. Get your accountId via JQL:
acli jira workitem search --jql "assignee = currentUser()" --limit 1 --json,
read fields.assignee.accountId.
- Assignee on create: nest inside
additionalAttributes as
"assignee": { "accountId": "..." }.
- Active sprint id: resolve at runtime (see "Active (current) sprint" above);
never reuse
{refinement_sprint_id} for it.
acli JSON Template (for --from-json)
Assignee on create: add assignee.accountId inside additionalAttributes.
There is NO acli jira me. Get your own accountId via:
acli jira workitem search --jql "assignee = currentUser()" --limit 1 --json
then read fields.assignee.accountId.
{
"additionalAttributes": {
"{team_field}": "{team_id}",
"{sprint_field}": {refinement_sprint_id},
"assignee": { "accountId": "712020:..." }
},
"parentIssueId": "{project_key}-NNN",
"projectKey": "{project_key}",
"summary": "Ticket title",
"type": "Task",
"description": {
"type": "doc",
"version": 1,
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Description here." }]
}
]
}
}
acli Edit JSON Format
Uses "issues" (array of keys) instead of "projectKey":
{
"issues": ["{project_key}-N"],
"description": {
"type": "doc",
"version": 1,
"content": [...]
}
}
Ticket framing (summary = deliverable, not motivation)
The summary and description must describe the WORK to be done, not the event
that prompted it. A PR, bug sighting, or incident is the MOTIVATION; it belongs
in the description as context, never as the headline.
- Wrong: "Remove duplicate handler type in chat-embedded (PR #2100)" — that
frames a one-off PR fix when the actual deliverable was a review-rule
enhancement.
- Right: "Enhance AI-review duplication rule: same-app scope + near-dup
detection" — names the durable change; the PR is one motivating line in the
body.
Before creating: ask "what is the lasting deliverable?" If the summary names a
PR/ticket/incident as its subject, rewrite it to name the change instead.
Common Workflows
Read a ticket from a URL
Extract key from URL (e.g. {project_key}-NNN from
https://{cloud_id}/browse/{project_key}-NNN), then MCP
mcp__atlassian__getJiraIssue with issueIdOrKey: "{project_key}-NNN".
Search team tickets
MCP mcp__atlassian__searchJiraIssuesUsingJql, or fallback:
acli jira workitem search --jql "project = {project_key} AND status = 'New'" --limit 20
acli jira workitem search --jql "project = {project_key} AND sprint = {refinement_sprint_id}" --limit 50
1---2name: jira-atlassian-cli3description: Load before any Jira interaction or mcp__atlassian__* call. Holds required field values (team ID, sprint IDs, custom field mappings) unavailable elsewhere; skipping causes missing fields on created tickets. Triggers: Jira URLs, ticket keys (e.g. ABC-123), MCP tools mcp__atlassian__*, acli, or keywords create/read/edit/search/transition, ticket, Jira, issue, sprint, backlog, refinement, work item.4---56# Jira Tools78## Team Context (load before any Jira call)910Private team values live in `~/OneDrive/work/contexts/jira-data.yaml`.1112- Read the ENTIRE file once per session. No partial reads; risks missing13 `my_team` (pointer to default team key) or mixing cross-team values.14- Resolve team: user-named team, else top-level `my_team` field.15- Substitute placeholders from resolved team: `{cloud_id}`, `{project_key}`,16 `{team_field}`, `{team_id}`, `{sprint_field}`, `{refinement_sprint_id}`,17 `{active_sprint_board_id}`.18- File missing: ask user. Do not guess. End turn.1920### Active (current) sprint is NOT constant2122- `{refinement_sprint_id}` is stable; the active sprint id rotates every sprint.23- Never hardcode the active sprint id. Resolve it at runtime.24- `{active_sprint_board_id}` is the constant agile board; sprints are its25 children. Resolve the active sprint from the board in one call (verified):2627 ```bash28 acli jira board list-sprints --id {active_sprint_board_id} --state active --json29 ```3031 Read `id` from the single active sprint. Flag is `--id` (the board id), not32 `--board`. The command is `board list-sprints`; there is no33 `acli jira sprint list`.3435- Do NOT use the old search+view path (`customfield_10020` is rejected in a36 search `--fields` list and stripped from search results). The board endpoint37 replaces it.3839## Tool Priority4041Prefer MCP (`mcp__atlassian__*`) for everything. Supports markdown42(`contentFormat: "markdown"`) and full ADF (`contentFormat: "adf"`). Pass43`cloudId: "{cloud_id}"` on all MCP calls.4445Use `acli` only when MCP cannot do the job, or not available (bulk ops, advanced46JQL export, edge cases).4748## MCP Create/Edit Notes4950- `contentFormat: "adf"` required when description contains acceptance criteria51 or any checklist. Jira markdown renders `- [ ]` as literal text in bullets,52 not checkboxes. Only `taskList`/`taskItem` ADF nodes produce real checkboxes.53- `contentFormat: "markdown"` for simple descriptions (headings, bold, lists,54 links, no checklists).55- `contentFormat: "adf"` for full control (checkboxes, panels, complex layouts).56 Pass ADF JSON as the `description` string.57- Always set team and sprint via `additional_fields`:5859 ```json60 "additional_fields": {61 "{team_field}": "{team_id}",62 "{sprint_field}": {refinement_sprint_id}63 }64 ```6566- Epic link: `"parent": "{project_key}-NNN"`.67- API response body is unreliable for verifying rendering. Create/edit/get68 responses echo `description` as markdown even when sent or requested as ADF69 (including `responseContentFormat: "adf"`); ADF features (checkboxes, expand,70 panels) appear flat in the echo but render correctly in Jira. Verify by71 opening the issue URL.72- `expand` IS valid in issue ADF. `editJiraIssue` `INVALID_INPUT` almost always73 means a structural error in a sibling node, not `expand`. Most common:74 paragraph-wrapped `taskItem.content` (see ADF Reference).7576## ADF Reference (for `contentFormat: "adf"`)7778Pass ADF JSON as the `description` value.7980Canonical docs:81https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/.82Per-node spec at `.../document/nodes/<nodeName>/`.8384### Debugging a node85861. Fetch the node spec for allowed `content`, `attrs`, `marks`:8788 ```bash89 defuddle parse \90 "https://developer.atlassian.com/cloud/jira/platform/apis/document/nodes/<nodeName>/" \91 --md92 ```93942. Still rejected: isolate by replacing the suspect subtree with a minimal valid95 node (e.g. `taskList` -> `bulletList`). Success means the suspect is the96 cause. MCP errors usually name the offending node.97983. MCP-stricter-than-spec traps:99 - `taskItem.content`: bare inline nodes, NOT `paragraph`-wrapped. Same rule100 inside `expand`. See101 [atlassian/atlassian-mcp-server#25](https://github.com/atlassian/atlassian-mcp-server/issues/25)102 (also confirms markdown `- [ ]` does NOT auto-convert to `taskList`; send103 ADF).104 - `tableCell` / `tableHeader` / `panel` / `expand`: block-node children only;105 wrap raw text in `paragraph`.106107### ADF Node Types108109Use inside `description.content`:110111- **Paragraph:**112 `{ "type": "paragraph", "content": [{ "type": "text", "text": "..." }] }`113- **Heading:**114 `{ "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "..." }] }`115- **Code block:**116 `{ "type": "codeBlock", "attrs": { "language": "tsx" }, "content": [{ "type": "text", "text": "..." }] }`117- **Bullet list:**118 `{ "type": "bulletList", "content": [{ "type": "listItem", "content": [{ "type": "paragraph", "content": [...] }] }] }`119- **Ordered list:**120 `{ "type": "orderedList", "content": [/* same as bulletList */] }`121- **Bold text:**122 `{ "type": "text", "text": "...", "marks": [{ "type": "strong" }] }`123- **Inline code:**124 `{ "type": "text", "text": "...", "marks": [{ "type": "code" }] }`125- **Task list (checkboxes):**126 `{ "type": "taskList", "attrs": { "localId": "ac-1" }, "content": [{ "type": "taskItem", "attrs": { "localId": "ac-1-1", "state": "TODO" }, "content": [{ "type": "text", "text": "..." }] }] }`127128 Acceptance criteria MUST use `taskList`/`taskItem`, not `bulletList`.129 `taskItem.content` MUST be bare `text` nodes; paragraph-wrapping is rejected130 with `INVALID_INPUT` (see Debugging trap above).131132- **Expand (collapsible section):**133 `{ "type": "expand", "attrs": { "title": "..." }, "content": [<block nodes>] }`134 Children: block nodes (`paragraph`, `bulletList`, `heading`, etc.). Use to135 hide long context (findings, traces) below the fold.136137- **Inline smart link** (renders as Jira card / Notion preview / any URL):138 `{ "type": "inlineCard", "attrs": { "url": "https://{cloud_id}/browse/{project_key}-NNN" } }`139 Use inside a `paragraph` content array. Works for any URL.140141- **Panel** (info/warning/error/success callout):142143 ```json144 {145 "type": "panel",146 "attrs": { "panelType": "info" },147 "content": [148 {149 "type": "paragraph",150 "content": [{ "type": "text", "text": "Note text here" }]151 }152 ]153 }154 ```155156 `panelType`: `info`, `note`, `warning`, `error`, `success`.157158- **Table:**159160 ```json161 {162 "type": "table",163 "attrs": { "isNumberColumnEnabled": false, "layout": "default" },164 "content": [165 {166 "type": "tableRow",167 "content": [168 {169 "type": "tableHeader",170 "content": [171 {172 "type": "paragraph",173 "content": [{ "type": "text", "text": "Header" }]174 }175 ]176 }177 ]178 },179 {180 "type": "tableRow",181 "content": [182 {183 "type": "tableCell",184 "content": [185 {186 "type": "paragraph",187 "content": [{ "type": "text", "text": "Cell" }]188 }189 ]190 }191 ]192 }193 ]194 }195 ```196197 Header row: `tableHeader`. Data rows: `tableCell`. Cell content: block nodes198 (wrap text in `paragraph`).199200- **Horizontal rule:** `{ "type": "rule" }`201- **Link mark:**202 `{ "type": "text", "text": "click here", "marks": [{ "type": "link", "attrs": { "href": "https://..." } }] }`203204## Issue Linking (Blocks, Relates, etc.)205206Use MCP `mcp__atlassian__createIssueLink`. Available link types:207208| Name | Inward (passive) | Outward (active) |209| ------------- | ---------------- | ---------------- |210| **Blocks** | is blocked by | blocks |211| **Relates** | relates to | relates to |212| **Duplicate** | is duplicated by | duplicates |213| **Fixes** | is fixed by | fixes |214| **Improves** | is improved by | improves |215216Directionality for "Blocks": `inwardIssue` = blocker, `outwardIssue` = blocked.217A blocks B:218219```220inwardIssue: "{project_key}-A", outwardIssue: "{project_key}-B", type: "Blocks"221```222223## Subtasks224225MCP create with `issueTypeName: "Sub-task"` and `parent: "{project_key}-NNN"`.226Same `parent` field as epic linking; issue type determines subtask vs. epic227child.228229### Sprint Field230231`{sprint_field}` takes a plain integer, not an object:232233```json234"{sprint_field}": {refinement_sprint_id}235```236237NOT `{ "id": ... }` (errors with "Number value expected as the Sprint id").238239#### Active (current) sprint vs refinement240241`jira-data.yaml` holds `refinement_sprint_id` and `active_sprint_board_id`. The242current/active sprint id is NOT in the yaml and changes every sprint. To target243the active sprint (user says "current sprint", "this sprint", "not refinement"),244resolve it live from the board in one call:245246`acli jira board list-sprints --id {active_sprint_board_id} --state active --json`247then read `id` from the single active sprint. Use that integer as248`{sprint_field}` in the create JSON.249250### Parent / Epic Linking251252MCP create: `"parent": "{project_key}-NNN"`. MCP edit253(`mcp__atlassian__editJiraIssue`):254`fields: {"parent": {"key": "{project_key}-NNN"}}`.255256## acli Fallback Reference257258Use `acli jira workitem` subcommands only when MCP cannot do the job.259260### Quick Reference261262Substitute `{project_key}` before running.263264| Operation | Command |265| ---------- | ------------------------------------------------------------------------------- |266| View | `acli jira workitem view {project_key}-N` |267| View (all) | `acli jira workitem view {project_key}-N --fields '*all' --json` |268| Search | `acli jira workitem search --jql "project = {project_key} AND ..." --limit 20` |269| Create | `acli jira workitem create --project {project_key} --type Task --summary "..."` |270| Create ADF | `acli jira workitem create --from-json file.json` |271| Edit | `acli jira workitem edit --key {project_key}-N --summary "New title"` |272| Transition | `acli jira workitem transition --key {project_key}-N --status "In Progress"` |273| Comment | `acli jira workitem comment create --key {project_key}-N --body "..."` |274275### acli pitfalls (verified)276277- `acli jira workitem edit` prompts y/N; pass `--yes` for non-interactive runs.278- No `acli jira me`. Get your accountId via JQL:279 `acli jira workitem search --jql "assignee = currentUser()" --limit 1 --json`,280 read `fields.assignee.accountId`.281- Assignee on create: nest inside `additionalAttributes` as282 `"assignee": { "accountId": "..." }`.283- Active sprint id: resolve at runtime (see "Active (current) sprint" above);284 never reuse `{refinement_sprint_id}` for it.285286### acli JSON Template (for `--from-json`)287288Assignee on create: add `assignee.accountId` inside `additionalAttributes`.289There is NO `acli jira me`. Get your own accountId via:290`acli jira workitem search --jql "assignee = currentUser()" --limit 1 --json`291then read `fields.assignee.accountId`.292293```json294{295 "additionalAttributes": {296 "{team_field}": "{team_id}",297 "{sprint_field}": {refinement_sprint_id},298 "assignee": { "accountId": "712020:..." }299 },300 "parentIssueId": "{project_key}-NNN",301 "projectKey": "{project_key}",302 "summary": "Ticket title",303 "type": "Task",304 "description": {305 "type": "doc",306 "version": 1,307 "content": [308 {309 "type": "paragraph",310 "content": [{ "type": "text", "text": "Description here." }]311 }312 ]313 }314}315```316317### acli Edit JSON Format318319Uses `"issues"` (array of keys) instead of `"projectKey"`:320321```json322{323 "issues": ["{project_key}-N"],324 "description": {325 "type": "doc",326 "version": 1,327 "content": [...]328 }329}330```331332## Ticket framing (summary = deliverable, not motivation)333334The summary and description must describe the WORK to be done, not the event335that prompted it. A PR, bug sighting, or incident is the MOTIVATION; it belongs336in the description as context, never as the headline.337338- Wrong: "Remove duplicate handler type in chat-embedded (PR #2100)" — that339 frames a one-off PR fix when the actual deliverable was a review-rule340 enhancement.341- Right: "Enhance AI-review duplication rule: same-app scope + near-dup342 detection" — names the durable change; the PR is one motivating line in the343 body.344345Before creating: ask "what is the lasting deliverable?" If the summary names a346PR/ticket/incident as its subject, rewrite it to name the change instead.347348## Common Workflows349350### Read a ticket from a URL351352Extract key from URL (e.g. `{project_key}-NNN` from353`https://{cloud_id}/browse/{project_key}-NNN`), then MCP354`mcp__atlassian__getJiraIssue` with `issueIdOrKey: "{project_key}-NNN"`.355356### Search team tickets357358MCP `mcp__atlassian__searchJiraIssuesUsingJql`, or fallback:359360```bash361acli jira workitem search --jql "project = {project_key} AND status = 'New'" --limit 20362acli jira workitem search --jql "project = {project_key} AND sprint = {refinement_sprint_id}" --limit 50363```