Atlassian Skill
Use the Atlassian Command Line Interface (ACLI) for supported Jira and Confluence operations. Use the bundled REST API scripts only where ACLI does not provide the required operation or behavior.
ACLI installation and authentication
Check whether ACLI is installed:
command -v acli
If the command is missing, install ACLI globally through mise and the aqua registry:
mise use -g aqua:atlassian.com/acli
Check that each product is authenticated:
acli jira auth status
acli confluence auth status
If a product is not authenticated, use its browser login:
acli jira auth login --web
acli confluence auth login --web
Jira and Confluence can select different accounts. Switch them independently:
acli jira auth switch
acli confluence auth switch
Script authentication
REST scripts identify the selected user from current_profile in
~/.config/acli/jira_config.yaml or confluence_config.yaml. Script logic MUST
NOT parse the human-readable output of acli auth status.
API tokens live in ~/.local/secrets/atlassian-tokens.json, organized first by
site and then by email:
{
"yourcompany.atlassian.net": {
"user@example.com": "<api-token>",
"another-user@example.com": "<api-token>"
}
}
The file MUST have mode 0600. Scripts find the exact site-and-email entry for
the selected product profile and never fall back to another account. Before the
first request, they call the product's current-user endpoint and verify that the
token's account ID matches the selected ACLI profile.
If the selected account has no token, stop and ask the user to create an Atlassian API token at https://id.atlassian.com/manage-profile/security/api-tokens and add it under the matching site and email. Never ask the user to paste the token into chat.
Script dependency recovery
If a bundled script reports a missing dependency, run this from the skill directory before retrying:
bun ci
This installs the lockfile-pinned dependencies required by the scripts.
Usage Guidelines
Apply these rules on every Jira create, update, or comment, and every Confluence create or update.
Write complex documents in ADF
Jira descriptions accept Markdown (converted to ADF) or a version-1 ADF
document (descriptionFormat: "adf"). The Markdown converter cannot
represent merged or split cells, cell backgrounds, multiline cell bodies,
or smart links.
MUST write ADF from the start when the document needs any of:
- Tables with multiline cell content, nested lists or code in a cell,
merged/split cells (
colspan/rowspan), or cell backgrounds - Nested lists
- Panels, expands, layouts, or other ADF-only nodes
- Inline live links or cards to Jira issues or Confluence pages
MAY use Markdown only for short prose: headings, emphasis, flat lists, fenced code, and extremely simple one-paragraph tables with no merged cells.
If any ADF-only feature is needed, write the full ADF document from the
start and send it with descriptionFormat: "adf" through a JSON file
(@/tmp/payload.json).
Jira comments currently go through the Markdown converter. Keep comments simple, and put complex tables in the issue description as ADF.
Confluence page bodies in this skill use storage HTML, which already supports complex tables. The ADF-vs-Markdown rule applies to Jira descriptions.
Link Jira issues and Confluence pages
When mentioning another Jira issue or Confluence page, SHOULD turn that
mention into a hyperlink. A bare issue key (PROJ-123) or unlinked page
title is not enough.
Build the URL from script output (url on jira-get, jira-search,
confluence-get, or confluence-search) so the host matches this site.
- Markdown:
[PROJ-123](https://<site>/browse/PROJ-123)and[Page title](https://<site>/wiki/...) - Confluence storage HTML:
<a href="https://<site>/browse/PROJ-123">PROJ-123</a> - ADF: an
inlineCardinside a paragraph, or a standaloneblockCard, withattrs.urlset to that URL
Smart links are heavier than a text link. An inline Jira card also
renders the issue summary and status. In dense places (tables, long key
lists, repeated mentions in one paragraph), use a text node with a
link mark, or Markdown [key](url). Do not put an inlineCard on
every mention.
Set Jira issue links when the relationship is clear
Mentioning issue A while creating or editing issue B usually means the
issues are related. If the relationship is clear, SHOULD also create the
Jira issue link on B with jira-link.ts. A textual mention is not a
substitute.
The relationship phrase is the inward or outward wording as it should read on B:
- B is blocked by A →
add B "is blocked by" A - B blocks A →
add B "blocks" A - B duplicates A →
add B "duplicates" A
Link type names are instance-specific. Run jira-link.ts types and copy
the inward or outward phrase. List existing links on B first
(jira-get.ts or jira-link.ts list) and skip if the same type already
points at A.
Do not create a link when the mention is incidental or the relationship
is unclear. Do not guess a type name such as "Blocks"; pass the
phrase.
Available commands and scripts
Use ACLI for the operations documented with acli commands below. The bundled
scripts remain only where ACLI does not cover the full behavior.
Jira
Search Issues
bunx tsx scripts/jira-search.ts "<JQL query>" [maxResults] [nextPageToken]
Examples:
bunx tsx scripts/jira-search.ts "assignee = currentUser() AND status != Done"bunx tsx scripts/jira-search.ts "project = PROJ AND type = Bug" 50bunx tsx scripts/jira-search.ts "project = PROJ" 50 "token..."(pagination)
See docs/jql-guide.md for JQL syntax reference.
Get Issue Details
bunx tsx scripts/jira-get.ts <issueKey>
Example: bunx tsx scripts/jira-get.ts PROJ-123
Returns url, the description ADF, and links (each relationship phrase
as it reads on this issue).
Create Issue
bunx tsx scripts/jira-create.ts '<JSON>'
Single issue:
bunx tsx scripts/jira-create.ts '{"project": "PROJ", "type": "Story", "summary": "New feature", "description": "Details here"}'
Bulk create (array):
bunx tsx scripts/jira-create.ts '[{"project": "PROJ", "type": "Bug", "summary": "Bug 1"}, {"project": "PROJ", "type": "Bug", "summary": "Bug 2"}]'
Update Issue
bunx tsx scripts/jira-update.ts <issueKey> '<JSON updates>'
Example: bunx tsx scripts/jira-update.ts PROJ-123 '{"status": "In Progress", "assignee": "user@example.com"}'
Description payloads accept descriptionFormat:
"markdown"is the default.descriptionmust be a string and the scripts convert it to ADF. Use it only for short, simple content. See Usage Guidelines."adf"sends a version-1 ADF document asdescriptionwithout conversion. MUST use it for complex documents: nested lists, complex tables, panels, smart links, and other ADF-only nodes.
Both jira-create.ts and jira-update.ts support the field. For a non-trivial
ADF payload, write the JSON to a file:
{
"descriptionFormat": "adf",
"description": {
"type": "doc",
"version": 1,
"content": []
}
}
Writing ADF Text
descriptionFormat: "adf" bypasses the Markdown converter. The scripts send
each ADF text value unchanged, so Markdown delimiters render as literal
characters.
When writing ADF, MUST NOT place Markdown syntax in a text value. For
example, `command`, **bold**, and [label](https://example.com) are
literal text, not formatting. Use ADF nodes and marks instead:
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Run " },
{
"type": "text",
"text": "command",
"marks": [{ "type": "code" }]
}
]
}
Use marks for inline formatting (code, strong, em, strike,
underline, and link). Use structural ADF nodes such as heading,
bulletList, orderedList, table, inlineCard, blockCard, and
codeBlock for block and smart-link formatting. If the source content is
simple Markdown, omit descriptionFormat or set it to "markdown" so the
converter creates the ADF document. If it needs any ADF-only feature, write
ADF from the start.
ADF tables
Markdown tables are one paragraph per cell, with no colspan, rowspan, or background. MUST use ADF for anything richer.
Cell content is a block array: multiple paragraphs, lists, or code blocks
are valid. tableHeader / tableCell attrs include colspan, rowspan,
and background.
{
"type": "table",
"attrs": { "isNumberColumnEnabled": false, "layout": "default" },
"content": [
{
"type": "tableRow",
"content": [
{
"type": "tableHeader",
"attrs": { "background": "#deebff" },
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Component" }]
}
]
},
{
"type": "tableHeader",
"attrs": { "background": "#deebff" },
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Notes" }]
}
]
}
]
},
{
"type": "tableRow",
"content": [
{
"type": "tableCell",
"attrs": { "colspan": 2, "background": "#f4f5f7" },
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Shared across both columns." }]
},
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Second paragraph in the same cell." }]
}
]
}
]
}
]
}
ADF smart links
inlineCard belongs in a paragraph content array. blockCard is a
top-level block. Set attrs.url to the browse URL or Confluence page URL
from script output.
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Blocked by " },
{
"type": "inlineCard",
"attrs": { "url": "https://yourcompany.atlassian.net/browse/PROJ-123" }
},
{ "type": "text", "text": "." }
]
}
{
"type": "blockCard",
"attrs": {
"url": "https://yourcompany.atlassian.net/wiki/spaces/DEV/pages/123456"
}
}
In a dense table of issue keys, use a text node with a link mark instead
of inlineCard:
{
"type": "text",
"text": "PROJ-123",
"marks": [
{
"type": "link",
"attrs": { "href": "https://yourcompany.atlassian.net/browse/PROJ-123" }
}
]
}
bunx tsx scripts/jira-update.ts PROJ-123 @/tmp/issue-update.json
Jira replaces the whole description document on update; it does not patch
individual ADF nodes. Read the issue first, transform the affected nodes
locally, then send the complete description document in the update payload.
Never replace the document with a partial ADF fragment.
Jira Description Lists
Use Markdown only for flat lists. For a numbered list with nested bullets, use
descriptionFormat: "adf" with jira-create.ts or jira-update.ts. The
Markdown converter can emit a series of one-item orderedList nodes followed
by sibling bulletList nodes. Jira renders every ordered-list node as 1. and
does not indent the sibling bullets.
The scripts reject nested Markdown lists before sending an update, rather than producing malformed ADF. Use explicit ADF when a description needs nested lists.
For an existing issue, use jira-get.ts to read the current document,
transform the required nodes locally, and send the complete ADF document through
jira-update.ts. Jira does not support a partial-node description update.
{
"type": "orderedList",
"attrs": { "order": 1 },
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Derive signing keys." }]
},
{
"type": "bulletList",
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Derive from the master key." }]
}
]
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Add key discovery." }]
}
]
}
]
}
The invariants are:
- One logical numbered sequence is one
orderedListnode with every top-level item in itscontentarray. Do not create oneorderedListper item. - A subordinate
bulletListis a child of its parentlistItem, after that item's paragraph. Do not place it beside theorderedListas a top-level sibling. - Read the issue back after updating it. Confirm the sequence is one ordered list with the expected item count and that each subordinate bullet list is nested under its parent item.
Comments
# Get comments
bunx tsx scripts/jira-comment.ts <issueKey> get
# Add comment - inline markdown
bunx tsx scripts/jira-comment.ts <issueKey> add "<markdown text>"
# Add comment - read markdown from a file
bunx tsx scripts/jira-comment.ts <issueKey> add -f <path>
# Add comment - read markdown from stdin (pipe-friendly)
cat notes.md | bunx tsx scripts/jira-comment.ts <issueKey> add --stdin
Comment bodies support full markdown via the same converter used for issue
descriptions: headings, bold/italic/strike/underline/inline-code, bullet/
ordered/task lists, fenced code blocks (with language), tables, blockquotes,
GitHub-style alerts (> [!NOTE] etc.), <details> expand blocks, footnotes,
and links.
For long or multi-line comment bodies, prefer -f <path> or --stdin over
inline arguments to avoid shell argument-length limits and quoting issues.
When a comment mentions another Jira issue or Confluence page, SHOULD
hyperlink it with Markdown ([PROJ-123](https://<site>/browse/PROJ-123)).
Comments cannot send ADF smart links.
Issue Links
bunx tsx scripts/jira-link.ts types
bunx tsx scripts/jira-link.ts list <issueKey>
bunx tsx scripts/jira-link.ts add <issueKey> "<relationship>" <otherIssueKey>
bunx tsx scripts/jira-link.ts remove <linkId>
relationship is the inward or outward phrase as it should read on
issueKey, copied from types. Do not pass the type name ("Blocks").
Examples:
bunx tsx scripts/jira-link.ts add PROJ-200 "is blocked by" PROJ-100bunx tsx scripts/jira-link.ts add PROJ-200 "blocks" PROJ-100bunx tsx scripts/jira-link.ts add PROJ-200 "relates to" PROJ-100
List links before adding. A duplicate of the same type and direction is a no-op
(alreadyLinked: true).
Resolve Field Names / IDs
bunx tsx scripts/jira-fields.ts [filter]
Examples:
bunx tsx scripts/jira-fields.ts(list every field)bunx tsx scripts/jira-fields.ts "story points"(find a field by name)bunx tsx scripts/jira-fields.ts customfield_10023(resolve an ID back to its name)
Returns JSON: { count, fields: [{ id, name, custom, type, customType }] }.
Custom field ID rule (MANDATORY): Jira custom field IDs (e.g.
customfield_10023) are instance-specific — the same ID maps to different
fields on different Jira sites, and an unexplained ID is unverifiable by anyone
reading your output. Therefore:
- Never hardcode or copy a
customfield_*ID without first resolving its name for the current instance viajira-fields.ts. - When you set custom fields on an issue, include an ID → name mapping table
in your response so the user can confirm each field is correct (e.g.
customfield_10023 → "Story Points"). - If you are unsure which ID corresponds to a field name the user mentioned,
resolve it first (
jira-fields.ts "<name>") instead of guessing.
Upload Attachment
bunx tsx scripts/jira-attachment.ts <issueKey> <filePath> [fileName]
Examples:
bunx tsx scripts/jira-attachment.ts PROJ-123 ./diagram.pngbunx tsx scripts/jira-attachment.ts PROJ-123 ./image.png architecture.png
Image embedding note:
- For markdown image syntax (
), the converter usesmediaSingleonly for Atlassian-hosted URLs. - External image URLs fall back to a clickable link to avoid Jira
INVALID_INPUTerrors. - If you want embedded images, upload the file first with
jira-attachment.ts, then use the returned AtlassiancontentUrlin your markdown image URL.
Delete Attachment
acli jira workitem attachment delete --id <attachmentId>
Example: acli jira workitem attachment delete --id 251415
Confluence
Personal Space
When the user asks to work with their personal space (e.g., "write a page in my personal space", "search my personal space", "read a page from my space"), you MUST first discover their personal space before performing the requested operation.
Auto-detection rule: Any mention of "my space", "my personal space", "personal space", or "my Confluence space" means the user's personal Confluence space. Always run the discovery script first to get the space key and ID, then use those values in subsequent operations (create, search, get, etc.).
Discover Personal Space
bunx tsx scripts/confluence-personal-space.ts
No arguments needed. Returns the current user's personal space key, ID, name, and URL.
Example output:
{
"accountId": "5b10a2844c20165700ede21g",
"displayName": "Jane Smith",
"space": {
"id": 98304,
"key": "~5b10a2844c20165700ede21g",
"name": "Jane Smith",
"type": "personal",
"status": "current",
"url": "https://yourcompany.atlassian.net/wiki/spaces/~5b10a2844c20165700ede21g"
}
}
Then use the returned space.key as the space key for other Confluence operations. For example:
- Create page:
bunx tsx scripts/confluence-create.ts '{"space": "~5b10a2844c20165700ede21g", "title": "My Notes", "body": "<p>Content</p>"}' - Search pages:
bunx tsx scripts/confluence-search.ts "space = ~5b10a2844c20165700ede21g AND type = page" - Get page by title:
bunx tsx scripts/confluence-get.ts "My Notes" ~5b10a2844c20165700ede21g
Note: Personal space keys on Confluence Cloud use the format ~accountId (not ~username). The discovery script handles this automatically.
Search Pages
bunx tsx scripts/confluence-search.ts "<CQL query>" [maxResults]
Examples:
bunx tsx scripts/confluence-search.ts "title ~ 'Roadmap'"bunx tsx scripts/confluence-search.ts "space = DEV AND type = page" 25
See docs/cql-guide.md for CQL syntax reference.
Get Page Content
bunx tsx scripts/confluence-get.ts <pageId>
# or by title
bunx tsx scripts/confluence-get.ts "<page title>" <spaceKey>
Create Page
bunx tsx scripts/confluence-create.ts '<JSON>'
Example:
bunx tsx scripts/confluence-create.ts '{"space": "DEV", "title": "New Page", "body": "<p>Content here</p>"}'
Optional parent page:
bunx tsx scripts/confluence-create.ts '{"space": "DEV", "title": "Child Page", "body": "<p>Content</p>", "parentId": "123456"}'
Update Page
bunx tsx scripts/confluence-update.ts <pageId> '<JSON updates>'
Example: bunx tsx scripts/confluence-update.ts 123456 '{"title": "Updated Title", "body": "<p>New content</p>"}'
Page Properties (v2)
# List properties
bunx tsx scripts/confluence-properties.ts <pageId> get
# Get a single property
bunx tsx scripts/confluence-properties.ts <pageId> get <propertyKey>
# Set a property
bunx tsx scripts/confluence-properties.ts <pageId> set '<JSON>'
# Delete a property
bunx tsx scripts/confluence-properties.ts <pageId> delete <propertyKey>
# Get page emoji
bunx tsx scripts/confluence-properties.ts <pageId> get-emoji
# Set page emoji (updates both emoji-title-published and emoji-title-draft)
bunx tsx scripts/confluence-properties.ts <pageId> set-emoji "🚀"
# Remove page emoji
bunx tsx scripts/confluence-properties.ts <pageId> remove-emoji
Example property payloads:
'{"key": "my-property", "value": {"foo": "bar"}}''{"properties": [{"key": "one", "value": 1}, {"key": "two", "value": "two"}]}'
Note: Page CRUD uses the Confluence REST API v2. CQL search still uses the legacy endpoint because the v2 API does not expose CQL search.
Query Language References
For generating correct queries:
- Jira: Read
docs/jql-guide.mdfor JQL syntax, fields, operators, and functions - Confluence: Read
docs/cql-guide.mdfor CQL syntax and fields
Large Content — File-Based Input
All scripts that accept a '<JSON>' argument also support @<filepath> syntax: write the JSON to a file first, then pass @path/to/file.json instead of the inline JSON string. This avoids shell argument-length limits that cause failures with large page bodies or issue descriptions.
You MUST use file-based input when creating or updating Confluence pages or Jira issues with non-trivial body/description content. Inline JSON is fine only for short payloads (simple metadata updates, status changes, etc.).
Workflow
- Write the JSON payload to a temporary file (e.g.,
/tmp/confluence-payload.json) - Pass
@/tmp/confluence-payload.jsonas the argument instead of the raw JSON string - Clean up the temp file afterward
Examples
Create a Confluence page with a large body:
# 1. Write payload to file
cat > /tmp/page.json << 'ENDJSON'
{"space": "DEV", "title": "Architecture Overview", "body": "<h1>Architecture</h1><p>Long content here...</p>"}
ENDJSON
# 2. Pass with @filepath
bunx tsx scripts/confluence-create.ts @/tmp/page.json
Update a Confluence page:
bunx tsx scripts/confluence-update.ts 123456 @/tmp/update-payload.json
Create a Jira issue with a long description:
bunx tsx scripts/jira-create.ts @/tmp/issue.json
Update a Jira issue:
bunx tsx scripts/jira-update.ts PROJ-123 @/tmp/update.json
Important: When generating content programmatically (as an AI agent), always use the Write tool to create the JSON file, then invoke the script with @filepath. Never attempt to pass large HTML or markdown content as an inline shell argument.
Common Workflows
Find and update my open issues
- Search:
bunx tsx scripts/jira-search.ts "assignee = currentUser() AND status != Done" - Update:
bunx tsx scripts/jira-update.ts PROJ-123 '{"status": "Done"}'
Create issues from a list
- Bulk create:
bunx tsx scripts/jira-create.ts '[{...}, {...}, {...}]'
Find and read documentation
- Search:
bunx tsx scripts/confluence-search.ts "title ~ 'API Documentation'" - Get content:
bunx tsx scripts/confluence-get.ts 123456
Create a new documentation page
- Create:
bunx tsx scripts/confluence-create.ts '{"space": "DEV", "title": "API Guide", "body": "<h1>API Guide</h1><p>...</p>"}'
Work with your personal space
- Discover:
bunx tsx scripts/confluence-personal-space.ts→ note thespace.keyvalue - Search:
bunx tsx scripts/confluence-search.ts "space = <space.key> AND type = page" - Create:
bunx tsx scripts/confluence-create.ts '{"space": "<space.key>", "title": "My Page", "body": "<p>Content</p>"}' - Read:
bunx tsx scripts/confluence-get.ts "<page title>" <space.key>
Link a related Jira issue
- Types:
bunx tsx scripts/jira-link.ts types→ copy the inward/outward phrase - Existing:
bunx tsx scripts/jira-link.ts list PROJ-200 - Add:
bunx tsx scripts/jira-link.ts add PROJ-200 "is blocked by" PROJ-100
Hyperlink the mention in the description as well. See Usage Guidelines.