Connect dothings to your terminal & Claude

do things is a team task workspace. The justdothings package gives you a CLI and an MCP server, so you — or a Claude agent — can manage tasks, projects, and team membership without leaving your editor.

Quickstart

Three steps, then ask Claude to add a task.

npm i -g justdothings
justdothings login          # paste a token from /settings/tokens
justdothings install-mcp    # add the MCP server to Claude
justdothings install-skill  # teach Claude Code the CLI (writes .claude/skills/…)

Then in Claude: “add a todo to the website project: fix the footer links, high priority.”

Install the CLI

npm i -g justdothings
justdothings --version

Requires Node 18+. The same package ships the CLI (justdothings, dt) and the MCP server (justdothings mcp).

Create an API token

Generate a personal access token at /settings/tokens. It's shown once — copy it. A token acts as you: it carries your workspace membership and project access, and anything it creates is attributed to you. Workspace-scoped tokens are fenced to a single workspace — useful for CI or per-environment automation.

justdothings login            # paste it when prompted
# …or, statelessly:
export DOTHINGS_TOKEN=dt_live_…

Configure the MCP server

justdothings install-mcpprints this with your token inlined. Paste it into your client's MCP config:

Project .mcp.json, or run claude mcp add.

{
  "mcpServers": {
    "dothings": {
      "command": "justdothings",
      "args": ["mcp"],
      "env": { "DOTHINGS_TOKEN": "dt_live_…" }
    }
  }
}

Claude Code skill

Using Claude Code? Install the skills and Claude will automatically know how to use the CLI and the MCP server — commands, tools, share link format, gotchas and all. It writes .claude/skills/dothings-cli/SKILL.md and .claude/skills/dothings-mcp/SKILL.md in the current repo (commit them so your team gets them too), or into ~/.claude/skills/ with --global for every project.

justdothings install-skill            # this repo
justdothings install-skill --global   # every project on this machine

Both are generated from the same manifest as this page, so re-run the command after upgrading the CLI to refresh them. No CLI handy? Fetch them directly:

mkdir -p .claude/skills/dothings-cli .claude/skills/dothings-mcp
curl -fsSL https://dothings.fyi/docs/skill.md     -o .claude/skills/dothings-cli/SKILL.md
curl -fsSL https://dothings.fyi/docs/mcp-skill.md -o .claude/skills/dothings-mcp/SKILL.md
Preview dothings-cli/SKILL.md
---
name: dothings-cli
description: Manage dothings.fyi tasks from the terminal with the `justdothings` CLI — list/search/create/update/move/delete tasks, read & add comments, manage labels, create typed task links, link GitHub issues/PRs to tasks, manage pages/docs, and manage workspace membership & invites. Use whenever the user wants to view or change their dothings tasks or board, mark tasks done/finished, triage a backlog, assign people, attach a GitHub item to a task, manage pages/docs, or manage workspace members/invites from the command line. Triggers on "my dothings tasks", "mark this task done", "what's on my board", "add a dothings task", "link this PR to a task", "justdothings", "dt tasks".
---

# dothings CLI (`justdothings`)

A CLI for **dothings.fyi**, a task/board app. Same binary also serves the MCP server (`justdothings mcp`). Talks to the REST API at `https://api.dothings.fyi` by default.

## Install & invoke

Published on npm: `npm i -g justdothings`. Bins: `justdothings`, `dt`, `dothings` (all identical).

**Every command accepts `--json`** for machine-readable output — always pass it when you need to parse the result (ids, status, etc.). List/create commands also take `-q`/`--quiet` to print only ids, one per line.

**Exit codes:** `0` ok · `1` generic · `2` not logged in / bad token · `3` forbidden · `4` not found · `5` bad input or likely duplicate · `6` network. With `--json`, an error is a single JSON line on stderr: `{"error":{"status","code","message"}}`.

## Auth (do this first if commands 401)

- `justdothings login` — opens the browser to approve a new token (no pasting). `--token <dt_live_…>` or `--paste` for headless use (create one at `https://dothings.fyi/settings/tokens`).
- `justdothings whoami` — current user + accessible projects. Good connectivity check.
- `justdothings auth status` — show configured base URL + token presence.
- `justdothings logout` — clear the saved token.

Config: token from `DOTHINGS_TOKEN` env or `~/.config/dothings/config.json`.

## Enums (the API rejects others)

- **status:** `think_about` · `todo` · `doing` · `in_review` · `finished`  ("done" = `finished`)
- **priority:** `none` · `low` · `medium` · `high` · `urgent`
- **link types:** `blocks` · `relates` · `duplicates` · `implements` · `parent`
- **workspace roles:** `owner` · `admin` · `member`
- **project roles:** `admin` · `member`

Projects, members, labels are **project-scoped** — pass `--project <idOrSlug>` (slug like `dothings-fyi` works).

## Command reference

- `justdothings login [--token <t> | --paste] [--base-url <url>]` — sign in through your browser (approve a code; no token pasting); --token / --paste use an existing token
- `justdothings logout` — remove the stored API token
- `justdothings whoami` — current user + accessible projects
- `justdothings auth status [--json]` — show the configured base URL + whether a token is set
- `justdothings auth keys` — list your active personal access tokens
- `justdothings profile set [--display-name]` — update your own display name
- `justdothings notifications read <id>` — mark one notification read
- `justdothings notifications read-all` — mark all your notifications read
- `justdothings notifications read-messages [--comment <id>…]` — mark mention/reply messages read
- `justdothings projects list [--search -q]` — list projects (-q prints slugs only)
- `justdothings projects get <idOrSlug>` — show one project
- `justdothings projects create --name <n> [--description --color --member <id>…]` — create a project (admin)
- `justdothings projects rename <idOrSlug> <name>` — rename a project (admin)
- `justdothings projects set-description <idOrSlug> <description>` — set a project's description (admin)
- `justdothings projects archive <idOrSlug>` — archive a project (admin)
- `justdothings projects unarchive <idOrSlug>` — unarchive a project (admin)
- `justdothings members list --project <slug>` — list assignable people
- `justdothings members add <projectIdOrSlug> <userId>` — add a member (admin)
- `justdothings members remove <projectIdOrSlug> <userId>` — remove a member (admin)
- `justdothings members set-role <projectIdOrSlug> <userId> <role>` — set a project member's role (admin)
- `justdothings teammates list` — list everyone in the workspace (admin)
- `justdothings labels list --project <slug>` — list labels
- `justdothings labels create --project <slug> --name <n> [--color <hex>]` — create a label
- `justdothings tasks list [--project --status --priority --assignee --me --label --search --updated-since --all -q --compact]` — list/search tasks (--updated-since <iso> for only tasks changed after it; table hides finished unless --all; --json returns all; -q prints ids only; --compact drops body/attachments from --json)
- `justdothings tasks mine [--project --status --priority --label --search --all -q --compact]` — tasks assigned to you (-q prints ids only)
- `justdothings tasks get <id> [-q]` — show a task in full (id, key (DOTH-84), or share link)
- `justdothings tasks create --project <slug> --title <t> [--body|--body-file --status --priority --assignee… --label… --start --due -q]` — create a task (-q prints the new id only)
- `justdothings tasks create --project <slug> --title <t> --plan [--body --status --priority --label… --json]` — dry-run pre-flight, creates nothing: likely duplicates (key/title/score/link) and the suggested assignees/labels/due/cycle it would use; --json prints the raw POST /v1/tasks/prepare plan
- `justdothings tasks create --project <slug> --title <t> --apply-suggestions [--force …create flags]` — create, filling ONLY the assignees/labels/due you didn't pass from suggestions and assigning the suggested (active) cycle; if a duplicate scores ≥ 0.6 it prints it and exits 5 without creating (--force to create anyway)
- `justdothings tasks update <id> [--title --body|--body-file --status --priority --start --due --abandoned --add-assignee… --remove-assignee… --add-label… --remove-label…]` — update a task (id, key (DOTH-84), or share link)
- `justdothings tasks update --stdin [--dry-run …same patch flags]` — bulk: apply one patch to every task ref on stdin (one per line, or a `tasks list --json` array); add/remove deltas resolve per task; prints a line per task + summary, continues past errors, exits 1 if any failed; --dry-run shows what would change
- `justdothings tasks move <id> <status>` — move between columns (id, key (DOTH-84), or share link)
- `justdothings tasks move --stdin <status> [--dry-run --position <n>]` — bulk: move every task ref on stdin (one per line, or a `tasks list --json` array) to <status>; exits 1 if any failed; --dry-run lists current → new status
- `justdothings tasks start <id>` — move a task to doing (id, key (DOTH-84), or share link)
- `justdothings tasks done <id>` — move a task to finished (id, key (DOTH-84), or share link)
- `justdothings tasks open <id>` — open the task in your browser (id, key (DOTH-84), or share link)
- `justdothings tasks delete <id> --yes` — delete a task (id, key (DOTH-84), or share link)
- `justdothings comments list <taskId> [--since <iso>]` — read a comment thread (task: id, key (DOTH-84), or share link; --since: only what changed after that instant; --json adds ids + cursor)
- `justdothings comments add <taskId> [body] [--body|--body-file --reply-to <id> -q]` — add a comment (task: id, key (DOTH-84), or share link; -q prints the comment id)
- `justdothings comments edit <commentId> [body] [--body|--body-file]` — edit a comment (author/admin)
- `justdothings comments delete <commentId>` — delete a comment (author/admin)
- `justdothings attachments upload <file…> [--json]` — upload images (PNG/JPG/WEBP/GIF, ≤ 4 MB) and print ![](url) markdown to paste into a comment or task body
- `justdothings link <src> <tgt> --type <type>` — link two tasks (each: id, key (DOTH-84), or share link)
- `justdothings unlink <linkId>` — remove a task link
- `justdothings relink <linkId> <type>` — change a task link's type
- `justdothings github items <projectIdOrSlug>` — list a project's synced GitHub issues & PRs
- `justdothings github link <taskId> <githubItemId>` — link a GitHub issue/PR to a task (task: id, key (DOTH-84), or share link)
- `justdothings github link-pr <taskId> <number>` — sync + link a repo PR/issue #number to a task (task: id, key (DOTH-84), or share link)
- `justdothings github unlink <taskId> <githubItemId>` — remove a GitHub item link from a task (task: id, key (DOTH-84), or share link)
- `justdothings github connect <projectIdOrSlug> <repo>` — connect a repo + initial sync (admin)
- `justdothings github disconnect <projectIdOrSlug>` — disconnect the repo + drop items (admin)
- `justdothings github sync <projectIdOrSlug>` — re-pull a project's GitHub items (member)
- `justdothings github repos list` — list repos the workspace can connect (admin)
- `justdothings github account connect` — verify YOUR GitHub account via device flow
- `justdothings github account disconnect` — unlink your GitHub account
- `justdothings github account status` — show your linked GitHub account + verification
- `justdothings start <task> [--no-cycle --no-checkout -q]` — pick up a task in one step: move to doing, add YOU to assignees (keeps existing ones), put it in the project's active cycle if it has none (--no-cycle skips), and check out its branch (reuses an existing local branch; --no-checkout skips; outside a git repo it warns, not fails); prints the share link. `tasks start` is the plain move
- `justdothings pr [task] [--title <t> --summary <text> --draft --base <branch>]` — open a GitHub PR for a task via `gh` and link it: task defaults to the key in the current branch name (doth-84-slug); title `<KEY> <task title>`; body = summary line + `Fixes <KEY>` + share link; pushes with `git push -u origin HEAD` when the branch has no upstream; an existing PR for the branch is just linked. Requires gh
- `justdothings today [--project <slug>]` — standup view (alias: standup): your doing + in review tasks, open tasks overdue or due within 3 days, tasks finished in the last 24h (by updated_at, best effort), and unread message count + top 5; --json → { doing, in_review, due_soon, finished_recently, unread_messages: { count, items } }
- `justdothings inbox [--all --limit <n> --mark-read]` — unread notifications + mention/reply messages merged newest first (one line per event: when, who, type/kind, task key + title, snippet, link with ?c=<comment>); --all includes read, --limit caps (default 30), --mark-read marks everything read after printing; --json → { notifications, messages }
- `justdothings branch <task>` — print a task's git branch name (eng-42-slug; task: id, key (DOTH-84), or share link); --checkout to switch to it
- `justdothings pages list --project <slug>` — list a project's pages
- `justdothings pages get <id>` — show a page with its full content
- `justdothings pages create --project <slug> --type <doc|sheet|tweet> [--title --content|--content-file -q]` — create a page (-q prints the new id only)
- `justdothings pages update <id> [--title --content|--content-file --archive --unarchive]` — update a page
- `justdothings pages delete <id> --yes` — delete a page
- `justdothings pages link <pageId> <taskId>` — link a page to a task (task: id, key (DOTH-84), or share link)
- `justdothings pages unlink <pageId> <taskId>` — remove a page ↔ task link (task: id, key (DOTH-84), or share link)
- `justdothings cycles list --project <slug> [-q]` — list a project's cycles with progress (-q prints ids only)
- `justdothings cycles create --project <slug> --name <name>` — create a cycle (--status --start --end; -q prints the new id only)
- `justdothings cycles update <id>` — update a cycle (--name --status --start --end; pass 'none' to --start/--end to clear a date)
- `justdothings cycles delete <id> --yes` — delete a cycle (its tasks are detached)
- `justdothings cycles assign <task> <cycleId>` — assign a task (id, key (DOTH-84), or share link) to a cycle ('none' to detach)
- `justdothings workspace rename <name>` — rename the workspace (owner)
- `justdothings workspace members list` — list workspace members (member)
- `justdothings workspace members set-role <userId> <role>` — set a workspace member's role (admin)
- `justdothings workspace members remove <userId>` — remove a workspace member (admin)
- `justdothings workspace invites send <email> [--role]` — invite someone to the workspace (admin)
- `justdothings workspace invites resend <id>` — resend a workspace invite (admin)
- `justdothings workspace invites revoke <id>` — revoke a workspace invite (admin)
- `justdothings mcp` — run the stdio MCP server
- `justdothings install-mcp [--client claude-code|claude-desktop|cursor --with-token]` — print the MCP config snippet (no token unless --with-token; claude-code also prints the `claude mcp add` one-liner)
- `justdothings install-skill [--global --print --skill <cli|mcp|all>]` — install or refresh the Claude Code skills (dothings-cli + dothings-mcp) into the repo's .claude/skills, or ~/.claude/skills with --global
- `justdothings docs` — open the documentation in your browser
- `justdothings completion <bash|zsh|fish>` — print a shell completion script (e.g. eval "$(justdothings completion zsh)")

## Referring to tasks

Every command that takes a task accepts its **key** (`DOTH-84`), its **id**, or its **share link** (`https://dothings.fyi/t/<id>`) — use whichever you have. To find one, `tasks list --search <words>` (or `tasks mine`).

## Recipes

- **Add a task:** resolve the project (`projects list`) → `tasks create --project <slug> --title "…" --plan` (read-only: likely duplicates + suggested assignees/labels/due/cycle) → if no duplicate, the same command with `--apply-suggestions --body "Summary sentence.\n\n## Context\n…" -q`.
- **Pick up work:** `today` (or `tasks mine`) → `start DOTH-84` (doing + assigns you + active cycle + checks out the branch).
- **Ship it:** `pr` from the task's branch — opens the PR with `gh` (title `<KEY> <title>`, body `Fixes <KEY>`) and links it; the board then follows the PR.
- **Inbox:** `inbox` lists unread notifications + mentions/replies; `--mark-read` clears them.
- **Bulk:** `tasks list --project <slug> --label bug --json | tasks update --stdin --add-label infra --dry-run`, then drop `--dry-run`. Also `tasks move --stdin <status>`.
- **Finish:** `tasks done <id>` (closes linked GitHub issues). Discard an idea instead with `tasks update <id> --status finished --abandoned true`.
- **Link an existing PR:** `github link-pr <task> <prNumber>` (syncs the repo first) — or `github items <project> --json` then `github link <taskId> <itemUuid>`.
- **Comment / reply:** `comments list <taskId> --json` for ids → `comments add <taskId> "…" --reply-to <commentId>`. Use `@username` to mention.
- **Reassign / relabel:** `tasks update <id> --add-assignee alice --remove-label bug` (deltas) or `--assignee alice` (replaces).
- **Attach a doc:** `pages create --project <slug> --type doc --title "…" --content-file spec.json --json` → `pages link <pageId> <taskId>`.

## Always include share links

When referencing any task or comment in conversation, always append the share link inline — don't just mention the title or ID:
- Task: `https://dothings.fyi/t/<taskId>`
- Comment: `https://dothings.fyi/t/<taskId>?c=<commentId>`

## Gotchas

- **`--body` interprets `\n` as a newline** — write multiline bodies inline with `\n` (or `\t`), or use `--body-file <path>` for verbatim / complex content.
- **Moving a task to `finished` closes any linked GitHub issues.** To discard a `think_about` idea instead, use `tasks update <id> --abandoned true`.
- `github link`/`unlink` take the **item UUID**, not the PR/issue number — use `github link-pr <task> <number>` to link by number.
- Labels must already exist before assigning; create with `labels create` first.
- `workspace` commands require a **workspace-scoped token**. An unscoped legacy token will get a 400.
- Dates (`--start`, `--due`) are `YYYY-MM-DD`; pass `none` on `tasks update` to clear one.
- `tasks update` with `--assignee`/`--label` **replaces** the whole set; use `--add-*`/`--remove-*` for deltas.
- Errors print `justdothings: <message> (HTTP <status> <code>)` on stderr and exit with the codes above — 2 means log in, 3 no access to that project, 4 a wrong id or key.
- Prefer `--json` + parse over scraping the table when scripting or chaining.

## Keeping this skill current

`justdothings install-skill` (re)writes this file (and the companion `dothings-mcp` skill) into the current repo's `.claude/skills/`; `--global` targets `~/.claude/skills/` instead. Re-run it after `npm i -g justdothings@latest` so the command reference matches the installed CLI.

## Conventions

The conventions below are shared with the MCP server, so they name MCP tools. CLI equivalents: `list_members` → `members list`, `create_label` → `labels create`, `list_projects` → `projects list`, `get_task`/`update_task` → `tasks get`/`tasks update`, `get_page`/`update_page` → `pages get`/`pages update`, `link_page` → `pages link`, `prepare_task` → `tasks create --plan`, `list_inbox` → `inbox`, `link_github_pr` → `github link-pr` (or `pr`), `set_task_cycle` → `cycles assign`.

### Status (the workflow column)
- think_about — a rough idea or "maybe"; not committed yet. Capture things worth keeping
  that aren't ready to act on.
- todo — committed and ready to be picked up (the right default when a user says "add a task").
- doing — actively in progress right now.
- in_review — work is done and awaiting review; a task lands here automatically when its
  linked GitHub PR opens, and moves to finished when that PR merges.
- finished — done (or, for a think_about idea, discarded).

### Finishing vs. discarding
Moving a task to finished marks it done AND closes any linked GitHub issues. To drop a
think_about idea rather than complete it, set abandoned: true (update_task) so it isn't
counted as a real completion — finished alone reads as "done".

### Priority
none, low, medium, high, urgent — set it to reflect real urgency. Default to none unless
the user implies otherwise.

### Referring to tasks
Anything that takes a task — every tool argument and every CLI command — accepts its id,
its key (DOTH-84), or its share link (https://dothings.fyi/t/<id>). Pass whichever you have;
there is no need to look up an id first.

### Lead with a summary
Everything you write for people — every task body AND every comment — starts with a plain
1–2 sentence summary of the outcome or the point. Nothing comes before it: no heading, no
bullet, no preamble. Details follow the summary:
- a task body: the summary, then "## Context" and an "## Acceptance criteria" checklist;
- a comment: the summary (what happened, what you decided, or what you need), then the
  supporting detail.

### Writing a task body (markdown)
Bodies are GitHub-flavored markdown, written so a teammate could pick the task up cold:

    Rate-limit the public REST API so one noisy token can't starve everyone else.

    ## Context
    Why this matters / where it came from.

    ## Acceptance criteria
    - [ ] concrete, checkable outcome
    - [ ] another

### Assignees & labels
- Assignees are referenced by username; resolve a person's name via list_members (or the
  members prepare_task returns).
- Every task gets an assignee — an ownerless task is one nobody does; work in progress
  especially needs an owner.
- Labels are project-scoped and must exist before use (create one with create_label).

### Enriching a task before you create it
A one-line request is not a finished task. Call prepare_task (CLI: tasks create --plan)
BEFORE create_task: one read-only call that returns likely open duplicates (scored 0..1),
the project's labels and members, its active cycle, and suggested assignees, labels, due
date and cycle. It writes nothing. Use it instead of separate list_tasks / list_members /
list_labels / list_cycles lookups — and treat its suggestions as a starting point, not a
verdict: apply the rules below on top of them.

1. **Don't duplicate.** Read prepare_task's duplicates (or list_tasks with 2–3 distinctive
   words from the request). If an open task already covers the work, say so and stop —
   comment on it, update it, or bump its priority instead of filing a second card. Related
   but distinct: create it, then link_tasks the two (relates, or duplicates/blocks when
   that's the real relationship).
2. **Always assign.** Stop at the first hit: whoever the request names or implies ("for
   gordo", "@az", "I'll take it", "me" — resolve via the members list) → the area's usual
   owner (who owns the recent tasks carrying the label you're applying; when the same
   member owns two or more, assign them AND the current user — committed work only, since
   assigning notifies people and a raw think_about idea shouldn't ping anyone) → the
   current user.
3. **Tag it — that's your job.** Auto-tagging is an agent rule: prepare_task suggests
   labels but never applies them, so choose them deliberately from the project's own
   vocabulary. Label sets differ per project, so classify the request into concepts and
   apply the most specific EXISTING label covering each (a project with backend/frontend
   doesn't need an invented "code"), max three. Anything touching the codebase —
   implementing, refactoring, fixing, an endpoint, a component, a migration, a test, a
   build, a named file, function or repo — is a code concept and gets labelled as one.
   Create a label only when nothing existing covers the concept, only from the canonical
   set (code, bug, feature, infra, design, docs, ops, growth), at most one per task.
4. **Urgent work gets a date; real work gets a body.** Urgent with no stated deadline is
   due today, high is due by the end of the week; anything past a one-liner gets a body
   that leads with a summary (see "Lead with a summary").
5. **Committed work joins the running cycle.** If the project has an active cycle
   (prepare_task's active_cycle), set_task_cycle the new task onto it — todo, doing and
   in_review only; think_about ideas stay in the backlog.

### Tasks and the code they produce
When the work lives in a connected repo, wire the two together instead of letting them drift:
- Starting a task: move it to doing, make sure it has an assignee, put it in the active
  cycle, and branch using the task's own branch_name (eng-42-slug). The CLI's start
  command does all of that in one step.
- Once a PR exists, link it. CLI: pr opens the PR from the task's branch and links it.
  MCP: link_github_pr(task_id, number) syncs the repo and links PR/issue #number
  (link_github takes an already-synced item's id from list_github_items). A linked PR
  moves the task to in_review when it opens and to finished when it merges, so the board
  tracks the code without anyone maintaining it by hand.

### Reading your inbox
list_inbox (CLI: inbox, or today for your day at a glance) returns your unread
notifications and messages (mentions + replies to you) in one call. Clear them afterwards
with mark_notification_read, mark_all_notifications_read or mark_messages_read.

### Linking tasks
Relationships have a type — pick the one matching the real dependency:
- blocks — source must be done before the target can proceed.
- relates — loosely related.
- duplicates — source duplicates the target.
- implements — source implements the target.
- parent — source is the parent of the target (subtask hierarchy).
A task with an unfinished `blocks` link into it is blocked: every task carries
blocked_by — the blockers' id, key, title and status — and the app marks it. Read it
before starting a task; finishing the blocker clears it. Merging or closing a PR moves a
task only when it's the task's last open PR.

### Polling cheaply
Don't re-read whole threads or boards to notice change. GET /v1/tasks/:id/comments?since=<cursor>
(comments list --since, list_comments since) returns only comments written or edited after the
cursor, plus ids (drop what's missing — it was deleted) and the next cursor. GET /v1/tasks?updated_since=
(tasks list --updated-since, list_tasks updated_since) returns only tasks changed after an instant.

### Searching
list_tasks / tasks list --search / GET /v1/tasks?q= match, across the projects the caller
can see: title, description, any comment, a comment id or link, a linked PR/issue (title,
#number, repo#number, URL), a linked task's key or title (any link type), a linked page's
title, an assignee's name, a label, the task's key or number ("DOTH-126", "126"), or its
id / share link. Case-insensitive substring; the app's search bar has the same reach plus
typo tolerance.

### Resolving a project
Tasks belong to exactly one project. Match the user's words to a project name or slug via
list_projects (get_project for one); if several could match, ask which one.

### GitHub & task links
get_task returns a task's linked github_items (issues AND pull requests), its links
(typed relationships to other tasks) and blocked_by — read them to see what a task depends
on, references, or is waiting on before acting. Every task also carries attachments: image URLs parsed out of
its markdown body (the inline ![](url) images the web composer uploads). To attach an image
yourself — a screenshot as proof, say — `attachments upload <file>` (POST /v1/attachments)
prints the ![](url) markdown; put it in a comment or the task body.
To attach an already-synced issue/PR by its id: list_github_items, then
link_github(task_id, github_item_id) — the item must be in the task's project. Remove one
with unlink_github, passing get_task's github_items[].id. Linking never creates issues or
PRs on GitHub.

### Pages (in-project docs, sheets & tweet drafts)
A project can hold pages — list_pages/get_page to read, create_page/update_page to write,
link_page to attach one to a task. A page's content shape is keyed by its type:
- doc — a TipTap/ProseMirror node: { type: "doc", content: [ … ] }.
- sheet — a table: { columns: [{ id, name, type, options?, width? }], rows: [{ "<columnId>": cell }] };
  a column type is text | number | date | due | updated | checkbox | select | member
  (options lists a select's allowed values; a cell is a string, number, boolean, or null).
- tweet — a draft thread: { tweets: [{ id, text, images? }] }.
update_page REPLACES content, so get_page first and send back the full value. The anchored,
Google-Docs-style comment threads on a doc page are web-only (no REST/CLI/MCP surface).

### Notifications & attribution
You act AS the token's user; tasks, comments and completions are attributed to them.
Assigning, @-mentioning, or replying notifies that person (in-app + push) — don't
bulk-assign or mention more people than intended.
Preview dothings-mcp/SKILL.md
---
name: dothings-mcp
description: Install and use the dothings.fyi MCP server so a Claude client (Claude Code, Claude Desktop, Cursor) can manage dothings tasks through tools — list_tasks, get_task, create_task, update_task, move_task, add_comment, link_tasks, link_github, pages, cycles, workspace admin and more (60 tools). Use when the user wants to connect dothings to Claude/Cursor, set up or configure the dothings MCP server, generate the `.mcp.json` snippet, or understand/drive their tasks via MCP tools rather than the terminal. Triggers on "add dothings to Claude", "set up the dothings MCP", "install-mcp", "dothings mcp server", "connect my tasks to Cursor".
---

# dothings MCP server

The `justdothings` binary (`npm i -g justdothings`) is **both** a CLI and an MCP server. `justdothings mcp` runs a stdio MCP server named `dothings` exposing 60 tools over the REST API at `https://api.dothings.fyi`. For terminal usage see the `dothings-cli` skill — same auth, same data, same permissions.

## Install into a client

```bash
justdothings login                                          # approve in the browser; saves the token locally
claude mcp add --scope user dothings -- justdothings mcp    # Claude Code, all projects
justdothings install-mcp --client claude-desktop            # or: cursor | claude-code — prints the config block
```

`install-mcp` prints a ready-to-paste block. It does **not** embed your token — the server reads the saved login:

```json
{ "mcpServers": { "dothings": { "command": "justdothings", "args": ["mcp"] } } }
```

- `install-mcp --with-token` adds `env.DOTHINGS_TOKEN` for clients that can't read the saved login. **Never commit a real token** — keep it in the client's env/secret store.
- The token comes from `DOTHINGS_TOKEN` in the environment or a prior `justdothings login`. Override the API base with `DOTHINGS_BASE_URL`.
- Verify: the `dothings` server should list 60 tools and a `dothings://conventions` resource.

## Tools (60)

**You (self)**
- `whoami` — the authenticated user + accessible projects
- `update_profile` — update your own display name
- `list_api_keys` — your active personal access tokens (self)
- `mark_notification_read` — mark one of your notifications read
- `mark_all_notifications_read` — mark all your notifications read
- `mark_messages_read` — mark mention/reply messages read
- `list_inbox` — your notifications + mention/reply messages in one call (unread by default)

**Projects & members**
- `list_projects` — list/resolve projects (by name or slug)
- `get_project` — one project by id or slug
- `create_project` — create a project (admin)
- `update_project` — rename / set description (admin)
- `list_teammates` — everyone in the workspace (admin); ids feed add_member
- `add_member` — add a teammate to a project (admin)
- `remove_member` — remove a teammate from a project (admin)

**Tasks**
- `list_tasks` — list/search tasks with filters (search covers title, description and comments; compact by default; limit; updated_since)
- `prepare_task` — read-only pre-flight before create_task: duplicates, labels, members, active cycle, suggestions
- `get_task` — one task in full (body, assignees, labels, linked GitHub issues/PRs, task links, image attachments)
- `create_task` — create a task in a project
- `update_task` — patch a task; assignees/labels replace the set, add_*/remove_* apply deltas
- `move_task` — move a task to a status column
- `delete_task` — permanently delete a task

**Comments**
- `add_comment` — add a markdown comment (supports @mentions, replies)
- `edit_comment` — edit a comment (author or admin)
- `delete_comment` — delete a comment (author or admin)
- `list_comments` — read a task's comment thread

**Labels & assignable people**
- `list_labels` — labels in a project
- `create_label` — create a project label
- `list_members` — people assignable in a project

**Task links**
- `link_tasks` — create a typed link between two tasks
- `unlink_tasks` — remove a task link by id
- `update_task_link` — change a task link's type

**GitHub items**
- `list_github_items` — list a project's synced GitHub issues & PRs
- `link_github` — link a GitHub issue/PR to a task
- `link_github_pr` — sync + link a PR/issue to a task by its number
- `unlink_github` — remove a GitHub item link from a task

**Pages (docs, sheets, tweet drafts)**
- `list_pages` — list a project's pages (docs, sheets, tweet drafts)
- `get_page` — one page in full, incl. its typed content
- `create_page` — create a page (doc|sheet|tweet) in a project
- `update_page` — update a page's title / content / archived flag
- `delete_page` — permanently delete a page
- `link_page` — link a page to a task (same project)
- `unlink_page` — remove a page ↔ task link

**Project admin & repo wiring**
- `connect_github_repo` — connect a repo "owner/repo" + initial sync (admin)
- `disconnect_github_repo` — disconnect the repo + drop synced items (admin)
- `sync_github` — re-pull a project's GitHub issues & PRs (member)
- `archive_project` — archive or unarchive a project (admin)
- `set_project_member_role` — set a member's per-project role: "admin" or "member"

**Workspace**
- `rename_workspace` — rename the token's workspace (owner)
- `list_workspace_members` — list workspace members with roles (member)
- `set_workspace_member_role` — set a member's workspace role (admin)
- `remove_workspace_member` — remove a member from the workspace (admin)
- `invite_workspace_member` — invite someone to the workspace by email (admin)
- `resend_workspace_invite` — resend a pending invite email, refresh expiry (admin)
- `revoke_workspace_invite` — cancel a pending workspace invite (admin)
- `list_installable_repos` — repos the workspace can connect via GitHub App (admin)

**Cycles**
- `list_cycles` — list a project's cycles (sprints) with progress counts
- `create_cycle` — create a cycle in a project
- `update_cycle` — update a cycle's name/status/dates
- `delete_cycle` — delete a cycle (its tasks are detached)
- `set_task_cycle` — assign a task to a cycle (or detach)

## Enums (the API rejects others)

- **status:** `think_about` · `todo` · `doing` · `in_review` · `finished`  ("done" = `finished`)
- **priority:** `none` · `low` · `medium` · `high` · `urgent`
- **link types:** `blocks` · `relates` · `duplicates` · `implements` · `parent`
- **workspace roles:** `owner` · `admin` · `member`; **project roles:** `admin` · `member`

## Always include share links

When referencing any task or comment in conversation, append the share link inline:
- Task: `https://dothings.fyi/t/<taskId>`
- Comment: `https://dothings.fyi/t/<taskId>?c=<commentId>`

## Gotchas

- Call `prepare_task` before `create_task` — one read-only call instead of list_tasks/list_members/list_labels/list_cycles; its suggestions aren't applied for you.
- Any task argument accepts the task's id, key (`DOTH-84`) or share link.
- `update_task` `assignees`/`labels` **replace** the whole set; use `add_assignees`/`remove_assignees`/`add_labels`/`remove_labels` to change part of it. `body` and `update_page` `content` also replace — `get_task`/`get_page` first.
- `list_tasks` is **compact by default** (no `body`/`attachments`) — use `get_task` for the body, or pass `compact: false`.
- `move_task` to `finished` **closes linked GitHub issues** and records a completion. To discard a `think_about` idea use `update_task` with `status: finished, abandoned: true`.
- `link_github`/`unlink_github` take the item **UUID** (from `list_github_items` or `get_task`'s `github_items[].id`); to link by PR/issue number use `link_github_pr`. The item must be in the task's project.
- Labels must exist before use (`create_label`). Assignees are usernames (`list_members`).
- Dates are `YYYY-MM-DD`; pass `null` to clear `start_date`/`due_date`.
- `delete_task`, `delete_page`, `delete_cycle`, `disconnect_github_repo`, `remove_workspace_member` are destructive — confirm intent before calling.
- Workspace tools need a **workspace-scoped token**; an unscoped legacy token gets a 400.
- Errors come back as `{ error: { code, message } }`: 401 bad token, 403 no access to that project, 404 wrong id, 400 bad input.

## Conventions

### Status (the workflow column)
- think_about — a rough idea or "maybe"; not committed yet. Capture things worth keeping
  that aren't ready to act on.
- todo — committed and ready to be picked up (the right default when a user says "add a task").
- doing — actively in progress right now.
- in_review — work is done and awaiting review; a task lands here automatically when its
  linked GitHub PR opens, and moves to finished when that PR merges.
- finished — done (or, for a think_about idea, discarded).

### Finishing vs. discarding
Moving a task to finished marks it done AND closes any linked GitHub issues. To drop a
think_about idea rather than complete it, set abandoned: true (update_task) so it isn't
counted as a real completion — finished alone reads as "done".

### Priority
none, low, medium, high, urgent — set it to reflect real urgency. Default to none unless
the user implies otherwise.

### Referring to tasks
Anything that takes a task — every tool argument and every CLI command — accepts its id,
its key (DOTH-84), or its share link (https://dothings.fyi/t/<id>). Pass whichever you have;
there is no need to look up an id first.

### Lead with a summary
Everything you write for people — every task body AND every comment — starts with a plain
1–2 sentence summary of the outcome or the point. Nothing comes before it: no heading, no
bullet, no preamble. Details follow the summary:
- a task body: the summary, then "## Context" and an "## Acceptance criteria" checklist;
- a comment: the summary (what happened, what you decided, or what you need), then the
  supporting detail.

### Writing a task body (markdown)
Bodies are GitHub-flavored markdown, written so a teammate could pick the task up cold:

    Rate-limit the public REST API so one noisy token can't starve everyone else.

    ## Context
    Why this matters / where it came from.

    ## Acceptance criteria
    - [ ] concrete, checkable outcome
    - [ ] another

### Assignees & labels
- Assignees are referenced by username; resolve a person's name via list_members (or the
  members prepare_task returns).
- Every task gets an assignee — an ownerless task is one nobody does; work in progress
  especially needs an owner.
- Labels are project-scoped and must exist before use (create one with create_label).

### Enriching a task before you create it
A one-line request is not a finished task. Call prepare_task (CLI: tasks create --plan)
BEFORE create_task: one read-only call that returns likely open duplicates (scored 0..1),
the project's labels and members, its active cycle, and suggested assignees, labels, due
date and cycle. It writes nothing. Use it instead of separate list_tasks / list_members /
list_labels / list_cycles lookups — and treat its suggestions as a starting point, not a
verdict: apply the rules below on top of them.

1. **Don't duplicate.** Read prepare_task's duplicates (or list_tasks with 2–3 distinctive
   words from the request). If an open task already covers the work, say so and stop —
   comment on it, update it, or bump its priority instead of filing a second card. Related
   but distinct: create it, then link_tasks the two (relates, or duplicates/blocks when
   that's the real relationship).
2. **Always assign.** Stop at the first hit: whoever the request names or implies ("for
   gordo", "@az", "I'll take it", "me" — resolve via the members list) → the area's usual
   owner (who owns the recent tasks carrying the label you're applying; when the same
   member owns two or more, assign them AND the current user — committed work only, since
   assigning notifies people and a raw think_about idea shouldn't ping anyone) → the
   current user.
3. **Tag it — that's your job.** Auto-tagging is an agent rule: prepare_task suggests
   labels but never applies them, so choose them deliberately from the project's own
   vocabulary. Label sets differ per project, so classify the request into concepts and
   apply the most specific EXISTING label covering each (a project with backend/frontend
   doesn't need an invented "code"), max three. Anything touching the codebase —
   implementing, refactoring, fixing, an endpoint, a component, a migration, a test, a
   build, a named file, function or repo — is a code concept and gets labelled as one.
   Create a label only when nothing existing covers the concept, only from the canonical
   set (code, bug, feature, infra, design, docs, ops, growth), at most one per task.
4. **Urgent work gets a date; real work gets a body.** Urgent with no stated deadline is
   due today, high is due by the end of the week; anything past a one-liner gets a body
   that leads with a summary (see "Lead with a summary").
5. **Committed work joins the running cycle.** If the project has an active cycle
   (prepare_task's active_cycle), set_task_cycle the new task onto it — todo, doing and
   in_review only; think_about ideas stay in the backlog.

### Tasks and the code they produce
When the work lives in a connected repo, wire the two together instead of letting them drift:
- Starting a task: move it to doing, make sure it has an assignee, put it in the active
  cycle, and branch using the task's own branch_name (eng-42-slug). The CLI's start
  command does all of that in one step.
- Once a PR exists, link it. CLI: pr opens the PR from the task's branch and links it.
  MCP: link_github_pr(task_id, number) syncs the repo and links PR/issue #number
  (link_github takes an already-synced item's id from list_github_items). A linked PR
  moves the task to in_review when it opens and to finished when it merges, so the board
  tracks the code without anyone maintaining it by hand.

### Reading your inbox
list_inbox (CLI: inbox, or today for your day at a glance) returns your unread
notifications and messages (mentions + replies to you) in one call. Clear them afterwards
with mark_notification_read, mark_all_notifications_read or mark_messages_read.

### Linking tasks
Relationships have a type — pick the one matching the real dependency:
- blocks — source must be done before the target can proceed.
- relates — loosely related.
- duplicates — source duplicates the target.
- implements — source implements the target.
- parent — source is the parent of the target (subtask hierarchy).
A task with an unfinished `blocks` link into it is blocked: every task carries
blocked_by — the blockers' id, key, title and status — and the app marks it. Read it
before starting a task; finishing the blocker clears it. Merging or closing a PR moves a
task only when it's the task's last open PR.

### Polling cheaply
Don't re-read whole threads or boards to notice change. GET /v1/tasks/:id/comments?since=<cursor>
(comments list --since, list_comments since) returns only comments written or edited after the
cursor, plus ids (drop what's missing — it was deleted) and the next cursor. GET /v1/tasks?updated_since=
(tasks list --updated-since, list_tasks updated_since) returns only tasks changed after an instant.

### Searching
list_tasks / tasks list --search / GET /v1/tasks?q= match, across the projects the caller
can see: title, description, any comment, a comment id or link, a linked PR/issue (title,
#number, repo#number, URL), a linked task's key or title (any link type), a linked page's
title, an assignee's name, a label, the task's key or number ("DOTH-126", "126"), or its
id / share link. Case-insensitive substring; the app's search bar has the same reach plus
typo tolerance.

### Resolving a project
Tasks belong to exactly one project. Match the user's words to a project name or slug via
list_projects (get_project for one); if several could match, ask which one.

### GitHub & task links
get_task returns a task's linked github_items (issues AND pull requests), its links
(typed relationships to other tasks) and blocked_by — read them to see what a task depends
on, references, or is waiting on before acting. Every task also carries attachments: image URLs parsed out of
its markdown body (the inline ![](url) images the web composer uploads). To attach an image
yourself — a screenshot as proof, say — `attachments upload <file>` (POST /v1/attachments)
prints the ![](url) markdown; put it in a comment or the task body.
To attach an already-synced issue/PR by its id: list_github_items, then
link_github(task_id, github_item_id) — the item must be in the task's project. Remove one
with unlink_github, passing get_task's github_items[].id. Linking never creates issues or
PRs on GitHub.

### Pages (in-project docs, sheets & tweet drafts)
A project can hold pages — list_pages/get_page to read, create_page/update_page to write,
link_page to attach one to a task. A page's content shape is keyed by its type:
- doc — a TipTap/ProseMirror node: { type: "doc", content: [ … ] }.
- sheet — a table: { columns: [{ id, name, type, options?, width? }], rows: [{ "<columnId>": cell }] };
  a column type is text | number | date | due | updated | checkbox | select | member
  (options lists a select's allowed values; a cell is a string, number, boolean, or null).
- tweet — a draft thread: { tweets: [{ id, text, images? }] }.
update_page REPLACES content, so get_page first and send back the full value. The anchored,
Google-Docs-style comment threads on a doc page are web-only (no REST/CLI/MCP surface).

### Notifications & attribution
You act AS the token's user; tasks, comments and completions are attributed to them.
Assigning, @-mentioning, or replying notifies that person (in-app + push) — don't
bulk-assign or mention more people than intended.

Claude cloud sessions

A Claude cloud session (claude.ai/code, started from your phone) can work the board like any other agent: it needs a token, network access to the API, and the CLI installed when the session starts. Set these on the cloud environment, once.

  1. Network access: Full, or Custom with api.dothings.fyi allowed — the default list doesn't include it.
  2. Environment variables: DOTHINGS_TOKEN=dt_live_… — a token from Settings → Tokens. It acts as you, in the workspace it was made in, for anyone who uses the environment.
  3. Setup script:one line, the same for every repo — an environment's script runs whichever repo the session opens.
curl -fsSL https://dothings.fyi/docs/cloud-setup.sh | bash

It finds the checkout (the runner's working directory isn't it), installs dependencies for whatever lockfile is there, installs the CLI, and checks the token — so a bad token fails here, not mid-task. A repo with its own scripts/cloud-setup.sh gets control instead and owns its setup.

What the script does
#!/bin/bash
# Claude cloud session setup — any repo. https://dothings.fyi/docs#cloud
#
# The runner's working directory isn't the checkout, so find it. A repo that
# has its own scripts/cloud-setup.sh owns its setup and gets control; anything
# else gets dependencies, the dothings CLI and a token check.
set -u

# Everything runs from main, called on the last line: piped through bash, the
# script is read as it runs, and a child that reads stdin (npm does) would
# swallow the rest of it. Parsing the whole function first avoids that, and
# main's stdin is /dev/null so nothing can.
main() {

  repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"
  if [ -z "$repo" ]; then
    # Where cloud runners put a checkout — never all of /, which on a dev
    # machine finds someone else's repo first.
    hit="$(find "$PWD" "$HOME" /workspace /workspaces /repo /src /home -maxdepth 4 -name .git -type d -not -path '*/node_modules/*' 2>/dev/null | head -1)"
    [ -n "$hit" ] && repo="$(dirname "$hit")"
  fi
  if [ -z "$repo" ] || [ ! -d "$repo" ]; then
    echo "!! no checkout found. cwd=$PWD HOME=$HOME"; ls -la "$PWD"; exit 1
  fi
  cd "$repo"
  echo "repo $repo"

  # The repo's own setup, if it has one.
  if [ -f scripts/cloud-setup.sh ]; then
    echo "handing off to scripts/cloud-setup.sh"
    exec bash scripts/cloud-setup.sh
  fi

  # Dependencies, by lockfile. CI=true: no TTY to answer pnpm's prompts.
  export CI=true
  if [ -f pnpm-lock.yaml ]; then
    corepack enable >/dev/null 2>&1 || true
    want="$(node -p "(require('./package.json').packageManager || '').replace(/^pnpm@/, '').split('+')[0]" 2>/dev/null || true)"
    [ -n "$want" ] && corepack prepare "pnpm@$want" --activate >/dev/null 2>&1
    command -v pnpm >/dev/null 2>&1 || npm i -g pnpm >/dev/null 2>&1
    pnpm install --frozen-lockfile || { echo "!! pnpm install failed"; exit 1; }
  elif [ -f bun.lockb ] || [ -f bun.lock ]; then
    command -v bun >/dev/null 2>&1 && bun install --frozen-lockfile || echo "-- bun lockfile but no bun; skipping deps"
  elif [ -f yarn.lock ]; then
    corepack enable >/dev/null 2>&1 || true
    yarn install --immutable 2>/dev/null || yarn install --frozen-lockfile || { echo "!! yarn install failed"; exit 1; }
  elif [ -f package-lock.json ]; then
    npm ci || { echo "!! npm ci failed"; exit 1; }
  elif [ -f package.json ]; then
    npm install || { echo "!! npm install failed"; exit 1; }
  else
    echo "-- no package.json; skipping deps"
  fi

  # The dothings CLI, and proof the token works.
  npm i -g justdothings >/dev/null 2>&1 || echo "!! couldn't install justdothings"
  if [ -z "${DOTHINGS_TOKEN:-}" ]; then
    echo "-- DOTHINGS_TOKEN isn't set: no board access this session"
  elif who="$(justdothings whoami 2>/dev/null)" && [ -n "$who" ]; then
    echo "board: signed in as ${who%%$'\n'*}"
  else
    echo "!! DOTHINGS_TOKEN was rejected, or api.dothings.fyi isn't reachable (network access)"
  fi
}

main </dev/null

The token is the login — never run justdothings login in a session; it needs a terminal to approve a code in. With the skill committed in the repo, a session then knows the CLI without being told.

The data model

Everything lives inside a workspace. A workspace contains projects, and projects contain tasks. Members join the workspace first, then get added to individual projects. Workspace roles (owner › admin › member) control what each person can manage; project roles (admin · member) control task-level access within a project.

A task belongs to one project and sits in one status column. Both the CLI and the MCP tools understand these semantics — the agent gets them in every tool description.

StatusMeaning
think_aboutThink abouta rough idea or maybe; not committed yet
todoTo docommitted and ready to be picked up
doingDoingactively in progress now
in_reviewIn reviewwork is done and awaiting review — a task lands here when its linked PR opens
finishedFinisheddone (or, for a think_about idea, discarded)

Priority: nonelowmediumhighurgent

Link types:
  • blockssource must be done before the target can proceed
  • relatesloosely related
  • duplicatessource duplicates the target
  • implementssource implements the target
  • parentsource is the parent of the target (subtask hierarchy)

A task with an unfinished blocks link into it is blocked: it carries a blocked_by array (the blockers' id, key, title, status), the board shows a red mark with the count, and the ticket opens with the blocker as a card. Finish the blocker and it clears. A task with several pull requests follows the last of them — one merging while another is open moves nothing.

Search — the filter bar on the board and the table matches titles, descriptions and comments and forgives typos; tasks list --search and GET /v1/tasks?q= match the same fields as an exact substring, across the projects you can see.

Task bodies are markdown — a one-line summary, then ## Context and an ## Acceptance criteria checklist works well. Inline images (![](url)) are also surfaced separately on every task as an attachments array of image URLs.

Workspace & team management

Workspace admins can manage membership and projects programmatically — useful for onboarding automation, CI pipelines, or Claude agents that maintain the board.

Projects (admin)
  • Create, rename, set description, archive / unarchive
  • Add or remove members; set per-project role (admin · member)
  • Connect a GitHub repo for issue & PR sync
Workspace membership (admin)
  • List all members with their workspace roles
  • Invite by email; resend or revoke pending invites
  • Promote or demote members (admin · member); owners can also grant owner
  • Remove members from the workspace

All workspace commands require a workspace-scoped token. Rename requires the owner role; all other membership ops require admin.

GitHub integration

Connect a repo to a project and its issues & pull requests sync in — link them to tasks, and finishing a task can close its linked issue. Your workspace installs the dothings GitHub App on your own org or account, and it reads only the repos you grant — everything else stays invisible.

To connect one: open a project → Settings Connect GitHub / add repos. That opens GitHub's repo picker to install the app on your org; pick the repos you want, then choose one back in Settings to start syncing. A fresh signup owns their workspace, so they can do this themselves — workspace admins and project admins can connect repos too.

CLI reference

CommandDescription
justdothings login [--token <t> | --paste] [--base-url <url>]sign in through your browser (approve a code; no token pasting); --token / --paste use an existing token
justdothings logoutremove the stored API token
justdothings whoamicurrent user + accessible projects
justdothings auth status [--json]show the configured base URL + whether a token is set
justdothings auth keyslist your active personal access tokens
justdothings profile set [--display-name]update your own display name
justdothings notifications read <id>mark one notification read
justdothings notifications read-allmark all your notifications read
justdothings notifications read-messages [--comment <id>…]mark mention/reply messages read
justdothings projects list [--search -q]list projects (-q prints slugs only)
justdothings projects get <idOrSlug>show one project
justdothings projects create --name <n> [--description --color --member <id>…]create a project (admin)
justdothings projects rename <idOrSlug> <name>rename a project (admin)
justdothings projects set-description <idOrSlug> <description>set a project's description (admin)
justdothings projects archive <idOrSlug>archive a project (admin)
justdothings projects unarchive <idOrSlug>unarchive a project (admin)
justdothings members list --project <slug>list assignable people
justdothings members add <projectIdOrSlug> <userId>add a member (admin)
justdothings members remove <projectIdOrSlug> <userId>remove a member (admin)
justdothings members set-role <projectIdOrSlug> <userId> <role>set a project member's role (admin)
justdothings teammates listlist everyone in the workspace (admin)
justdothings labels list --project <slug>list labels
justdothings labels create --project <slug> --name <n> [--color <hex>]create a label
justdothings tasks list [--project --status --priority --assignee --me --label --search --updated-since --all -q --compact]list/search tasks (--updated-since <iso> for only tasks changed after it; table hides finished unless --all; --json returns all; -q prints ids only; --compact drops body/attachments from --json)
justdothings tasks mine [--project --status --priority --label --search --all -q --compact]tasks assigned to you (-q prints ids only)
justdothings tasks get <id> [-q]show a task in full (id, key (DOTH-84), or share link)
justdothings tasks create --project <slug> --title <t> [--body|--body-file --status --priority --assignee… --label… --start --due -q]create a task (-q prints the new id only)
justdothings tasks create --project <slug> --title <t> --plan [--body --status --priority --label… --json]dry-run pre-flight, creates nothing: likely duplicates (key/title/score/link) and the suggested assignees/labels/due/cycle it would use; --json prints the raw POST /v1/tasks/prepare plan
justdothings tasks create --project <slug> --title <t> --apply-suggestions [--force …create flags]create, filling ONLY the assignees/labels/due you didn't pass from suggestions and assigning the suggested (active) cycle; if a duplicate scores ≥ 0.6 it prints it and exits 5 without creating (--force to create anyway)
justdothings tasks update <id> [--title --body|--body-file --status --priority --start --due --abandoned --add-assignee… --remove-assignee… --add-label… --remove-label…]update a task (id, key (DOTH-84), or share link)
justdothings tasks update --stdin [--dry-run …same patch flags]bulk: apply one patch to every task ref on stdin (one per line, or a `tasks list --json` array); add/remove deltas resolve per task; prints a line per task + summary, continues past errors, exits 1 if any failed; --dry-run shows what would change
justdothings tasks move <id> <status>move between columns (id, key (DOTH-84), or share link)
justdothings tasks move --stdin <status> [--dry-run --position <n>]bulk: move every task ref on stdin (one per line, or a `tasks list --json` array) to <status>; exits 1 if any failed; --dry-run lists current → new status
justdothings tasks start <id>move a task to doing (id, key (DOTH-84), or share link)
justdothings tasks done <id>move a task to finished (id, key (DOTH-84), or share link)
justdothings tasks open <id>open the task in your browser (id, key (DOTH-84), or share link)
justdothings tasks delete <id> --yesdelete a task (id, key (DOTH-84), or share link)
justdothings comments list <taskId> [--since <iso>]read a comment thread (task: id, key (DOTH-84), or share link; --since: only what changed after that instant; --json adds ids + cursor)
justdothings comments add <taskId> [body] [--body|--body-file --reply-to <id> -q]add a comment (task: id, key (DOTH-84), or share link; -q prints the comment id)
justdothings comments edit <commentId> [body] [--body|--body-file]edit a comment (author/admin)
justdothings comments delete <commentId>delete a comment (author/admin)
justdothings attachments upload <file…> [--json]upload images (PNG/JPG/WEBP/GIF, ≤ 4 MB) and print ![](url) markdown to paste into a comment or task body
justdothings link <src> <tgt> --type <type>link two tasks (each: id, key (DOTH-84), or share link)
justdothings unlink <linkId>remove a task link
justdothings relink <linkId> <type>change a task link's type
justdothings github items <projectIdOrSlug>list a project's synced GitHub issues & PRs
justdothings github link <taskId> <githubItemId>link a GitHub issue/PR to a task (task: id, key (DOTH-84), or share link)
justdothings github link-pr <taskId> <number>sync + link a repo PR/issue #number to a task (task: id, key (DOTH-84), or share link)
justdothings github unlink <taskId> <githubItemId>remove a GitHub item link from a task (task: id, key (DOTH-84), or share link)
justdothings github connect <projectIdOrSlug> <repo>connect a repo + initial sync (admin)
justdothings github disconnect <projectIdOrSlug>disconnect the repo + drop items (admin)
justdothings github sync <projectIdOrSlug>re-pull a project's GitHub items (member)
justdothings github repos listlist repos the workspace can connect (admin)
justdothings github account connectverify YOUR GitHub account via device flow
justdothings github account disconnectunlink your GitHub account
justdothings github account statusshow your linked GitHub account + verification
justdothings start <task> [--no-cycle --no-checkout -q]pick up a task in one step: move to doing, add YOU to assignees (keeps existing ones), put it in the project's active cycle if it has none (--no-cycle skips), and check out its branch (reuses an existing local branch; --no-checkout skips; outside a git repo it warns, not fails); prints the share link. `tasks start` is the plain move
justdothings pr [task] [--title <t> --summary <text> --draft --base <branch>]open a GitHub PR for a task via `gh` and link it: task defaults to the key in the current branch name (doth-84-slug); title `<KEY> <task title>`; body = summary line + `Fixes <KEY>` + share link; pushes with `git push -u origin HEAD` when the branch has no upstream; an existing PR for the branch is just linked. Requires gh
justdothings today [--project <slug>]standup view (alias: standup): your doing + in review tasks, open tasks overdue or due within 3 days, tasks finished in the last 24h (by updated_at, best effort), and unread message count + top 5; --json → { doing, in_review, due_soon, finished_recently, unread_messages: { count, items } }
justdothings inbox [--all --limit <n> --mark-read]unread notifications + mention/reply messages merged newest first (one line per event: when, who, type/kind, task key + title, snippet, link with ?c=<comment>); --all includes read, --limit caps (default 30), --mark-read marks everything read after printing; --json → { notifications, messages }
justdothings branch <task>print a task's git branch name (eng-42-slug; task: id, key (DOTH-84), or share link); --checkout to switch to it
justdothings pages list --project <slug>list a project's pages
justdothings pages get <id>show a page with its full content
justdothings pages create --project <slug> --type <doc|sheet|tweet> [--title --content|--content-file -q]create a page (-q prints the new id only)
justdothings pages update <id> [--title --content|--content-file --archive --unarchive]update a page
justdothings pages delete <id> --yesdelete a page
justdothings pages link <pageId> <taskId>link a page to a task (task: id, key (DOTH-84), or share link)
justdothings pages unlink <pageId> <taskId>remove a page ↔ task link (task: id, key (DOTH-84), or share link)
justdothings cycles list --project <slug> [-q]list a project's cycles with progress (-q prints ids only)
justdothings cycles create --project <slug> --name <name>create a cycle (--status --start --end; -q prints the new id only)
justdothings cycles update <id>update a cycle (--name --status --start --end; pass 'none' to --start/--end to clear a date)
justdothings cycles delete <id> --yesdelete a cycle (its tasks are detached)
justdothings cycles assign <task> <cycleId>assign a task (id, key (DOTH-84), or share link) to a cycle ('none' to detach)
justdothings workspace rename <name>rename the workspace (owner)
justdothings workspace members listlist workspace members (member)
justdothings workspace members set-role <userId> <role>set a workspace member's role (admin)
justdothings workspace members remove <userId>remove a workspace member (admin)
justdothings workspace invites send <email> [--role]invite someone to the workspace (admin)
justdothings workspace invites resend <id>resend a workspace invite (admin)
justdothings workspace invites revoke <id>revoke a workspace invite (admin)
justdothings mcprun the stdio MCP server
justdothings install-mcp [--client claude-code|claude-desktop|cursor --with-token]print the MCP config snippet (no token unless --with-token; claude-code also prints the `claude mcp add` one-liner)
justdothings install-skill [--global --print --skill <cli|mcp|all>]install or refresh the Claude Code skills (dothings-cli + dothings-mcp) into the repo's .claude/skills, or ~/.claude/skills with --global
justdothings docsopen the documentation in your browser
justdothings completion <bash|zsh|fish>print a shell completion script (e.g. eval "$(justdothings completion zsh)")

Every command takes --json.

MCP tools

ToolWhat it does
whoamithe authenticated user + accessible projects
update_profileupdate your own display name
list_api_keysyour active personal access tokens (self)
mark_notification_readmark one of your notifications read
mark_all_notifications_readmark all your notifications read
mark_messages_readmark mention/reply messages read
list_inboxyour notifications + mention/reply messages in one call (unread by default)
list_projectslist/resolve projects (by name or slug)
get_projectone project by id or slug
create_projectcreate a project (admin)
update_projectrename / set description (admin)
list_teammateseveryone in the workspace (admin); ids feed add_member
add_memberadd a teammate to a project (admin)
remove_memberremove a teammate from a project (admin)
list_taskslist/search tasks with filters (search covers title, description and comments; compact by default; limit; updated_since)
prepare_taskread-only pre-flight before create_task: duplicates, labels, members, active cycle, suggestions
get_taskone task in full (body, assignees, labels, linked GitHub issues/PRs, task links, image attachments)
create_taskcreate a task in a project
update_taskpatch a task; assignees/labels replace the set, add_*/remove_* apply deltas
move_taskmove a task to a status column
delete_taskpermanently delete a task
add_commentadd a markdown comment (supports @mentions, replies)
edit_commentedit a comment (author or admin)
delete_commentdelete a comment (author or admin)
list_commentsread a task's comment thread
list_labelslabels in a project
create_labelcreate a project label
list_memberspeople assignable in a project
link_taskscreate a typed link between two tasks
unlink_tasksremove a task link by id
update_task_linkchange a task link's type
list_github_itemslist a project's synced GitHub issues & PRs
link_githublink a GitHub issue/PR to a task
link_github_prsync + link a PR/issue to a task by its number
unlink_githubremove a GitHub item link from a task
list_pageslist a project's pages (docs, sheets, tweet drafts)
get_pageone page in full, incl. its typed content
create_pagecreate a page (doc|sheet|tweet) in a project
update_pageupdate a page's title / content / archived flag
delete_pagepermanently delete a page
link_pagelink a page to a task (same project)
unlink_pageremove a page ↔ task link
connect_github_repoconnect a repo "owner/repo" + initial sync (admin)
disconnect_github_repodisconnect the repo + drop synced items (admin)
sync_githubre-pull a project's GitHub issues & PRs (member)
archive_projectarchive or unarchive a project (admin)
set_project_member_roleset a member's per-project role: "admin" or "member"
rename_workspacerename the token's workspace (owner)
list_workspace_memberslist workspace members with roles (member)
set_workspace_member_roleset a member's workspace role (admin)
remove_workspace_memberremove a member from the workspace (admin)
invite_workspace_memberinvite someone to the workspace by email (admin)
resend_workspace_inviteresend a pending invite email, refresh expiry (admin)
revoke_workspace_invitecancel a pending workspace invite (admin)
list_installable_reposrepos the workspace can connect via GitHub App (admin)
list_cycleslist a project's cycles (sprints) with progress counts
create_cyclecreate a cycle in a project
update_cycleupdate a cycle's name/status/dates
delete_cycledelete a cycle (its tasks are detached)
set_task_cycleassign a task to a cycle (or detach)

REST API

Base https://api.dothings.fyi — endpoints are under /v1. Authenticate with Authorization: Bearer dt_live_….

curl -H "Authorization: Bearer dt_live_…" \
  https://api.dothings.fyi/v1/whoami
MethodPath
POST/v1/auth/device
start a CLI browser login — unauthenticated, rate-limited per IP { token_hash, token_prefix?, client_name? } → { user_code, verification_uri, verification_uri_complete, expires_in, interval }
GET/v1/whoami
current user + accessible projects
PATCH/v1/whoami
update your own profile { display_name? }
POST/v1/whoami/github/device
start device-flow GitHub account linking (self)
POST/v1/whoami/github/device/poll
poll a pending device authorization { device }
DELETE/v1/whoami/github
disconnect your GitHub account (self)
POST/v1/previews
record a successful preview deploy { repo, branch, url, sha, message?, pr_number? } as an In review task holding the link (one per branch, or the task the branch is named after), link the PR, and push it to your devices
POST/v1/whoami/push
send a web push to YOUR own devices { title, body?, url (https), tag? } — for automations like preview-deploy alerts (self)
GET/v1/api-keys
your active personal access tokens (self)
GET/v1/teammates
everyone in the workspace (admin)
GET/v1/notifications?unread=&limit=
your notifications, newest first (self; unread=1 for unread only; limit ≤ 200, default 50) → { notifications: [{ id, type, read, created_at, actor, task: { id, key, title, project_id } | null, comment_id, snippet }] }
GET/v1/messages?unread=&limit=
comments directed at you — mentions + replies, one per comment, newest first (self; unread=1; limit ≤ 200, default 50) → { messages: [{ comment_id, kind, read, created_at, author, task, snippet }] }
POST/v1/notifications/:id/read
mark one notification read (self)
POST/v1/notifications/read-all
mark all notifications read (self)
POST/v1/messages/read
mark mention/reply messages read { comment_ids? }
GET/v1/projects?q=
list projects
POST/v1/projects
create a project (admin) { name, description?, color?, member_ids? }
GET/v1/projects/:id
one project (id or slug)
PATCH/v1/projects/:id
rename / set description / archive (admin) { name?, description?, archived? }
GET/v1/projects/:id/members
assignable people
POST/v1/projects/:id/members
add a member (admin) { user_id }
DELETE/v1/projects/:id/members?user_id=
remove a member (admin)
PATCH/v1/projects/:id/members/:userId
set a member's project role (admin) { role }
GET/v1/projects/:id/labels
labels
POST/v1/projects/:id/labels
create a label { name, color? }
POST/v1/projects/:id/github
connect a repo (admin) { repo }
DELETE/v1/projects/:id/github
disconnect the repo (admin)
POST/v1/projects/:id/github/sync
re-pull GitHub items (member)
GET/v1/tasks?project=&status=&priority=&assignee=&label=&q=&updated_since=&limit=&offset=&compact=
list/search tasks (q matches title, description and comments; updated_since=<ISO> for only tasks changed after it; paginated: total/has_more/limit/offset; limit ≤ 500, default 200; compact=1 omits body/attachments)
POST/v1/tasks
create a task
POST/v1/tasks/prepare
READ-ONLY pre-flight for create { project, title, body?, status?, priority?, labels?[] } → likely open duplicates (scored 0..1), the project's labels/members/active cycle, and suggested assignees/labels/due_date/cycle_id; writes nothing
GET/v1/tasks/:id
one task, fully hydrated (:id = task id, key like ENG-42, or share link — same for every /tasks/:id route)
PATCH/v1/tasks/:id
update fields; assignees/labels replace (:id = id, key, or share link)
POST/v1/tasks/:id/move
move to a status column { status, position? } (:id = id, key, or share link)
PUT/v1/tasks/:id/assignees
replace assignees { assignees[] } (:id = id, key, or share link)
PUT/v1/tasks/:id/labels
replace labels { labels[] } (:id = id, key, or share link)
DELETE/v1/tasks/:id
delete a task (:id = id, key, or share link)
GET/v1/tasks/:id/comments?since=
comment thread (:id = id, key, or share link); with since=<ISO> only what changed after it, plus ids (still present) and cursor
POST/v1/tasks/:id/comments
add a comment { body, reply_to_id? } (:id = id, key, or share link)
PATCH/v1/comments/:id
edit a comment (author/admin) { body }
DELETE/v1/comments/:id
delete a comment (author/admin)
POST/v1/attachments
upload an image (multipart `file`, PNG/JPG/WEBP/GIF ≤ 4 MB) → { url, markdown } to embed in a comment or task body
POST/v1/task-links
link tasks { source_task_id, target_task_id, type } (each an id, key, or share link)
PATCH/v1/task-links/:id
change a link's type { type }
DELETE/v1/task-links/:id
remove a link
GET/v1/projects/:id/github-items
list synced GitHub issues & PRs
POST/v1/github-links
link a GitHub item to a task { task_id, github_item_id } (task_id = id, key, or share link)
DELETE/v1/github-links?task_id=&github_item_id=
remove a GitHub item link (task_id = id, key, or share link)
GET/v1/pages?project_id=
list a project's pages (summaries)
POST/v1/pages
create a page { project_id, type, title?, content? }
GET/v1/pages/:id
one page with full content
PATCH/v1/pages/:id
update a page { title?, content?, archived? }
DELETE/v1/pages/:id
delete a page
POST/v1/page-links
link a page to a task { page_id, task_id } (task_id = id, key, or share link)
DELETE/v1/page-links?page_id=&task_id=
remove a page ↔ task link (task_id = id, key, or share link)
GET/v1/cycles?project_id=
a project's cycles with task/done counts
POST/v1/cycles
create a cycle { project_id, name, status?, start_date?, end_date? }
PATCH/v1/cycles/:id
update a cycle { name?, status?, start_date?, end_date? }
DELETE/v1/cycles/:id
delete a cycle (tasks' cycle cleared)
PATCH/v1/workspace
rename the workspace (owner) { name }
GET/v1/workspace/members
list workspace members (member)
PATCH/v1/workspace/members/:userId
set a member's workspace role (admin) { role }
DELETE/v1/workspace/members/:userId
remove a workspace member (admin)
POST/v1/workspace/invites
invite a member by email (admin) { email, role? }
POST/v1/workspace/invites/:id/resend
resend a pending invite (admin)
DELETE/v1/workspace/invites/:id
revoke a pending invite (admin)
GET/v1/workspace/github
list repos the workspace can connect (admin)

Pointing an LLM at dothings? Give it /docs/llms.txt — the whole API, CLI, and MCP surface in one plain-text file.