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 --versionRequires 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 machineBoth 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.mdPreview 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  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  images the web composer uploads). To attach an image
yourself — a screenshot as proof, say — `attachments upload <file>` (POST /v1/attachments)
prints the  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  images the web composer uploads). To attach an image
yourself — a screenshot as proof, say — `attachments upload <file>` (POST /v1/attachments)
prints the  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.
- Network access: Full, or Custom with
api.dothings.fyiallowed — the default list doesn't include it. - 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. - 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 | bashIt 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.
| Status | Meaning |
|---|---|
| think_aboutThink about | a rough idea or maybe; not committed yet |
| todoTo do | committed and ready to be picked up |
| doingDoing | actively in progress now |
| in_reviewIn review | work is done and awaiting review — a task lands here when its linked PR opens |
| finishedFinished | done (or, for a think_about idea, discarded) |
Priority: nonelowmediumhighurgent
blocks— source must be done before the target can proceedrelates— loosely relatedduplicates— source duplicates the targetimplements— source implements the targetparent— source 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 () 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.
- Create, rename, set description, archive / unarchive
- Add or remove members; set per-project role (admin · member)
- Connect a GitHub repo for issue & PR sync
- 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
| Command | Description |
|---|---|
| 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  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)") |
Every command takes --json.
MCP tools
| Tool | What it does |
|---|---|
| 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) |
| 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) |
| 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 |
| 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 |
| list_labels | labels in a project |
| create_label | create a project label |
| list_members | people assignable in a project |
| 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 |
| 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 |
| 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 |
| 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" |
| 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) |
| 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) |
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| Method | Path |
|---|---|
| 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.