# CLI reference

Use this page for command behavior, shared input rules, and practical examples. For JSON contracts, discovery, and unattended commands, see [Scripting and JSON output](/docs/scripting). For database settings and upgrades, see [Settings](/docs/settings) and [Data and backups](/docs/data).

## Contents

- [Shared conventions](#editing-conventions)
  - [Text values](#text-values)
  - [Words and repeated flags](#words-and-repeated-flags)
  - [Changing tasks in bulk](#changing-tasks-in-bulk)
- [Projects and columns](#start-a-project)
- [Tasks](#create-and-read-tasks)
  - [Next ready task](#next-ready-task)
  - [Task blame](#task-blame)
  - [Search tasks](#search-tasks)
  - [Edit tasks](#edit-tasks)
  - [Move and order tasks](#move-and-order-tasks)
  - [Archive and delete tasks](#archive-and-delete-tasks)
  - [Task priority](#task-priority)
  - [Task deadlines](#task-deadlines)
- [Labels](#labels)
- [Task relations](#task-relations)
- [Task comments](#task-comments)
- [Activity](#activity)
  - [Who am I?](#who-am-i)
- [Agent skill](#agent-skill)
- [Command aliases](#command-aliases)
- [Help](#help)

## Editing conventions

Edit commands change only the fields you supply, whereas omitted fields keep their current values. At least one edit field is required. After input normalization, a valid edit that leaves all values unchanged succeeds without writing changes, updating modification timestamps, or recording activity.

Move, order, archive, unarchive, and task-label attachment commands also succeed without changes when the requested state is already satisfied. This does not apply to creation: creating an existing relation, for example, is an error.

Edit, move, order, archive, unarchive and task-label attachment commands include `changed` in their `--json` results. It is `false` when the requested state was already satisfied, and a note is printed on stderr. A real change sets `changed` to `true` and prints no note. For label attachments, changing any of the requested labels counts as a change.

Names and titles are trimmed and cannot be blank or contain line breaks or control characters, such as tabs.

Project, task, and label descriptions may contain multiple lines. Omitted descriptions on creation, empty strings, and whitespace-only strings are stored as `null`. Note that non-blank descriptions and comment text normalize CRLF line endings to LF, preserving all other whitespace, including lone carriage returns. Use `--description ''` on an edit to clear a description. JSON returns the full text or `null`.

To change part of a description, or of a comment's text, `--append` and `--replacing` modify how `--description` (or `--text` on `task comment edit`) is applied to the stored text:

```sh title="Edit descriptions"
zinn task edit MDR-1 --description 'Bring waffles.' --append
zinn task edit MDR-1 --description 'new sentence' --replacing 'old sentence'
zinn project edit MDR --description database --replacing board --all
```

Flags:

- `--append` adds the text as a new paragraph,
- `--replacing <old>` replaces the one place where `<old>` appears, matching exactly and respecting case.
- `--all` replaces every match.

A command with no match, or with several matches and no `--all`, fails without changing anything.
`--description ''` (or `--text ''`) with `--replacing` deletes the match. `--append` and `--replacing` cannot be combined.

Append text and both the old and new text in a replacement use the same CRLF-to-LF normalization.

Descriptions and comment text have a limit of 1 MiB (1,048,576 UTF-8 bytes). Full text is checked **before** normalization, and partial edits check the resulting text. Exceeding the limit fails without writing changes.

## Text values

A value that begins with a dash, such as a Markdown bullet, must be joined to its flag with `=`. Otherwise the command fails without changing anything:

```sh title="Edit a description with a leading dash"
zinn task edit MDR-1 --description='- Bring waffles.' --append
```

For multi-line text, such as Markdown, read it from a file or from stdin instead. `--description-file <path>` on `project`, `task` and `label` `create` and `edit`, and `--text-file <path>` on `task comment create` and `edit`, take the text from a file. A path of `-` reads stdin. With a heredoc and a quoted delimiter (`'EOF'`) in bash or zsh, the shell passes `$`, backticks, quotes and leading dashes as written:

```sh title="Edit multi-line text"
zinn task edit MDR-1 --description-file - <<'EOF'
- Bring waffles.
- Count the `numbers` for $5 and "Kier".
EOF
zinn task comment create MDR-1 --text-file notes.md
```

- A command takes one `-`, and refuses it when stdin is a terminal.
- The path must be a regular file or `-`. A directory or another special file, such as a named pipe, is refused before anything is read.
- The text must be UTF-8. A leading byte order mark is dropped; other encodings, such as UTF-16, and text containing a NUL character fail.
- Trailing line breaks are dropped, as `$(cat file)` does, so the stored text ends with the last character of the last line.
- Empty or blank input fails. To clear a description, use `--description ''`.
- The 1 MiB limit applies to the bytes read, so larger input fails before it is stored.
- The inline flag and its file flag cannot be combined. `--append` and `--replacing` work with either; the `--replacing <old>` text stays inline.
- A failed read changes nothing.

On Windows PowerShell 5.1, pass a file rather than a pipe: piping to a program converts non-ASCII text to `?` before Zinn receives it.

## Words and repeated flags

`--` ends the options. Every word after the first `--` is considered a plain word, even a secondary instance of `--` or anything else that starts with a dash. Flags after it, such as `--json`, are not read. A trailing `--` changes nothing:

```sh title="End option parsing with double dash"
zinn project column create MDR -- -later
zinn task search -- -flag
```

Parser and routing error messages are capped at 1,024 UTF-16 units. Short repair commands preserve the original values. Longer repairs give guidance and mark the full command as omitted, so apply the correction to your original input.

A flag can be given once.
Only these flags repeat:

- `--label` on `task create`, `task list` and `task search`,
- `--depends-on` and `--related` on `task create`,
- `--add-label` and `--remove-label` on `task edit`,
- `--action` and `--changed` on `activity list`.

Any other flag given twice, `--json` and `--yes` included, **fails** without changing anything.
A help flag is the _exception_: the first one shows help and the rest of the line is ignored, so `-h --help` works and help is given whenever needed! No problem!

## Changing tasks in bulk

`task move`, `task order`, `task archive`, `task unarchive` and `task delete` accept multiple task keys, so several tasks can be changed in one command:

- Tasks are changed in the order given.
- If any task fails, for example because of an unknown key, none change.
- A task already in the requested state is unchanged, with a note on stderr. Move, archive and unarchive also accept a repeated key. Order and delete require distinct keys, ignoring case.
- With `--json`, one task prints an object and several print an array. Move, order, archive and unarchive include a `changed` field, which is `false` when the task was already in the requested state. Delete results omit `changed`.
- Delete resolves every key before asking for one confirmation naming all selected tasks. An unknown or repeated key refuses the whole command before confirmation.

## Start a project

Create a project with a key and optionally provide a name and description:

```sh title="Create a project"
zinn project create MDR --name 'Macrodata Refinement' --description 'Refine macrodata'
```

Project keys are stored in uppercase. An omitted name defaults to the key.

Descriptions print in full with `project view`. See [Editing conventions](#editing-conventions) for name validation and description storage and clearing, and [Text values](#text-values) for `--description-file`.

Each project starts with _Backlog_, _TODO_, _Doing_, _Review_, _Done_ and _Cancelled_ columns.

Every column has a category, which says what its tasks are to Zinn whatever the column is called or where it sits: `backlog`, `todo`, `in_progress`, `review`, `done` or `cancelled`. The default columns take the category of the same meaning. Tasks in `done` and `cancelled` columns are closed: they no longer hold back tasks that depend on them. [`task next`](#next-ready-task) hands out tasks from `todo` columns, then `backlog` columns. Renaming or moving a column keeps its category.

You can list projects or inspect a project's columns with:

```sh title="List projects and columns"
zinn project list
zinn project view MDR
zinn project column list MDR
```

`project list` shows each project's active and archived task counts, including zeros. Active means unarchived, in any column, including Done. Parents and subtasks each count once in their owning project.

`project view` shows the project's description and task counts. Use `zinn task list <project-key>` to read its tasks.

`project view` and `project column list` accept an omitted project key when [defaultProject](/docs/settings) is configured.

`project column list` counts unarchived tasks. With `--json`, it returns `[{ "id": "<column-id>", "name": "Backlog", "is_first": true, "is_last": false, "category": "backlog", "taskCount": 3 }, ...]`.

Edit a project:

```sh title="Edit a project"
zinn project edit MDR --name 'Macrodata Refinement (Morning Shift)'
zinn project edit MDR --description 'Meet the quarterly quota'
zinn project edit MDR --description ''
```

Provide `--name`, a description (`--description` or `--description-file`), or both.
The project key can be omitted when [defaultProject](/docs/settings) is configured.
See [Text values](#text-values) for values that begin with a dash and multi-line text.

Add another column at the end of the board, with its category:

```sh title="Add a column"
zinn project column create MDR Boardlog --category backlog
```

`--category` is required. Without it, or with an unknown value, the command fails and lists the valid values.

Rename a column or change its category:

```sh title="Edit a column"
zinn project column edit MDR Doing --name Refining
zinn project column edit MDR Shipped --category done --yes
```

Provide `--name`, `--category` or both. Column names follow the shared [name and title rules](#editing-conventions) and match case-insensitively. Names must be unique within the project. Changing only the casing is allowed. Renaming keeps tasks in the column.

Changing the category of a column that holds tasks, active or archived, asks for confirmation, since it changes which of them count as closed. Use `--yes` to skip the prompt, which `--json` and runs without a terminal require. An empty column changes without a prompt.

Change a column's position on the board:

```sh title="Order columns"
zinn project column order MDR Review first
zinn project column order MDR Review previous
zinn project column order MDR Review next
zinn project column order MDR Review last
zinn project column order MDR Review before Refining
zinn project column order MDR Review after Done
```

Directions follow board order: `first` is the start of the board and `last` is its end.
Moving a column to the position it already has, or before or after itself, does nothing. Tasks stay in their columns.

Delete an empty column:

```sh title="Delete a column"
zinn project column delete MDR Review
```

Deletion asks for confirmation. Use `--yes` to skip the prompt, including with `--json`. The column must hold no active or archived tasks. Use `task move` or `task delete` to clear tasks from the column. Archived tasks must be unarchived before moving.

New tasks enter the first column unless `--column` flag specifies a different column. A project may have no columns. Add one with `project column create` before creating tasks.

`zinn project delete MDR` asks for confirmation, then permanently deletes the project and all of its columns and tasks. `--yes` skips confirmation when the project key is **typed**. With an _omitted_ key and [defaultProject](/docs/settings), it always asks: `--yes` is ignored with a warning, and without a terminal the command refuses with exit code 1. With `--json`, type the key and pass `--yes` (see [Confirmations](/docs/scripting#confirmations-in-scripts)). Deleting a project releases its key for a future project. See [Task key reuse](/docs/data#task-key-reuse).

## Create and read tasks

Create a task with a title and an optional description. Titles follow the shared [name and title rules](#editing-conventions). New tasks enter the project's first column:

```sh title="Create tasks"
zinn task create MDR --title 'Refine 75% of the numbers'
zinn task create MDR --title 'Review the employee handbook' --description 'Prepare for a 75% Dance Experience'
```

The project key can be omitted when [defaultProject](/docs/settings) is configured. `--description-file` reads the description from a file or stdin (see [Text values](#text-values)).

`--column <name>` starts the task at the bottom of a specific column. The column name matching is case insensitive:

```sh title="Create a task in a column"
zinn task create MDR --title 'Compile the Lexington letters' --column Doing
```

Tasks are created with keys such as `MDR-1`. Task numbers increase in creation order within each project. List active tasks, optionally limited to one project, or view one task:

```sh title="List and view tasks"
zinn task list
zinn task list MDR
zinn task list --all-projects
zinn task list MDR --label CLI --label Groom
zinn task list MDR --limit 10
zinn task list MDR --sort activity --limit 10
zinn task list MDR --due-since 2026-10-25T00:00:00Z --sort due
zinn task list MDR --column Doing
zinn task list MDR --state open
zinn task list --column Done --all
zinn task view MDR-1
zinn task view MDR-1 --history
```

`task list --state open` leaves out tasks in done and cancelled columns, and `--state closed` lists only those. `--state all` is the default. A column's category decides its state, not its name or position. `--state` combines with every other filter, including `--archived`, `--all` and `--column`.

`task list`, `task search` and `activity list` accept `--limit <n>`, a decimal integer from `1` through `9007199254740991`. The cap applies after all filters and preserves the existing order. Omit it to return every match. With `--json`, each command returns an array.

`task list --starting-after <cursor>` continues after the last listed task in the selected sort order. The cursor is an opaque token. Copy it from the continuation command that `task list` prints on stderr when more tasks remain. A task ID is refused.

Continuation survives edits, moves, archiving, relabelling, deadline changes and deletion of the last listed task. Filters may change while `--sort` stays the same. A cursor made for another sort or another version is refused with a hint and a suggested `task list` command. No snapshot is kept between commands, so other tasks changing in between can cause repeats or skips. Ties in the order break by task ID.

When more matching tasks remain after `--limit`, stderr gives a shell-quoted continuation command in text and JSON modes.

`task list --sort activity` lists tasks by their newest recorded activity, including task changes, comments and relations. Tasks without recorded activity use their update time. Newest comes first, and equal times keep board order. Filters apply before sorting, and `--limit` caps the sorted result. Without `--sort`, board order is preserved. Sorting changes only the order in text and JSON.

`task list --sort due` lists deadlines from earliest to latest. Tasks without a deadline come last. Equal deadlines and undated tasks keep board order. Sorting does not change task positions. See [Task deadlines](#task-deadlines) for filters and input rules.

Task lists are previews, so they _omit_ descriptions, but `task view` prints the full description.

`task view` includes the task's creation and update times in local time with a UTC offset. Full task JSON objects include `created_at` and `updated_at` as stored integer epoch milliseconds across list, search, view, creation, updates and deletion, including bulk results. Edits, label attachment changes, moves, reorders, archiving and unarchiving advance the update time when they change the task. Comments, relations, column renames, global label-definition edits and operations that lead to no changes leave it untouched.

An omitted project key uses [defaultProject](/docs/settings), or every project when none is configured. `--all-projects` overrides the default.

`task view --history` includes the inspected task's recorded activity, newest first by time, then event ID. Reorders and comment changes are included. Use `activity view <activity-id>` for full recorded changes. See [Activity](#activity) for timestamps, event IDs, and record fields.

With `--history --json`, the task gains an `activity` array of full activity records. It returns `[]` when no events are recorded. Without `--history`, this field is _omitted_.

If a project is deleted and recreated with the same key, `task view --history` shows only the _current_ task's history. `activity list --task <key> --show-deleted` also includes history for **deleted tasks** that previously had that same key.

Repeat `--label <name>` to require every specified label. Matching ignores case.

Use `--column <name>` to filter by an exact column name, ignoring case, within one project or across projects. Column, label, and archive filters apply together. No matches is a successful empty result (`[]` with `--json`).

## Next ready task

`task next` shows the one task to work on next:

```sh title="Show the next ready task"
zinn task next MDR
zinn task next MDR --column Review
```

A task is ready when it is active, in a **todo or backlog** column, and every task it [depends on](#task-relations) is archived or in a done or cancelled column. Only dependency relations hold a task back. Tasks in a dependency cycle are never ready.

Ready tasks in todo columns come before those in backlog columns, so a low-priority todo task comes before an urgent backlog task. Within each, tasks are ordered by priority, urgent first and unset last. Equal priorities go by the earliest deadline, with undated tasks last, and then by the oldest task. Deadlines only order tasks.

An omitted project key uses [defaultProject](/docs/settings), or every project when none is configured. `--all-projects` overrides the default. `--column <name>` takes ready tasks from one column instead, with the same matching as `task list --column`. It can name a column of any category but done and cancelled, such as Review.

When no task is ready, the command still exits `0`. With `--json`, it prints the task object, or `null` when no task is ready.

Two callers can receive the same task. To take it, move it with [`task move --from`](#move-and-order-tasks), which fails with a `conflict` error if another caller moved it first. Then ask `task next` again.

## Task blame

```sh title="Find the recorded authors of current task content"
zinn task blame MDR-1
zinn task blame MDR-1 --json
```

`task blame <task-key>` attributes the current title, column, priority, deadline, label membership, parent, relations and description lines to recorded activity. It reads active and archived tasks. A [reused key](/docs/data#task-key-reuse) selects its current task lifetime. Unknown and deleted keys produce the ordinary missing-task error. For a worked example of the output, see [Task blame](/docs/blame).

Creation records can supply the initial attribution, including unset values. Later edits, moves and membership changes replace it. Labels are one collection, attributed to the latest attachment or removal, including removal when a label is deleted, when the recorded creation and changes add up to the current number of labels. Column and label renames preserve membership attribution while returning the current names. Parent assignment and removal follow outgoing `subtask` relation events. Each current relation carries its own creation attribution, including incoming links and the parent link.

Description comparison matches whole lines exactly, including whitespace. Unchanged lines carry their attribution, inserted or replaced lines belong to the introducing edit, and deleted lines disappear. Blank lines are preserved. A terminal newline ends the preceding line without adding another line, and changing only that newline preserves attribution. Null and empty descriptions have no lines. Matching keeps the shared lines at the start and the end in place, then matches the longest sequence of remaining lines that both versions share in the same order. When a comparison could be read more than one way, it treats a line as removed before treating it as added. Each edit has a fixed search budget, enough for about 2,500 replaced lines. Past it, lines not yet matched belong to that edit, so a heavily reordered description reads as rewritten.

Attribution is `null` when the required history is absent or unrecognized. Text calls it `unrecorded`. A scalar's latest recorded new value must agree with its current value. Description edits must join the previous recorded version before they can carry its attribution. A gap starts with unattributed old lines and attributes only inserted lines to the next edit. A later description record without both the old and new text discards the carried line attribution, so later edits cannot inherit it. If the final recorded description differs from the current text, all current lines are unattributed.

Recorded events retain the stored actor name, kind and identity fields, with `null` for absent metadata. Missing actor identity is separate from missing event attribution. Event IDs open with `zinn activity view <activity-id>`. Blame reads task content and history without recording activity.

With `--json`, the command returns the [`TaskBlame` result](/docs/core#task-blame-result).

## Search tasks

Search task titles, descriptions, and comments, optionally in one project:

```sh title="Search tasks"
zinn task search refine
zinn task search refine --all-projects
zinn task search 'quarterly quota' MDR
zinn task search MDR-1
zinn task search auth --all --limit 5
zinn task search auth MDR --label cli
zinn task search auth MDR --column Review
zinn task search refinement --due-until 2026-10-25T17:00:00+02:00
```

Each query matches the start of a word, ignoring case and accents, so `cafe` finds "Café" and `auth` finds "Authentication". Every word must match somewhere in the task: its title, its description, or any of its comments. A comment match lists the task the comment belongs to.

The query comes first, then the optional project key. An omitted key uses [defaultProject](/docs/settings), or every project when none is configured. `--all-projects` overrides the default.

If the project key does not exist, the error includes the search usage line to clarify the argument order. With `--json`, this guidance is in `error.help`.

If the whole query is an existing task key, such as `mdr-1`, that task comes first. Other results follow by relevance, with title matches weighted highest. Equally relevant tasks are ordered by project key, then task number.

An exact task-key query overrides the configured default project. A project key supplied after the query still filters the search.

Search defaults to active tasks. `--archived` searches archived tasks and `--all` searches both. A note on stderr reports hidden archived matches. `--limit <n>` caps the results.

`--label` works as it does for `task list`: repeat it to require every specified label. `--column` matches an exact column name, ignoring case, in the selected project or across projects. Label and column filtering happen before `--limit` and before archived matches are counted.

With `--json`, search returns an array of tasks like `task list` does. A search with no results succeeds. A query that begins with a dash goes after `--` (see [Words and repeated flags](#words-and-repeated-flags)).

Search uses an index that Zinn keeps up to date as tasks and comments change. `zinn reindex` rebuilds it from the tasks and their comments, archived tasks included:

```sh title="Rebuild the task search index"
zinn reindex
```

Only the index changes, completely or not at all, so no backup is saved. With `--json`, it prints `{ "databasePath": "...", "taskCount": 12 }`. The command fails without changing anything when there is no database yet, or when Zinn refuses changes to the database, such as one with a newer schema version (see [Upgrade the database](/docs/data#upgrade-a-database)).

## Labels

Labels are global and reusable across projects.

```sh title="Create and edit labels"
zinn label create urgent --color red --description 'Needs attention soon'
zinn label create review
zinn label edit review --color green
zinn label list
zinn label view urgent
```

### Adding to tasks

```sh title="Add and remove task labels"
zinn task create MDR --title 'Check the numbers' --label urgent --label review
zinn task label add MDR-1 urgent review
zinn task label remove MDR-1 review
zinn task edit MDR-1 --add-label review --remove-label urgent
```

Repeat `--label`, `--add-label`, or `--remove-label` for several labels. Each value is one name, so commas are part of the name. Quote names containing spaces: `--label 'needs review'`.

Label names follow the shared [name and title rules](#editing-conventions), preserve their stored casing, and match case-insensitively. Changing only the casing is allowed; names that differ only in case _cannot_ coexist.

Label colors default to white. Available colors are _red_, _green_, _yellow_, _blue_, _magenta_, _cyan_, _white_, and _gray_.

`--json` includes each label's color as a value.

`label edit` accepts name, color, and description. `label create` and `label edit` take `--description-file` for a description from a file or stdin (see [Text values](#text-values)). Renaming preserves every task attachment because attachments refer to the label's internal ID. Updating a label definition does not affect attached tasks' timestamps.

`label create`, `label edit`, `label list`, and `label delete` show a preview of the descriptions. `label view <name>` shows the full description, which preserves line breaks and whitespace. `label view --json` returns one object with `id`, `name`, `color`, and the `description` in full.

Task creation and edits apply content and label changes together, or roll back together on failure.
Changing a task's label attachments updates that task's modification timestamp. Archived tasks can also have their labels edited.

`--json` lists a task's labels as names in `labels`, with their IDs in `label_ids` in the same order. See [Entity identity](/docs/scripting#entity-identity) for the ID fields on task and nested results.

`zinn label delete urgent` asks for confirmation, then deletes the definition and removes it from every task. Declining confirmation leaves the label and its attachments unchanged. `--yes` skips the confirmation (see [Confirmations](/docs/scripting#confirmations-in-scripts)).
Use `task label remove` to remove a label attachment from one task instead. Task/project deletion cleans up label attachments, but does not affect labels themselves.

## Task relations

```sh title="Create and list task relations"
zinn task relation create MDR-1 MDR-2 --type dependency
zinn task relation create MDR-1 MDR-2 --type duplicate --yes
zinn task relation create MDR-1 OTHER-1 --type related
zinn task relation list MDR-1
```

`dependency` means the first task depends on the second; `duplicate` means the first is a duplicate of the second. `related` is symmetric.

Each task pair may have one relation across all types and both directions. Changing the type or direction asks for confirmation to replace the existing links. `--yes` authorizes replacement without a prompt, including with `--json` or without a terminal. Creating the exact existing relation is an error, including with `--yes`.

One confirmation names every link to remove. If those links change while the prompt is open, the command fails and asks you to run it again. Replacement applies completely or not at all, and records the removed links and the new link in activity with one timestamp.

Relations do not automatically move, complete, or archive tasks.
Tasks from separate projects can be related to each other. Tasks can also be related to archived tasks.

JSON relation arrays from `task relation list` and `task view` are ordered by creation timestamp ascending, then relation ID ascending for ties.

Use `zinn task relation create APP-2 APP-1 --type subtask` to make APP-2 a child of APP-1. Each task can have one parent and any number of subtasks, including nested subtasks.

Assigning a different parent asks to replace the previous parent link and any conflicting link to the new parent. Moving or archiving a parent does not change its children, and deleting a parent removes its child relations while retaining child task data.

`task create` can relate the new task with `--parent`, `--depends-on`, `--duplicate-of` and `--related`. Repeat `--depends-on` and `--related` to relate several tasks:

```sh title="Create a task with relations"
zinn task create MDR --title 'Sort the files' --parent MDR-1 --depends-on MDR-2
```

Repeating the same relation type and target counts once, ignoring task-key case. Different relation types for the same target abort creation without saving the task, consuming its number, or recording activity.

Remove a relation with `zinn task relation delete MDR-1 MDR-2 --type dependency`, using its original direction for `dependency`, `duplicate`, and `subtask`. For `related`, order does not matter.
Relation deletion requires no confirmation.

## Task comments

Add and list comments on a task:

```sh title="Create and list comments"
zinn task comment create MDR-1 --text 'Ready for review'
zinn task comment list MDR-1
```

Comments receive keys per task like `MDR-1#1`.

After creating or editing a comment, the text echo is limited to its first line and 80 terminal columns, including an ellipsis when text is omitted. Emoji and combining characters stay intact. `--json` keeps the full text.

View, edit or delete a comment using its key:

```sh title="View, edit, and delete a comment"
zinn task comment view 'MDR-1#1'
zinn task comment edit 'MDR-1#1' --text Approved
zinn task comment delete 'MDR-1#1'
```

`task comment create` and `task comment edit` take `--text-file` instead of `--text` to read the text from a file or stdin (see [Text values](#text-values)).

`task comment view` shows the current text in full, keeping its line breaks, including on archived tasks. With `--json`, it returns one object with `id`, `key`, `task_id`, `text`, `created_at` and `updated_at`.

Deletion asks for confirmation, unless `--yes` is used (see [Confirmations](/docs/scripting#confirmations-in-scripts)). Comments can be added to archived tasks. Comment mutations are recorded in the activity log without changing the task's modification timestamp.

## Edit tasks

Edit a task's title, description, priority, or labels:

```sh title="Edit tasks"
zinn task edit MDR-1 --title 'Meet the quarterly refinement quota'
zinn task edit MDR-1 --description 'Complete refinement before the waffle party'
zinn task edit MDR-1 --title 'Meet the quarterly refinement quota' --description ''
```

See [Editing conventions](#editing-conventions) for title validation, partial edits and clearing descriptions.

Archived tasks can be edited without unarchiving them.
See [Text values](#text-values) for values that begin with a dash and multi-line text.

## Move and order tasks

Move a task to another column in its project:

```sh title="Move a task"
zinn task move MDR-1 Doing
```

A moved task appears at the bottom of its destination column. Moving it to its current column does nothing. Archived tasks must be unarchived before they can be moved.

For multiple tasks, give their keys before the column. They move in input order; tasks already in that column retain their positions. Each task's column is looked up in its own project:

```sh title="Move tasks in bulk"
zinn task move MDR-1 MDR-2 APP-4 Doing
```

See [Changing tasks in bulk](#changing-tasks-in-bulk).

`--from` moves a task only if it is still in the given column. Use it to take a task that another agent or person may be taking at the same time:

```sh title="Take a task only if nobody else has"
zinn task move MDR-1 Doing --from TODO
```

When the task has left that column, the move changes nothing and fails with error code `conflict` and reason `column_changed`. The error's `expectedColumn` and `actualColumn` name both columns. This holds even when the task is already in the target column, so of several callers taking the same task, exactly one succeeds. With several task keys, one task outside the column fails the whole move.

Change a task's position within its current column:

```sh title="Order tasks"
zinn task order MDR-2 top
zinn task order MDR-2 up
zinn task order MDR-2 down
zinn task order MDR-2 bottom
zinn task order MDR-2 before MDR-1
zinn task order MDR-2 after MDR-1
zinn task order MDR-2 MDR-3 top
```

The task being reordered and the target of `before` or `after` must be active tasks in the same column.

For several tasks, ordering is the same as ordering the first task as requested, then each next task after the one before it. Tasks form a block in the given order. Each step that already satisfies its placement leaves that task unchanged and records no activity and other tasks retain their relative order. All source tasks must be distinct active tasks in the same project and column, and the target cannot be among them. `up` and `down` take one task.

## Archive and delete tasks

Active tasks are shown by default. Archive one or more tasks to hide them from active lists, then list archived tasks or all tasks:

```sh title="Archive and list tasks"
zinn task archive MDR-1
zinn task archive MDR-2 MDR-3
zinn task list MDR --archived
zinn task list MDR --all
```

`--archived` and `--all` cannot be used together.

Unarchiving puts a task at the bottom of its previous column:

```sh title="Unarchive tasks"
zinn task unarchive MDR-1
zinn task unarchive MDR-2 MDR-3
```

Archive or unarchive multiple tasks by giving several keys. Unarchived tasks land in the order given. See [Changing tasks in bulk](#changing-tasks-in-bulk).

Delete tasks permanently with `zinn task delete MDR-1 MDR-2`. Active and archived tasks from any project can be deleted together. It asks for one confirmation before performing the action, unless `--yes` is used (see [Confirmations](/docs/scripting#confirmations-in-scripts)). See [Changing tasks in bulk](#changing-tasks-in-bulk) for validation and JSON results. See [Task key reuse](/docs/data#task-key-reuse) for what happens to task numbers.

## Task priority

Priority is optional: `low`, `medium`, `high`, or `urgent`. Priority values, including `none`, are case-insensitive. Stored values and JSON use lowercase. New tasks have no assigned priority by default.

Text priority markers are `-` (unassigned), `L` (low), `M` (medium), `H` (high), and `U` (urgent).

```sh title="Set task priority"
zinn task create MDR --title 'Review the numbers' --priority high
zinn task edit MDR-1 --priority urgent
zinn task edit MDR-1 --priority none
```

Use `none` to clear priority. Priority does not affect board ordering.

## Task deadlines

Set one exact deadline when creating or editing a task:

```sh title="Set, clear and filter task deadlines"
zinn task create MDR --title 'Review the refinement report' --due 2026-10-25T15:30:00.123Z
zinn task list MDR --due-since 2026-10-25T00:00:00Z --due-until 2026-10-25T18:00:00+02:00 --sort due
zinn task search refinement MDR --due-until 2026-10-25T18:00:00+02:00
zinn task edit MDR-1 --due none
```

`--due` accepts integer Unix epoch milliseconds or a complete timestamp with seconds and an explicit `Z` or numeric UTC offset, such as `2026-10-25T17:00:00+02:00`. Fractional seconds may have one to three digits. Integer values range from `-8640000000000000` to `8640000000000000`, inclusive. Date-only values, timestamps without an offset, relative dates, impossible dates, leap seconds, `24:00`, and fractions longer than three digits are rejected.

Use `--due none` to clear a deadline. The word is case-insensitive. Omitting `--due` leaves a new task undated and leaves an edited task unchanged. Repeating the current deadline or clearing an unset deadline does not update the task or record activity.

`--due-since` and `--due-until` select deadlines inclusively on task list and search. Use either bound alone or both together. Equal bounds select that exact millisecond. An upper bound before the lower bound is invalid. Undated tasks do not match a supplied bound. Deadline bounds intersect the other filters and apply before `--limit`. Search keeps its existing relevance order, including task-key matches. Due sorting is available on task list only. It does not change board order or the default list and search order.

Task text shows deadlines in local time with a UTC offset, seconds, and any nonzero milliseconds. Full task JSON objects include `due_at` as integer epoch milliseconds or `null` when unset. Activity text uses the same precision for deadline changes. Event timestamps keep their existing precision.

## Activity

Zinn records changes to tasks, projects, columns, labels, relations, and comments, including who made them.

Deleting a project records one project deletion rather than separate deletion activity for every cascaded row.
Each removed cross-project relation records `relation_delete` activity on its surviving task, including archived tasks, without changing that task's `updated_at`.

```sh title="List and view activity"
zinn activity list
zinn activity list --limit 10
zinn activity list --since 2026-10-25T00:00:00Z --until 2026-10-25T12:00:00Z
zinn activity list --project MDR
zinn activity list --project MDR --show-deleted
zinn activity list --project-id 019abcdf-1234-7000-8000-123456abcdef
zinn activity list --task MDR-1
zinn activity list --task MDR-1 --show-deleted
zinn activity list --task-id 019abcdf-1234-7000-8000-123456abcdef
zinn activity list --session conversation-id
zinn activity list --client zinn-opencode:panel
zinn activity list --session conversation-id --thread child-thread-id
zinn activity list --action comment_create --action comment_edit
zinn activity list --action edit --changed description
zinn activity list --task MDR-1 --changed due_at
zinn activity list --changed column=Review
zinn activity list --changed priority=high --changed labels=bug
zinn activity list --changed column=Review --changed column=Done --limit 10
zinn activity list --follow --changed column=Review
zinn activity view 019abcdf-1234-7
```

`--limit` caps the newest matching events and must be an integer from `1` through `9007199254740991`. Reorders and both affected-task events from relation changes are included.

Use `--starting-after <activity-id>` to continue toward older events, while excluding the cursor event. It accepts the same full IDs and unique prefixes as `activity view`. The cursor event need not match the filters. Keep the same filters when continuing. Without `--limit`, the command returns every remaining match.

When another matching event exists after a capped page, stderr gives a command to continue with the last returned event's full ID. The command retains the filters and output mode. This hint also appears with `--json`, whose stdout remains an array of full activity records. Scripts can continue with the last event's ID whenever a page contains exactly the requested limit. An empty array marks exhaustion.

`--follow` prints activity as any client commits it and runs until stopped. It prints only events committed after it starts, oldest first in commit order. The other filters apply to followed activity as they do to a listing. `--follow` cannot be used with `--limit` or `--starting-after`.

Ctrl-C ends a following session. It also ends successfully once the reader of its output has closed it, as in `zinn activity list --follow | head -1`, when it writes the next event. It ends with a `conflict` error that names the database when its schema changes, including a restore, migration or reindex, or when its file is replaced or removed. Run the command again to follow from that point.

`--since` and `--until` select events on or after the lower bound and on or before the upper bound, using their recorded `created_at`. Use either flag alone or both together. Equal bounds select events at exactly that millisecond. `--until` must be at or after `--since`. All supplied filters must match before `--limit` applies.

Time values accept integer epoch milliseconds or `YYYY-MM-DDTHH:mm:ss` followed by `Z` for UTC or a numeric offset such as `+01:00`. Optional fractional seconds have 1 to 3 digits, so `.1`, `.12` and `.123` mean 100, 120 and 123 milliseconds. For example, `2026-10-25T12:00:00.123Z` and `2026-10-25T13:00:00.123+01:00` select the same instant on every computer. Integer inputs always mean milliseconds and must be between `-8640000000000000` and `8640000000000000`, inclusive. Use `--since=-1` for a negative value.

This is a bounded subset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6). Times require seconds and an explicit offset. Impossible dates, leap seconds, `24:00` and fractions longer than 3 digits are rejected.

`--task` selects the existing active or archived task by readable key. If that task is missing, the command fails and points to `--show-deleted`.

`--project` selects the existing project's activity, including its columns and active, archived, and deleted tasks. If that project is missing, the command fails and points to `--show-deleted`. Task comments, label attachments, and reorders are included. Global label-definition events are excluded. A relation between projects includes only the selected project's task event; a relation within the project includes both task events.

Add `--show-deleted` to a readable-key selector to include every recorded existing or deleted identity sharing that key. With `--project`, this expands project identities; explicitly deleted tasks are already included for each selected project.

`--task-id` and `--project-id` each select one exact identity, including after deletion, using a full case-insensitive UUID in canonical hyphenated or compact 32-hex form. ID prefixes are rejected. `--project`, `--project-id`, `--task`, and `--task-id` are mutually exclusive; `--show-deleted` requires `--project` or `--task`.

Deleted history and exact ID selection include the original project-deletion event when its snapshot contains the selected task. A valid exact ID or history query without recorded matches returns an empty list.

`--session` and `--thread` match the complete stored IDs exactly, including case and whitespace. All supplied filters must match, and `--limit` applies after filtering. Rows without the requested identity do not match. No matches return empty output, or `[]` with `--json`, and exit successfully. Session activity records committed changes.

`--client <name>` matches a client anywhere in the recorded chain, including case. Zinn splits the chain on whitespace and takes each token's name before its first `/`. A name matches exactly or when followed by `:`, so `--client zinn-opencode` selects `zinn-opencode/0.3.0` and `zinn-opencode:panel/0.3.0`. Use `--client zinn-opencode:panel` to select the panel. `--client zinn` does not match `zinn-opencode`. Other filters must also match before the limit applies.

`--action <action>` matches the recorded action, such as `edit`, `move`, `comment_create`, `comment_edit` or `comment_delete`. Action names use the same spelling and case as activity JSON. Repeat `--action` to match any listed action. Action and field filters must match the same event.

`--changed <field>` matches a change to a task's `column`, `priority`, `labels`, `title`, `description` or `due_at`. Labels match additions and removals. Initial task creation fields also match, including an unassigned priority or description and an unset deadline. Initial labels match when at least one label is recorded. Use `--action edit --changed description` to select description edits. `due_at` matches any deadline change and does not take a value.

`--changed <field>=<value>` matches a recorded change to `column`, `priority`, or `labels`, ignoring value case. Column and priority match their new values. Labels match an added label. Initial values on task creation also match. Use `--changed priority=none` for creation without a priority or a change that clears it. Column and label names match as recorded in the event, including after renaming or deletion.

Repeat `--changed` for several filters. Filters for the same field match any of them, including a value-free filter alongside one with a value. For example, `--changed column=Review --changed column=Done` selects events entering either column. Different fields must match the same event, so `--changed priority=high --changed labels=bug` selects events that both set high priority and add bug. Other supplied filters must also match, before `--limit` applies. Unknown fields fail and name the accepted fields.

Task creation snapshots include `due_at` as an integer epoch-millisecond value or `null`. A deadline edit records its previous and new values, with `null` for an unset deadline. Older activity payloads keep their recorded shape.

Activity text uses local dates and times with a UTC offset. `activity list` uses the shortest globally unique left prefixes of at least eight hexadecimal digits for event IDs.

`activity view` accepts a full UUID or a unique prefix of at least eight hexadecimal digits for convenience. The match ignores case and accepts both the full hyphenated or compact forms. Prefixes must be unique across all activity. An ambiguous prefix lists candidate IDs and requires more characters. Use a full ID for reliable reference results. Using prefixes may result in unreliable results over time as history grows.

`activity view` preserves full recorded text and line breaks, including unknown change fields.

With `--json`, list returns an array of [activity records](/docs/core#activity-records) and view returns one. With `--follow --json`, list prints each record as one JSON object on its own line.

With `--show-deleted`, readable keys can include different identities after a project is deleted and recreated. Group JSON records by (`entity_type`, `entity_id`) to keep their histories distinct, or use `--task-id` or `--project-id` for one identity.

Hooks receive these same records on stdin, with the task's key added as `task_key`. See [Hooks](/docs/hooks).

### Who am I?

Run `zinn whoami` to see the actor a change would record in the current environment and folder, including the source of its name and kind:

```sh
zinn whoami
zinn whoami --json
```

For the human fallback, Git identity comes from the current folder. The command works without reading settings or opening storage, including when the store is missing or unusable. It shows the resolved name, email, kind, source, session, thread, agent, model, effort and client. An unnamed agent is shown as `unknown`.

`--json` returns one object with `actor_name`, `actor_email`, `actor_kind`, `actor_source`, `actor_session_id`, `actor_thread_id`, `actor_agent`, `actor_model`, `actor_effort` and `actor_client`, matching the actor fields of an activity record. Missing values are `null`.

Each mutation records the actor identity supplied by the CLI. Zinn checks `AI_AGENT` and `AGENT` declarations before known tool markers for Claude Code, Codex, OpenCode, Gemini CLI, Cline, Cursor, Copilot's coding agent, and Crush. Set `AI_AGENT` to name another tool. Numeric-only and boolean-word values for `AI_AGENT` or `AGENT` are treated as flags, not names, and do not prevent a later marker from identifying the agent. If only a generic flag is set, the actor kind is `agent` and its name is `null`.

With no agent marker, Zinn uses your git `user.name` and `user.email`, then your system username. `actor_source` records the deciding `env:<VARIABLE>`, `git-config` or `os-user`.

Integrations can supply `ZINN_SESSION_ID` and `ZINN_THREAD_ID`. When either is set, Zinn uses that pair. Otherwise it uses the first available pair: `CLAUDE_CODE_SESSION_ID` with a `null` thread, `CODEX_SESSION_ID` with `CODEX_THREAD_ID`, or OpenCode's `OPENCODE_SESSION_ID` as the thread with a `null` session. If a member is missing, it stays `null`; Zinn does not combine values from different pairs.

`ZINN_AGENT`, `ZINN_MODEL` and `ZINN_EFFORT` declare the host agent name, running model and effort for the execution making the change. Zinn trims surrounding whitespace, treats blank declarations as _unset_ and preserves other text. These values are declarations rather than authenticated identities. Activity JSON keeps these values with the event.

### Client chains

Every CLI write records `actor_client` as `zinn-cli/<package version>`. Set `ZINN_CLIENT` to declare the programs calling the CLI. Zinn trims its surrounding whitespace and appends the remaining text after its own token, separated by one space. An empty or whitespace-only declaration leaves only the CLI token. The declared text is preserved without validating its format.

```sh title="Declare a calling client"
ZINN_CLIENT='rex/0.9 zinn-opencode:panel/0.3.0' zinn whoami --json
```

For example, CLI version `0.44.0` records `zinn-cli/0.44.0 rex/0.9 zinn-opencode:panel/0.3.0` for that declaration. With no declaration, it records `zinn-cli/0.44.0`.

To format a declaration, see the [client chain convention](/docs/core#client-chain-convention). Client declarations identify the calling programs and are not authenticated identities. The chain is also kept in the `actor.client` field of each [local telemetry record](/docs/data#record-fields).

## Agent skill

`zinn skill install` installs the [Zinn workflow skill](/docs/installation#skill) bundled with this CLI. It runs `npx --yes skills add <skill folder> -g -y` with the skill folder inside the CLI's package, and passes the [skills CLI](https://github.com/vercel-labs/skills)'s output and exit code through:

```sh title="Install the agent skill"
zinn skill install
zinn skill install --json
```

With `--json`, stdout gets `{ "source": "<skill folder>" }`, and the skills CLI's output is reported as notices: on stderr after the result, or in `error.diagnostics` when the install fails. A failure's `error.details` holds the skill folder as `path` and the skills CLI's `exitCode`.

## Command aliases

These aliases work in every namespace that has the primary command, with the same arguments, options and confirmations:

| Primary command | Alias    |
| --------------- | -------- |
| `view`          | `show`   |
| `list`          | `ls`     |
| `delete`        | `rm`     |
| `create`        | `new`    |
| `edit`          | `update` |

For example, `zinn task show APP-1` runs `zinn task view APP-1`, and `zinn task comment new APP-1 --text 'Verified'` creates a comment. Alias words used as values stay values: `zinn task list rm` lists tasks in project `rm`.

A command's help names its full alias command. Root and namespace help use primary command names. [Command discovery](/docs/scripting#discover-the-command-contract) includes alias paths in each command descriptor.

## Help

Use root help to discover commands and namespace help to find what each command does, including nested commands. Open a command's help for its arguments and behavior:

```sh title="Show command help"
zinn --help
zinn project --help
zinn project column --help
zinn task --help
zinn task move --help
```

The short `-h` form works in the same positions. `--help` shows help for the command words before it and runs nothing. The rest of the line is ignored.

Running `zinn` without a command shows help. Use `zinn --version` (or `-v`) to print the CLI version.
