Axon CLI

v0.2.0

Axon CLI and MCP server

axon drives an Axon project from a terminal. axon-mcp hands the same operations to an AI agent as typed MCP tools. Both ship in one package and talk to the same v1 REST API.

14 groups
52 commands
48 MCP tools
14 global options
Install
Run it without installing
bunx axonjira login
bunx axonjira task list
Or install it
bun add -g axonjira
axon login
axon task list

The package installs axon, axonjira and axon-mcp. axonjira is the same program as axon, named after the package so bunx axonjira finds it. Node 20 or newer.

Credentials

Create your own API key in the app under Settings -> API Keys - any project member can, and the dialog that shows the new key also shows the command to run. Then run axon login (or bunx axonjira login) and paste it. The key is bound to one project and GET /menames it, so login learns the project, the key's owner and its scopes and saves them as a profile in ~/.axon/config.json, owner-only. A key the API refuses is reported and nothing is written. axon logout removes the profile again; the key keeps working until it is revoked in the app.

A key acts as the person who created it, and can do only what their project role allows in the app: a viewer's key reads; a member's key also moves tasks, comments, edits their own tasks and raises tickets; an admin's or owner's key can do everything. It stops working if they leave the project or the project is deleted, and its scopes drop if their role is lowered. Scopes are ranked - read < write < admin - and every delete needs admin.

Or the environment - the key alone is enough
export AXON_API_KEY="axj_<keyId>.<secret>"
export AXON_PROJECT_ID="<projectId>"          # optional
export AXON_BASE_URL="https://axonjira.com"   # optional
Or ~/.axon/config.json by hand
{
  "defaultProject": "acme",
  "projects": {
    "acme": { "apiKey": "axj_...", "projectId": "..." }
  }
}

The key comes from $AXON_API_KEY, otherwise the config file, which is read only when the key is missing or a profile is named with --profile or $AXON_PROFILE. The profile is that name, then defaultProject, then the only one. The project id comes from --project, $AXON_PROJECT_ID or the profile, and is otherwise learned from the key with one GET /me. axon config reports what resolved and where each field came from, and never prints the key itself.

Global options

Accepted by every command.

OptionDescription
--jsonPrint the result as JSON on stdout, and nothing else.
-y, --yesConfirm a destructive command without being asked.
--no-colorTurn color off. $NO_COLOR does the same.
--profile <name>Config-file profile to take credentials from.
--project <id>Project id, overriding the configured one.
--base-url <url>API origin or root to talk to.
--limit <n>Items per page, 1 to 100.
--cursor <c>Resume from the nextCursor of an earlier page.
--allWalk every page instead of returning the first.
--timeout <ms>Per-attempt timeout. 0 disables it.
--retries <n>Retries per request on 429, 5xx and network failure.
--idempotency-key <key>Re-running with the same key replays the first result instead of writing twice. A key reused with different arguments is refused.
-h, --helpShow help for the CLI, a group, or a command.
-V, --versionPrint the CLI version.
Commands

52 commands across 14 groups. Every command also takes the global options above.

axon login

Save an API key so every other command can use it

Runs save when no command is named.

Saves an API key as a profile in ~/.axon/config.json, so every later command finds it with no environment variables set. Create the key in the app under Settings -> API Keys. The key is read from a hidden prompt at a terminal, or from standard input when one is piped in - never from an argument, which shell history and `ps` would keep. Nothing else is asked for: the key is bound to one project, and one GET /me learns which, with the key's owner and scopes. A key the API refuses is reported and nothing is written. The profile is named after the project code, lowercased, unless --profile names it. The first profile saved becomes the default; --default makes this one the default instead. Replacing a profile that holds a different key asks first at a terminal, and takes --yes otherwise. A --base-url or $AXON_BASE_URL other than the default is saved with the profile. The file is written owner-only (0600, in a 0700 directory), and every other profile and field in it is kept.

Options

--defaultMake this profile the default even when another one already is.

Examples

axon login
printf %s "$AXON_API_KEY" | axon login
axon login --profile work --default

axon logout

Remove a saved API key from this machine

Runs remove when no command is named.

Removes a profile from ~/.axon/config.json: the one --profile names, otherwise the default. Removing the default leaves the file with none. This only forgets the key on this machine. The key keeps working until it is revoked in Settings -> API Keys. Makes no requests.

Examples

axon logout
axon logout --profile work

axon whoami

Show the project and scopes this key is bound to

Runs show when no command is named.

One request, to the only endpoint that is not scoped to a project id: it resolves the project from the key itself. A configured or --project id that is not that project is refused as forbidden, as every other command does; with no project id configured, the key's project is the one used. The workflow statuses it prints are per project and are not an enum. Their ids are what `axon task move` and `axon task list --status` take, and this is the only place to read them. The key owner is who the key acts as, and who `--assignee me` names. The capabilities say which newer API features this server has, such as `ticket.withTask` for `axon ticket create --with-task`.

Examples

axon whoami
axon whoami --json

axon config

Show where this CLI takes its credentials and base URL from

Runs show when no command is named.

Reports what this process resolved, not what any one file says: a key from the environment and a project id from the config file is a normal mix, and the source beside each field is what makes that legible. Succeeds even when nothing resolves - that is the case it exists to diagnose - and says so through `configured`. The API key itself is never printed, in full or in part, only where it was found. Makes no requests.

Examples

axon config
axon config --json

axon task

Create, find and change tasks

Filters combine as AND. Ids are printed raw: turning an assignee id into a name would cost a request per listing against a 120-a-minute ceiling. `axon member list` is the map.

Options

--status <status>Workflow status, by id or by name. `axon whoami` lists them.
--assignee <userId>Assignee, as a member userId.
--sprint <sprintId>Sprint id.
--epic <epicId>Epic id.
--priority <priority>Priority.one of: critical, high, medium, low
--type <type>Task type.one of: epic, story, bug, task
--fix-version <versionId>Version id.
--label <labelId>Label document id from `axon label list`, not a label name. One per request.
--component <componentId>Component document id from `axon component list`, not a component name. One per request, and not together with --label.
--title-prefix <prefix>Case-sensitive PREFIX match on the title alone, not a search: "auth" finds "auth token expiry" but neither "Auth token expiry" nor "fix auth token expiry", and a description is never looked at. Imposes a title-ascending sort.
--due-before <date>Due before this ISO date. Tasks with no due date are excluded.
--due-after <date>Due after this ISO date. Tasks with no due date are excluded.
--order-by <order-by>Sort field. A due-date range forces dueDate.one of: createdAt, updatedAt, dueDate
--order <order>Sort direction.one of: asc, desc
--external-ref <ref>The one task created with this --external-ref, or none. An exact lookup: no other filter or sort can go with it.

Examples

axon task list --status in_progress --assignee u_123
axon task list --due-before 2026-10-01 --order asc
axon task list --external-ref swarm:app-W12

A task converted from a ticket shows it under Ticket, as its key and the id `axon ticket view` takes.

Arguments

<ref>Task key (AXO-42) or task document id.

Examples

axon task view AXO-42

`--title` is the only required flag. The task is always created at the first status of the workflow; `axon task move` takes it from there. With --component and no --assignee, the lead of the first component is assigned, as in the app.

Options

--title <text>Title.
--description <text>Description.
--priority <priority>Priority.one of: critical, high, medium, low
--type <type>Task type.one of: epic, story, bug, task
--assignee <userId>Assignee, as a member userId, or `me` for the key's owner. Pass an empty value to unassign.
--sprint <sprintId>Sprint id, from `axon sprint list`. An empty value means none.
--epic <epicId>Epic id, from `axon epic list`. An empty value means none.
--fix-version <versionId>Version id, from `axon version list`. An empty value means none.
--label <labelId>Label id, from `axon label list`, not a label name. Repeatable and comma-separated. The ids replace the task's whole set; an empty value clears it.
--component <componentId>Component id, from `axon component list`, not a component name. Repeatable and comma-separated. The ids replace the task's whole set; an empty value clears it.
--points <n>Story points: 1, 2, 3, 5, 8, 13, 21. An empty value means unestimated.
--due <date>Due date, ISO 8601. An empty value means none.
--external-ref <ref>Your own id for it, up to 200 characters, unique per project. A second create with it fails as a conflict (exit 6) naming the one that has it, so a retried create cannot file a twin.

Examples

axon task create --title "fix the build" --priority high --points 3
axon task create --title "mirror job 12" --assignee me --external-ref ci:job-12

Only the flags you type are sent, so an untouched field keeps its value and an empty value on a nullable flag clears it. There is no --status here: a transition runs the project's workflow rules and can be refused, so `axon task move` owns it and the change is always explicit. --label takes label ids and replaces the whole set, so pass every id the task should end up with and --label "" to clear them. Names are not looked up: that would be a request you did not ask for, and `axon label list` is the map. --component works the same way, with ids from `axon component list`.

Arguments

<ref>Task key (AXO-42) or task document id.

Options

--title <text>Title.
--description <text>Description.
--priority <priority>Priority.one of: critical, high, medium, low
--type <type>Task type.one of: epic, story, bug, task
--assignee <userId>Assignee, as a member userId, or `me` for the key's owner. Pass an empty value to unassign.
--sprint <sprintId>Sprint id, from `axon sprint list`. An empty value means none.
--epic <epicId>Epic id, from `axon epic list`. An empty value means none.
--fix-version <versionId>Version id, from `axon version list`. An empty value means none.
--label <labelId>Label id, from `axon label list`, not a label name. Repeatable and comma-separated. The ids replace the task's whole set; an empty value clears it.
--component <componentId>Component id, from `axon component list`, not a component name. Repeatable and comma-separated. The ids replace the task's whole set; an empty value clears it.
--points <n>Story points: 1, 2, 3, 5, 8, 13, 21. An empty value means unestimated.
--due <date>Due date, ISO 8601. An empty value means none.
--resolution <resolution>Resolution. An empty value clears it.one of: done, wont_do, duplicate, cannot_reproduce

Examples

axon task update AXO-42 --points 5 --assignee ""
axon task update AXO-42 --label lbl_1,lbl_2
axon task update AXO-42 --component cmp_1

The status is matched against the project's own workflow, by id or by name. A transition the workflow forbids comes back as a 409 naming the rules that refused it, and some transitions require a comment: pass --comment for those.

Arguments

<ref>Task key (AXO-42) or task document id.
<status>Target workflow status, by id or name.

Options

--comment <text>Comment recorded against the transition. Required by some of them.

Examples

axon task move AXO-42 "in review" --comment "ready for a look"

Takes a member userId, which `axon member list` prints.

Arguments

<ref>Task key (AXO-42) or task document id.
[userId]Member userId to assign the task to, or `me`.

Options

--unassignClear the assignee instead of setting one.

Examples

axon task assign AXO-42 u_123
axon task assign AXO-42 --unassign

Sugar for `axon comment add`, which is the same call.

Arguments

<ref>Task key (AXO-42) or task document id.
<text...>The comment body. Every remaining word is joined with spaces.

Examples

axon task comment AXO-42 deployed to staging

Needs an `admin` key. Permanent, and nothing that references the task is cleaned up: comments, activity, and any task blocked by this one are all left pointing at an id that no longer resolves.

Arguments

<ref>Task key (AXO-42) or task document id.

axon comment

Read and write the comments on a task

Oldest first. The ids printed here are what `comment edit` and `comment delete` take.

Arguments

<taskRef>Task key (AXO-42) or task document id.

Posts as the key's creator, with the author name `API`. Any key that user created can edit or delete it later.

Arguments

<taskRef>Task key (AXO-42) or task document id.
<text...>The comment body. Every remaining word is joined with spaces.

Examples

axon comment add AXO-42 deployed to staging

The whole body is replaced; there is no partial edit. A key may only edit a comment written by the key's creator. Anyone else's is a 403, an `admin` key included.

Arguments

<taskRef>Task key (AXO-42) or task document id.
<commentId>The comment id, from `axon comment list`.
<text...>The comment body. Every remaining word is joined with spaces.

A hard delete. Needs an `admin` key, and a key may only delete a comment written by the key's creator - anyone else's is a 403 however wide the key's scope.

Arguments

<taskRef>Task key (AXO-42) or task document id.
<commentId>The comment id, from `axon comment list`.

axon ticket

File and triage intake tickets

Newest first.

Options

--status <status>Ticket status.one of: open, in_progress, resolved, closed
--type <type>Ticket type.one of: bug, feature, improvement, question
--external-ref <ref>The one ticket created with this --external-ref, or none. An exact lookup: --status and --type cannot go with it.

Examples

axon ticket list --status open --type bug
axon ticket list --external-ref swarm:app-W12

Arguments

<id>The ticket document id, from `axon ticket list`.

`--type`, `--title`, `--description` and `--priority` are all required; the description may be empty but must be given. The ticket is created open and unlinked. With --with-task it is also converted into a task in the same request, so it is never seen without one: it comes back in_progress and linked, and --json prints {ticket, task}. The task takes the ticket's assignee and component, and --external-ref names both (tickets and tasks keep separate refs). --task-type, --sprint, --epic, --fix-version, --label and --points file the task, so they need --with-task.

Options

--type <type>Ticket type.one of: bug, feature, improvement, question
--title <text>Title.
--description <text>Description.
--priority <priority>Priority.one of: critical, high, medium, low
--assignee <userId>Assignee, as a member userId, or `me` for the key's owner. Pass an empty value to unassign.
--steps <text>Steps to reproduce.
--expected <text>Expected result.
--actual <text>Actual result.
--environment <text>Environment: browser, build, device.
--page-url <url>Where it happened. Must be an absolute http(s) URL.
--component <componentId>Component id, from `axon component list`.
--needed-by <date>When it is needed. Becomes the converted task's due date.
--notifyNotify the project members. Fixed at creation and not editable after.
--external-ref <ref>Your own id for it, up to 200 characters, unique per project. A second create with it fails as a conflict (exit 6) naming the one that has it, so a retried create cannot file a twin.
--with-taskConvert the new ticket into a task in the same request.
--task-type <task-type>Task type, overriding the one derived from the ticket type (bug -> bug, feature -> story, anything else -> task).one of: epic, story, bug, task
--sprint <sprintId>Sprint id, from `axon sprint list`. An empty value means none.
--epic <epicId>Epic id, from `axon epic list`. An empty value means none.
--fix-version <versionId>Version id, from `axon version list`. An empty value means none.
--label <labelId>Label id, from `axon label list`, not a label name. Repeatable and comma-separated. The ids replace the task's whole set; an empty value clears it.
--points <n>Story points: 1, 2, 3, 5, 8, 13, 21. An empty value means unestimated.

Examples

axon ticket create --type bug --title "login loops" --description "" --priority high
axon ticket create --type feature --title "export to CSV" --description "" --priority medium --assignee me --with-task --sprint spr_1 --epic epc_1 --fix-version ver_1 --external-ref swarm:app-W12

Only the flags you type are sent, so an untouched field keeps its value and an empty value on a nullable flag clears it. The status is not here: `axon ticket move` owns it, so a lifecycle change is always a deliberate one. --notify cannot be changed after creation.

Arguments

<id>The ticket document id, from `axon ticket list`.

Options

--type <type>Ticket type.one of: bug, feature, improvement, question
--title <text>Title.
--description <text>Description.
--priority <priority>Priority.one of: critical, high, medium, low
--assignee <userId>Assignee, as a member userId, or `me` for the key's owner. Pass an empty value to unassign.
--steps <text>Steps to reproduce.
--expected <text>Expected result.
--actual <text>Actual result.
--environment <text>Environment: browser, build, device.
--page-url <url>Where it happened. Must be an absolute http(s) URL.
--component <componentId>Component id, from `axon component list`.
--needed-by <date>When it is needed. Becomes the converted task's due date.
--resolution-note <text>Resolution note. Cleared by a move back to open or in_progress, whatever is sent.

The four statuses are fixed: open, in_progress, resolved, closed. They are an enum of the API's own, not the project workflow a task moves through. Moving to resolved stamps who resolved it and when; moving back to open or in_progress clears those and the resolution note with them.

Arguments

<id>The ticket document id, from `axon ticket list`.
<status>One of: open, in_progress, resolved, closed.

Examples

axon ticket move 8f2c1a resolved

Takes a member userId, which `axon member list` prints.

Arguments

<id>The ticket document id, from `axon ticket list`.
[userId]Member userId to assign the ticket to, or `me`.

Options

--unassignClear the assignee instead of setting one.

Creates a task, adds it to the ticket's linked tasks and moves the ticket to in_progress, reopening it if it was resolved or closed. A ticket can hold several tasks: converting it again adds another. Once all of them are done, the ticket resolves itself and notifies its reporter. What comes back is the new TASK, not the ticket - that is what is printed and what --json carries. The ticket is named by the id you passed, since the response does not carry its key and looking it up would be a second request. The task takes the ticket's assignee and component unless --assignee or --component name its own; with nobody assigned, the first component's lead takes it. The other flags file it, all in this one request.

Arguments

<id>The ticket document id, from `axon ticket list`.

Options

--type <type>Task type, overriding the one derived from the ticket type (bug -> bug, feature -> story, anything else -> task).one of: epic, story, bug, task
--assignee <userId>Assignee, as a member userId, or `me` for the key's owner. Defaults to the ticket's assignee; an empty value assigns nobody but a component lead.
--component <componentId>Component id, from `axon component list`. Repeatable and comma-separated. Replaces the ticket's component on the task; an empty value means none.
--sprint <sprintId>Sprint id, from `axon sprint list`. An empty value means none.
--epic <epicId>Epic id, from `axon epic list`. An empty value means none.
--fix-version <versionId>Version id, from `axon version list`. An empty value means none.
--label <labelId>Label id, from `axon label list`, not a label name. Repeatable and comma-separated. The ids replace the task's whole set; an empty value clears it.
--points <n>Story points: 1, 2, 3, 5, 8, 13, 21. An empty value means unestimated.
--external-ref <ref>Your own id for it, up to 200 characters, unique per project. A second create with it fails as a conflict (exit 6) naming the one that has it, so a retried create cannot file a twin.

Examples

axon ticket convert 8f2c1a --type story
axon ticket convert 8f2c1a --assignee me --sprint spr_1 --epic epc_1 --fix-version ver_1 --points 3

axon sprint

Plan, start and close sprints

Newest first.

Options

--status <status>Sprint status.one of: planning, active, completed

Examples

axon sprint list --status active

Arguments

<id>The sprint document id, from `axon sprint list`.

`--name`, `--start` and `--end` are required. The sprint is created at status planning; `axon sprint start` begins it.

Options

--name <text>Sprint name.
--start <date>Start date, ISO 8601.
--end <date>End date, ISO 8601. Must be after the start date.
--goal <text>Sprint goal.
--capacity <points>Capacity in story points. An empty value means none.

Examples

axon sprint create --name "Sprint 14" --start 2026-10-01 --end 2026-10-15 --capacity 34

Only the flags you type are sent, so an untouched field keeps its value. A date is checked against the merged sprint, so moving one end alone cannot leave the sprint ending before it starts. The status belongs to `start` and `complete` and cannot be set here.

Arguments

<id>The sprint document id, from `axon sprint list`.

Options

--name <text>Sprint name.
--goal <text>Sprint goal.
--start <date>Start date, ISO 8601.
--end <date>End date, ISO 8601. Must be after the start date.
--capacity <points>Capacity in story points. An empty value means none.

Needs an `admin` key, and there is no way back through this API. Starting overwrites the sprint's start date with the current time and snapshots its tasks and points as the commitment the burndown is measured against - both are written once and never again. A sprint that is not in planning, or a project that already has an active sprint, is a 409.

Arguments

<id>The sprint document id, from `axon sprint list`.

Needs an `admin` key, and there is no way back through this API. Every task in a done status is stamped as completed in this sprint, and everything else is moved out of it - an unfinished task loses its sprint and lands back in the backlog. A sprint that is not active is a 409.

Arguments

<id>The sprint document id, from `axon sprint list`.

axon epic

Work with epics

Arguments

<id>The epic id.

Creates an epic at the rollup status the API computes for an empty epic. There is deliberately no --status: an epic's status is derived from its tasks and sub-epics, and sending one is a 400.

Options

--name <text>Epic name. Required.
--description <text>Description.
--color <hex>Color as #rrggbb. The API picks one from its palette when omitted.
--start <date>Start date, ISO 8601.
--target <date>Target date, ISO 8601.
--parent <epicId>Parent epic id.

Examples

axon epic create --name "Checkout revamp" --target 2026-12-31

Sends only the flags you type, so a field you do not name keeps its value. A --parent that would make the epic its own ancestor is refused as a conflict; an id that is not an epic is a validation failure.

Arguments

<id>The epic id.

Options

--name <text>Epic name.
--description <text>Description.
--color <hex>Color as #rrggbb. The API picks one from its palette when omitted.
--start <date>Start date, ISO 8601.
--target <date>Target date, ISO 8601.
--parent <epicId>Parent epic id. Pass an empty value to re-root it.

Examples

axon epic update epc_1 --parent ''

Needs a key with the `admin` scope. The epic is emptied before it is removed: its tasks are detached and its sub-epics re-rooted. On a large epic that cascade runs in batches, so a failure part-way can leave tasks already detached from an epic that still exists. Re-run it, and check the epic before assuming nothing changed.

Arguments

<id>The epic id.

axon version

Work with versions

Arguments

<id>The version id.

The version is always created `unreleased`. Use `axon version release` once it ships.

Options

--name <text>Version name. Required.
--description <text>Description.
--start <date>Start date, ISO 8601.
--release <date>Release date, ISO 8601.

Examples

axon version create --name 1.4.0 --start 2026-10-01

Sends only the flags you type, so a field you do not name keeps its value. Moving --status to released stamps the release date with the current time unless --release is given in the same command, which is how a release that already happened gets backdated.

Arguments

<id>The version id.

Options

--name <text>Version name.
--description <text>Description.
--status <status>Version status.one of: unreleased, released, archived
--start <date>Start date, ISO 8601.
--release <date>Release date, ISO 8601.

Examples

axon version update ver_1 --status released --release 2026-09-01

Shorthand for `axon version update <id> --status released`, and the same request. The release date is stamped with the current time; to backdate it, use `update` and pass --release. A version that is already released is a conflict; to correct its date, use `update --release` alone.

Arguments

<id>The version id.

Needs a key with the `admin` scope. Every task carrying this fix version is detached from it first, then the version is removed.

Arguments

<id>The version id.

axon component

Work with components

Arguments

<id>The component id.

Options

--name <text>Component name. Required.
--description <text>Description.
--lead <userId>Lead, as a member userId - the userId column of `axon member list`.

Examples

axon component create --name Billing --lead uid_42

Sends only the flags you type, so a field you do not name keeps its value.

Arguments

<id>The component id.

Options

--name <text>Component name.
--description <text>Description.
--lead <userId>Lead, as a member userId - the userId column of `axon member list`. Pass an empty value to clear it.

Examples

axon component update cmp_1 --lead ''

Needs a key with the `admin` scope. The id is stripped from every task that carries it and cleared from every ticket that points at it first, then the component is removed.

Arguments

<id>The component id.

axon label

Work with labels

Two things here differ from the app on purpose. Names are unique within the project, compared trimmed and case-insensitively, and creating or renaming onto a name already in use is a conflict rather than a silent hand-back of the existing label. The conflict carries that label's id as `labelId`, which is the id you were after. Deleting answers with a body rather than an empty 204: the id is stripped from every task carrying it first, and the count of tasks that changed is reported.

A label id from here is what every other --label takes - `axon task list` to filter by one, `axon task create` and `axon task update` to put them on a task. A label name is not: nothing resolves one for you.

Arguments

<id>The label id.

Names are unique within the project, compared trimmed and case-insensitively. A name already in use is a conflict, not a quiet re-use of the existing label: the error carries that label's id as `labelId`, which is the id you wanted anyway. This command does not turn that into a find-or-create, because "Bug" and "bug " differ by a typo as often as by intent.

Options

--name <text>Label name. Required.
--color <hex>Color as #rrggbb. Derived from the name when omitted, as the app derives it.

Examples

axon label create --name regression --color "#c62828"

Sends only the flags you type, so a field you do not name keeps its value. Renaming onto another label's name is a conflict carrying that label's id.

Arguments

<id>The label id.

Options

--name <text>Label name.
--color <hex>Color as #rrggbb. Derived from the name when omitted, as the app derives it.

Needs a key with the `admin` scope. The id is stripped from every task carrying it first, then the label is removed. The count of tasks that changed is reported, and is the only record of how far the strip reached - those tasks cannot be given the label back, because it no longer exists to be named.

Arguments

<id>The label id.

axon member

Look up project members

`userId` is what --assignee and --lead accept, and the listing leads with it. A member's `id` under --json is its membership document id, which is the same value: members are stored under their userId. Membership is managed in the app; this API only reads it.

The USERID column is the value every other command wants: `axon task create --assignee`, `axon task list --assignee` and `axon component create --lead` all take a userId. The record's `id`, visible under --json, is the membership document id. Members are stored under their userId, so it is the same value.

Examples

axon member list --all

axon event

Read the project activity log

Activity from the app and from this API alike. The default page is 50 events; --limit takes up to 100 and --all walks the whole log. An event without a TASK is project-level rather than about one task. The per-event `metadata` is under --json.

Examples

axon event list --limit 20
Exit codes

Distinct per error class, so a script can branch without reading the message.

0success
1unexpected error
2bad usage
3missing or malformed configuration
4not found
5unauthorized or forbidden
6conflict
7validation failed
8rate limited
9network failure
10webhook loop depth exceeded
MCP server

48 tools named axon_<resource>_<verb>, over the same API.

Run it with bunx --package axonjira axon-mcp. For Claude Code, an .mcp.json beside your project:

{
  "mcpServers": {
    "axon": {
      "command": "bunx",
      "args": [
        "--package",
        "axonjira",
        "axon-mcp"
      ],
      "env": {
        "AXON_API_KEY": "axj_<keyId>.<secret>"
      }
    }
  }
}

AXON_API_KEY is the only setting it needs; leave env out and it uses the profile axon login saved instead. AXON_PROJECT_ID, AXON_BASE_URL, AXON_PROFILE and AXON_LOOP_DEPTH are optional. Set AXON_LOOP_DEPTH when the server runs inside something reacting to an Axon webhook: it is passed through as x-axon-webhook-loop-depth, so a chain of agents reacting to each other is refused by the API rather than running forever. The server fails at startup, loudly, when credentials cannot be resolved.

Describe the key this server is using and the project it is bound to. CALL THIS FIRST when you do not already know the project: it returns the project id, code and name, the key's scopes, and the project's whole vocabulary - `workflow` as `id` + `label` pairs, plus task types, priorities, ticket statuses and ticket types. Workflow statuses are defined per project and are NOT a fixed enum, so this is the only way to learn the status ids `axon_task_move` takes and `axon_task_list` filters on; `isDone` on a status marks the ones that close work. The scopes say what the key may do at all: `read` to look, `write` to create and change, `admin` for every delete. A missing scope is a refusal no retry can fix. Takes no arguments and writes nothing.

One page of tasks. Filters combine as AND, and paging is explicit: follow `nextCursor` to read more. Ids come back raw - `axon_member_list` maps an assignee id to a person and `axon_sprint_list` a sprint id to a sprint. Not every filter combination is indexed; one that is not is refused with a 400 asking for fewer filters or the default sort, not a 500.

limit?
cursor?
status?
assigneeId?
sprintId?
epicId?
priority?
taskType?
versionId?
labelId?
componentId?
titlePrefix?
dueBefore?
dueAfter?
orderBy?
order?
externalRef?

One task in full, by key or by document id. A key has an endpoint of its own, so either form is a single request. The task carries fields this API cannot write - subtasks, attachments, blockers, time tracking - because it is serialized straight from the stored document. A task converted from a ticket carries `ticketId` and `ticketKey`; `axon_ticket_get` takes the `ticketId`.

taskIdOrKey

Creates a task. Only `title` is required. The task is always created at the workflow's first status; `axon_task_move` takes it from there. Counts against a limit of 30 task creations a minute per reporter. With `componentIds` and no `assigneeId`, the task goes to the lead of the first component, as it does in the app.

title
description?
priority?
taskType?
assigneeId?
sprintId?
epicId?
versionId?
labelIds?
componentIds?
storyPoints?
dueDate?
externalRef?
idempotencyKey?

Changes a task's fields. Only the fields you send are written; anything omitted keeps its value, and a nullable field set to null is cleared. There is deliberately no `status` here. The API writes the other fields first and applies a transition afterwards, so one request carrying both can land the edits and still fail the move with a 409; `axon_task_move` owns the status instead, and nothing here can leave a half-applied update behind. `labelIds` is the field that puts labels on a task, and it is the one the app renders and `axon_task_list` filters by. The older `labels` is refused with a 400, so do not reach for it: they are different fields, not two spellings of one.

taskIdOrKey
title?
description?
priority?
taskType?
assigneeId?
sprintId?
epicId?
versionId?
labelIds?
componentIds?
storyPoints?
resolution?
dueDate?
idempotencyKey?

Moves a task to another workflow status, running the project's own transition rules. This is the only way to change a task's status. A rule that refuses the move is a 409 whose `details.errors` names every validator that failed - read it and fix the cause (assign the task, close the blocker, add the comment) rather than retrying the same call. Some transitions require a comment and refuse the move without one. A resolution is not set here: use `axon_task_update`, which can write one before or after the move.

taskIdOrKey
status
comment?
idempotencyKey?

Assigns a task to a project member, or unassigns it. `assigneeId` is required rather than optional, so unassigning is an explicit `assigneeId: null` and can never happen by omission.

taskIdOrKey
assigneeId
idempotencyKey?

Deletes a task for good. Needs an `admin` key. Nothing that references the task is cleaned up: its comments, its activity and any task blocked by it are all left pointing at an id that no longer resolves. The endpoint is addressed by document id, so a key is resolved to one first - that costs an extra request.

taskIdOrKey
confirm
idempotencyKey?

One page of the comments on a task, oldest first. The `id` of each is what `axon_comment_update` and `axon_comment_delete` take, and `authorId` is what the API compares against this key's own identity when deciding whether a comment may be edited at all.

taskIdOrKey
limit?
cursor?

Adds a comment to a task. It posts as the key's creator with the author name `API`, and any key that user created can edit or delete it afterwards.

taskIdOrKey
content
idempotencyKey?

Replaces the text of a comment. The whole body is replaced; there is no partial edit. A key may only edit a comment written by the key's creator - anyone else's is a 403, an `admin` key included, and retrying will not help.

taskIdOrKey
commentId
content
idempotencyKey?

Deletes a comment for good. Needs an `admin` key. A key may only delete a comment written by the key's creator - anyone else's is a 403 however wide the key's scope. Unlike the other deletes in this API this one answers with a record rather than an empty response, so the deleted id comes back.

taskIdOrKey
commentId
confirm
idempotencyKey?

One page of tickets, newest first. This is also how a ticket id is found: tickets are addressed by document id everywhere else in this API, and the `id` field here is that id.

limit?
cursor?
status?
ticketType?
externalRef?

One ticket in full, by document id.

ticketId

Files a ticket. `ticketType`, `title`, `description` and `priority` are all required - the description may be an empty string, but it has to be sent. The ticket is created open and unlinked. It is an intake record, not work: `axon_ticket_convert` turns it into one or more tasks when someone decides to do it. Pass `task` to do both at once: the ticket is filed AND converted in one request, so it is never seen without its task, and `data` is `{ ticket, task }` instead of the ticket alone. `task` takes what `axon_ticket_convert` takes; send `{}` to take every default.

ticketType
title
description
priority
assigneeId?
stepsToReproduce?
expectedResult?
actualResult?
environment?
pageUrl?
componentId?
neededBy?
notifyMembers?
externalRef?
task?
idempotencyKey?

Changes a ticket's fields. Only the fields you send are written; anything omitted keeps its value, and a nullable field set to null is cleared. The status is not here: `axon_ticket_move` owns it, so a lifecycle change is always deliberate. `notifyMembers` cannot be changed after creation.

ticketId
ticketType?
title?
description?
priority?
assigneeId?
stepsToReproduce?
expectedResult?
actualResult?
environment?
pageUrl?
componentId?
neededBy?
resolutionNote?
idempotencyKey?

Moves a ticket through its own lifecycle. The four statuses are a fixed enum of the API's, not the workflow statuses a task moves through, and no project rules are run. Moving to `resolved` stamps who resolved it and when; moving back to `open` or `in_progress` clears those and the resolution note with them.

ticketId
status
idempotencyKey?

Assigns a ticket to a project member, or unassigns it. `assigneeId` is required rather than optional, so unassigning is an explicit `assigneeId: null` and can never happen by omission. Assigning a ticket does not assign the task a conversion creates.

ticketId
assigneeId
idempotencyKey?

Turns a ticket into a task: creates the task, appends it to the ticket's `linkedTaskIds`, and moves the ticket to `in_progress` (reopening it if it was resolved or closed). A ticket can hold several tasks: calling this again on the same ticket adds another task. Once every linked task is done, the ticket resolves itself and notifies its reporter - there is no need to resolve it by hand. WHAT COMES BACK IS THE NEW TASK, NOT THE TICKET. Read `data.task` for the task that was created - its key and id are what everything else in this API takes. The ticket itself is unchanged in the response because it is not in the response; read it back with `axon_ticket_get` if you need it. The task is filed in the same call: its assignee, sprint, epic, version, components, labels and points are all fields here, so no `axon_task_update` is needed afterwards. The new task counts against the limit of 30 task creations a minute per reporter.

ticketId
taskType?
assigneeId?
sprintId?
epicId?
versionId?
componentIds?
labelIds?
storyPoints?
externalRef?
idempotencyKey?

One page of sprints, newest first. This is where a sprint id comes from - the `sprintId` every task tool takes is the `id` field here.

limit?
cursor?
status?

One sprint in full, by document id. `committedTaskIds` and `committedStoryPoints` are absent until the sprint is started: they are the snapshot a burndown is measured against, written once by `axon_sprint_start`.

sprintId

Creates a sprint. `name`, `startDate` and `endDate` are required. The sprint is created at status `planning` and holds no tasks: put tasks in it with `axon_task_update` (`sprintId`), then begin it with `axon_sprint_start`, which is a separate and irreversible call.

name
startDate
endDate
goal?
capacityPoints?
idempotencyKey?

Changes a sprint's fields. Only the fields you send are written. A date is validated against the merged sprint, so moving one end alone cannot leave the sprint ending before it starts. There is no `status` field: the API rejects one outright. A sprint's status is changed only by `axon_sprint_start` and `axon_sprint_complete`.

sprintId
name?
goal?
startDate?
endDate?
capacityPoints?
idempotencyKey?

Starts a sprint. Needs an `admin` key, and there is no way back through this API. Starting OVERWRITES the sprint's start date with the current time, and snapshots the tasks in it and their points as the commitment every burndown is measured against - both are written once and never again, so a sprint started before its scope is ready has the wrong commitment for good. A sprint that is not in `planning`, or a project that already has an active sprint, is a 409.

sprintId
confirm
idempotencyKey?

Completes a sprint. Needs an `admin` key, and there is no way back through this API. Every task in a done status is stamped as completed in this sprint; EVERYTHING ELSE IS MOVED OUT OF THE SPRINT - an unfinished task loses its sprint and lands back in the backlog. Check what is still open with `axon_task_list` before calling this. A sprint that is not `active` is a 409.

sprintId
confirm
idempotencyKey?

List the project's epics, newest first, one page at a time. An epic id from here is what a task's `epicId` takes, and `parentEpicId` is null on a top-level epic. An epic's `status` is a read-only rollup recomputed from its tasks and sub-epics, not a field anything can set.

limit?
cursor?

Read one epic by id. An epic's `status` is a read-only rollup recomputed from its tasks and sub-epics, not a field anything can set.

epicId

Create an epic. There is deliberately no `status` argument: an epic's status is derived from its tasks and sub-epics, and sending one is rejected. Creating an epic attaches nothing to it - point a task at it afterwards with `axon_task_update`.

name
description?
color?
startDate?
targetDate?
parentEpicId?
idempotencyKey?

Change an epic. Only the fields you send are written, so anything you omit keeps its value. There is no `status` argument here either, for the same reason as on create.

epicId
name?
description?
color?
startDate?
targetDate?
parentEpicId?
idempotencyKey?

Delete an epic. Needs a key with the `admin` scope, and there is no undo. The epic is emptied before it goes: its tasks are detached and its sub-epics re-rooted to the top level. On a large epic that cascade runs in several batches and is NOT transactional, so a failure part-way can leave tasks already detached from an epic that still exists. If this fails, re-read the epic and its tasks rather than assuming nothing changed.

epicId
confirm
idempotencyKey?

List the project's versions, newest first, one page at a time. A version id from here is what a task's `versionId` (its fix version) takes.

limit?
cursor?

Read one version by id, including its status and release date.

versionId

Create a version. It is always created `unreleased`, whatever dates you give it; ship it later with `axon_version_release`.

name
description?
startDate?
releaseDate?
idempotencyKey?

Change a version. Only the fields you send are written, so anything you omit keeps its value. This is also the way to release one: send `status: "released"`, with a `releaseDate` if the release already happened and should be backdated.

versionId
name?
description?
status?
startDate?
releaseDate?
idempotencyKey?

Mark a version released. Exactly the request `axon_version_update` makes with `status: "released"` and nothing else, so the release date is stamped with the current time; to record a release that already happened, use `axon_version_update` and pass `releaseDate` as well. Releasing a version that is already released is a 409 conflict that changes nothing; to correct its date, send `releaseDate` alone through `axon_version_update`.

versionId
idempotencyKey?

Delete a version. Needs a key with the `admin` scope, and there is no undo. Every task carrying this fix version is detached from it first, then the version goes - so tasks survive, but their `versionId` is cleared and there is no record of which ones it was.

versionId
confirm
idempotencyKey?

List the project's components, newest first, one page at a time. A component id from here is what a task's `componentIds` and a ticket's `componentId` hold; `leadId` is a member `userId`.

limit?
cursor?

Read one component by id. Its `leadId` is a member `userId`; resolve it to a person with `axon_member_list`.

componentId

Create a component. Creating one attaches nothing to it; tasks and tickets point at a component of their own accord.

name
description?
leadId?
idempotencyKey?

Change a component. Only the fields you send are written, so anything you omit keeps its value.

componentId
name?
description?
leadId?
idempotencyKey?

Delete a component. Needs a key with the `admin` scope, and there is no undo. The id is stripped from every task that carries it and cleared from every ticket that points at it first, then the component goes - so the work survives, but which items belonged to the component is not recorded anywhere afterwards.

componentId
confirm
idempotencyKey?

List the project's labels, newest first, one page at a time. A label's `id`, not its `name`, is what `axon_task_list` filters on, so resolving a name to an id starts here. Putting a label on a task happens elsewhere: send the id in the `labelIds` of `axon_task_create` or `axon_task_update`, which replaces that task's whole set.

limit?
cursor?

Read one label by id. `nameNormalised` is the trimmed, lowercased form the uniqueness check compares, which is why "Bug" and "bug " collide.

labelId

Create a label. Names are unique within the project, compared trimmed and case-insensitively. A name already in use is a CONFLICT rather than a quiet hand-back of the existing label, and the conflict carries that label's id in `details.labelId` - which is the id you were after, so a conflict here is recoverable rather than fatal: read the id out of the error and use it instead of retrying. This is deliberately not a find-or-create, because "Bug" and "bug " differ by a typo as often as by intent. Putting a label on a task happens elsewhere: send the id in the `labelIds` of `axon_task_create` or `axon_task_update`, which replaces that task's whole set.

name
color?
idempotencyKey?

Change a label. Only the fields you send are written, so anything you omit keeps its value. Renaming onto another label's name is a conflict carrying that label's id in `details.labelId`; the two labels are not merged.

labelId
name?
color?
idempotencyKey?

Delete a label. Needs a key with the `admin` scope, and there is no undo. The id is stripped from every task's `labelIds` first, then the label goes. Unlike the other deletes this answers with a body rather than nothing: `{ id, deleted, tasksUpdated }`, where `tasksUpdated` counts the tasks that lost the label and is the only record of how far the strip reached. Putting a label on a task happens elsewhere: send the id in the `labelIds` of `axon_task_create` or `axon_task_update`, which replaces that task's whole set. Those tasks cannot be given it back afterwards: the label is gone, and `labelIds` refuses an id the project no longer has.

labelId
confirm
idempotencyKey?

List the project's members, most recently joined first, one page at a time. Use it to turn a name or an email into the id other tools want. `userId` is the value `assigneeId` on a task and `leadId` on a component take; a member's `id` is its membership document id, which is the same string because members are stored under their userId. Membership is managed in the Axon app; this API only reads it, so there is no tool here to add, change or remove a member.

limit?
cursor?

Read the project's activity log, newest first, one page at a time. It covers what people do in the Axon app as well as what API keys do, so it is the way to see whether a change came from a teammate or from an agent: `actorName` is the literal `API` for anything this API recorded and a person's name otherwise. An event with no `taskId` is project-level rather than about one task, and the per-event `metadata` carries whatever that event type records.

limit?
cursor?

Everything on this page is generated from the CLI's own command and tool declarations, so it cannot drift from what axon --help prints.

REST API reference