Install
openclaw skills install @robbeverhelst/do-cliInstall and drive do, the agent-drivable CLI for the do personal todo / life-tracking app. do is a thin client over the app's public REST API (/api/v1) with per-tenant API-key auth — capture tasks, see what's actionable today, complete/snooze tasks, log habits, and read entities (tasks, habits, notes, contacts, recipes, …). Every destructive verb (delete, rm, remove, revoke, detach, unlink) is refused unless the call passes --allow-destructive, so an autonomous agent can't destroy data by accident. Use when the user wants to manage their todos / life-tracking from a terminal or agent, mentions the do CLI, or asks to capture or review tasks and habits.
openclaw skills install @robbeverhelst/do-clido is a thin client over the do app's public REST API (/api/v1). It
inherits the same per-tenant auth, validation, and row-level-security wall as the
web app — there is no direct DB access. The v1 surface is the daily loop:
capture, today, complete/snooze a task, log a habit, and read entities. Every
destructive verb (delete, rm, remove, revoke, detach, unlink) is
refused unless you pass --allow-destructive, so it is safe to hand to an
autonomous agent: see Destructive verbs.
The live, authoritative surface is always
do help --json. This skill is the orientation; when in doubt, introspect the binary — the manifest is generated from the same registry the commands dispatch from, so they can't drift.
bunx @robbeverhelst/do <command>.npm i -g @robbeverhelst/do (or bun add -g @robbeverhelst/do), then run do <command>.do-<os>-<arch> asset from a GitHub Release
(https://github.com/robbeverhelst/do/releases) and put it on your PATH.Requires Bun ≥ 1.3 for the bunx/binary paths (bun --version; install with
curl -fsSL https://bun.sh/install | bash).
Every call needs a per-tenant API key, sent as Authorization: Bearer <key>.
Keys are minted by the app (the JWT-guarded POST /api-keys endpoint, e.g. from
the web app's settings); the CLI only consumes a key — it never creates one. A
key only reaches routes whose scope it holds, so grant the scopes for the verbs
you need (tasks:read/tasks:write, habits:write, …).
Config resolves in precedence order (first wins):
--api-key / --url flagsDO_API_KEY / DO_API_URL environment variables ← the agent / CI path~/.config/do/config.json ← written by do login / do config set, for humansThe hosted instance is https://do.robbe.work/api — so you only need a key:
# Agent / CI: just export the env vars.
export DO_API_KEY=do_xxx
export DO_API_URL=https://do.robbe.work/api # hosted instance (see note below)
# Human: verify a key and save it (file is written 0600).
do login --api-key do_xxx --url https://do.robbe.work/api
do whoami # prove the key resolves to a tenant + scopes
do config show # resolved config, key redacted, with sources
The URL ends in /api because the API sits behind the web app's same-origin
/api proxy (the CLI appends /api/v1 itself). For a self-hosted instance, use
https://<your-host>/api; for local dev, http://localhost:3001. Only the
API key is per-user — ask the user for a scoped key.
--json forces JSON; output is automatically JSON when stdout is piped
(non-TTY), so an agent always gets parseable output. A TTY gets pretty,
colored text (honoring NO_COLOR, --quiet, --verbose).
Failures print a JSON error envelope to stdout in JSON mode
({ "error": { "code", "message" } }) and a non-zero exit code:
| exit | meaning |
|---|---|
| 0 | success |
| 1 | runtime / network / unexpected HTTP |
| 2 | usage: unknown command, flag or field; a destructive verb without the flag |
| 3 | config / auth (no key, 401, 403) |
| 4 | not found (404) |
| 5 | validation (400) |
do capture quick-adds a task to the inbox from free text with inline tokens:
#tag — adds a tag (repeatable): #home #urgent! — stars the task (the 1–2 things that matter); !star / !s / !flag / !f too~area — files it into an existing Area by its title, case-insensitive, one word
(~garden); an unknown name files nothing. Valid names: do areas list --jsontomorrow 3pm, fri, next week, in 2 days, yesterday; a time makes it
timed (on the calendar); every monday attaches a recurrenceby friday, due fri 5pm (never the plan)30m, 1h, 1h30, 2 hoursAll are stripped from the title; standalone only (so C# or 10! in prose is
left alone). --at <ISO>, --due <ISO> and --duration <minutes> win over the text.
do help capture --json has the full grammar:
do capture "Email the landlord about the leak #home !"
# → task "Email the landlord about the leak", tag home, starred
do capture "File the taxes by friday 2h #admin"
# → Deadline Friday, estimate 2 hours
# Capture an idea into the inbox
do capture "Draft Q3 plan #work !"
# See what's actionable today (overdue, scheduled, due, in-progress), then the
# habits and routines due today under `.habits`
do today
# From earlier (#888): planned tasks of the last 14 days that weren't done —
# never "late". Answer each with today, later (the quiet day later this week),
# someday (no date, kept in its Area/Project) or let-go (cancelled, not Landed).
do today earlier
do today answer later 01J9Z2K7Q8ABCDEF0123456789 01J9Z2K7Q8ABCDEF0123456790
# Complete a task (idempotent — a no-op success if already done)
do task done 01J9Z2K7Q8ABCDEF0123456789
# Snooze a task forward (default +1 day, or an explicit target)
do task snooze 01J9Z2K7Q8ABCDEF0123456789 --days 3
do task snooze 01J9Z2K7Q8ABCDEF0123456789 --until 2026-07-01
# Plan it at a time so it shows on the calendar
do task schedule 01J9Z2K7Q8ABCDEF0123456789 --at 2026-09-09T16:00:00+02:00 --duration 60
# Log a habit check-in (value defaults to 1; --at for a past time)
do habit log 01J9Z2K7Q8ABCDEF0123456789
do habit log 01J9Z2K7Q8ABCDEF0123456789 --value 30 --at 2026-06-18T21:00:00Z
# Place a habit in the day: morning|midday|evening or "HH:MM" (null = any time)
do habits update 01J9Z2K7Q8ABCDEF0123456789 --data '{"timeOfDay":"morning"}'
# Check in by title or alias, the way a chat message says it ("wake", "12 pages")
do habit check-in wake
do habit check-in pages --value 12 # adds 12 to today
do habit check-in pages --value 40 --set # makes today's total 40
do habit check-in drill --at 2026-10-15 # back-date to a day (your time zone)
do habit check-in drill --note "felt heavy" # "drill done, felt heavy": the rest is a note
# Add or change a note afterwards (the check-in id is lastCheckIn.id in the reply,
# or from `do habit check-ins <id>`); --clear drops it
do habit note 01J9Z2K7Q8ABCDEF0123456789 01J9Z2K7Q8ABCDEF0123456790 "Dune, chapter 9"
# A 409 lists the candidates (error.details.candidates): ask which, then `do habit log <id>`.
do habit alias add 01J9Z2K7Q8ABCDEF0123456789 wake
# What's left today, by part of day (morning, midday, evening, anytime), + routines
do habits today
# Skip and pause, without guilt: a skip or a pause is never a miss
do habit skip 01J9Z2K7Q8ABCDEF0123456789 # "skip gym today"
do habit skip 01J9Z2K7Q8ABCDEF0123456789 --note "Dentist" # "skip gym, dentist": the why shows in the log
do habit pause 01J9Z2K7Q8ABCDEF0123456789 --until 2026-10-26 # "pause gym till the 26th"
do habit resume 01J9Z2K7Q8ABCDEF0123456789 # "gym's back on"
do habit remind 01J9Z2K7Q8ABCDEF0123456789 on # "remind me about my drill" (off by default)
do away --until 2026-10-19 # "away till Monday"
do away off # "I'm back"
# A batch (the Apple Medications model): a routine whose steps are habits, e.g.
# "AM Supplements: magnesium, D3, creatine". One call checks in every open step.
do routine done 01J9Z2K7Q8ABCDEF0123456789 # "took my supplements"
do routine undo <run.id> <taken stepRunIds…> # takes the batch back
# Retire a habit to the Grown shelf (history kept), or bring it back
do habit retire 01J9Z2K7Q8ABCDEF0123456789 # only after they say yes
do habit unretire 01J9Z2K7Q8ABCDEF0123456789 # "bring back flossing", or Undo
do habits list --retired # the Grown shelf
# Read entities: `do <entity> list` / `do <entity> get <id>`
do tasks list --filter unsorted # the Inbox: no day, Deadline, Area or Project; never a proposal
do tasks list --proposed # Proposed by Echo: waiting for the member's Accept
do tasks list --area <areaId> --status todo,in_progress --tag errand --due-before 2026-10-12
do tasks list --updated-since 2026-10-11T08:00:00Z # what changed since a sync
do areas list --active # the live Areas to file into (archived ones are put away)
do areas overview # how each Area is going, in words (see "Areas" below)
do habits list # each carries today { due, done, week? } (on today's schedule?)
do habits list | jq '.data[] | select(.today.due) .title' # what's on today
# "How are my habits going?" — this week's grid, a strength word per habit, patterns
# from the insights worker (each with its confidence) and when you usually check in
do progress habits
do notes get 01J9Z2K7Q8ABCDEF0123456789
# Pipe JSON into jq (auto-JSON because stdout is piped)
do today | jq '.items[] | select(.bucket == "overdue") .title'
# Sync: what changed since your last cursor (store .nextCursor, rerun while .hasMore)
do changes --since now # a fresh cursor, no history
do changes --since 812345-12 --entity task,project
Readable entities: tasks, habits, occurrences, shopping-lists,
inventory-items, recipes, meal-plans, notes, contacts, webhooks.
Any command whose verb is delete, rm, remove, revoke, detach or
unlink (and forms like kind-remove, unlink-task) is refused without
--allow-destructive: exit 2, error.code: "destructive_blocked", and no
request is sent. The message says what to add. do help <command> lists the
flag on each of them.
archive puts a row in Trash
without the flag.do health unlog,
do habit undo, do money goal-uncount and do tasks unblock run as they are.confirmation_required): don't retry it, tell them it waits for them.do challenges rm <id> # → exit 2: "add --allow-destructive to run it"; nothing sent
do challenges rm <id> --allow-destructive # after the member said yes
do tasks archive <id> # recoverable: to Trash, no flag
do views delete "Old view" --allow-destructive
Changed in 2.0.0: before, 18 commands deleted without the flag (challenges rm, views delete, health share revoke, echo memory rm, …). A script that
runs one of them now adds --allow-destructive.
Run do help for the human overview, or do help --json for the complete
machine-readable manifest — every command, argument, flag, required scope, return
type, the read entities, auth resolution, and exit codes. Introspect that instead
of trusting this skill when the surface may have grown:
do help --json | jq '.commands[] | {command, scopes, returns}'
do help --json | jq '.entities[].name'
Source + full docs: https://github.com/robbeverhelst/do
When a check-in message carries more than the habit, keep the rest as the
check-in's note (at most 280 characters): "drill done, felt heavy" →
do habit check-in drill --note "felt heavy"; "read 12 pages of Dune, chapter 9" →
do habit check-in pages --value 12 --note "Dune, chapter 9". The note shows in the
habit's log beside "Echo, 07:12" and on its day in the year view. To add one later
("add a note: legs were sore"), use do habit note <habit id> <check-in id> "…"
with the reply's lastCheckIn.id; --clear removes it. Don't invent a note when
the message has none.
When someone says they'll rest from a habit, use these verbs. Never call a skip or a pause a miss, and never nag about one.
| They say | Run |
|---|---|
| "skip gym today", "rest day for reading" | do habit skip <id> (find the id with do habits list) |
| "skipped the run yesterday" | do habit skip <id> --date <yesterday, YYYY-MM-DD> |
| "pause gym till the 26th", "no gym for two weeks" | do habit pause <id> --until <the day it's back> |
| "gym's back on", "resume reading" | do habit resume <id> |
| "remind me to do my drill", "stop reminding me about water" | do habit remind <id> on / off: one push a day at its time, never a follow-up |
| "away till Monday", "on holiday until the 3rd" | do away --until <the day they're back> |
| "I'm back", "come back early" | do away off |
--until is the day it's back (the first day it counts again), and must be
after today. Resolve "Monday" or "two weeks" to a date first; "till Monday" means
back on Monday.do habit check-ins <id> with kind: "skip"; undo it by
deleting that entry. A why ("dentist") goes in --note. Resting habits carry pause: { until, by }.A batch is a routine whose steps are habits ("AM Supplements: magnesium, D3, creatine"). When someone says they took or did the whole thing, check it in with one call, not one habit at a time.
| They say | Run |
|---|---|
| "took my supplements", "supps done", "took my AM supplements" | do routine done <routine id> |
| "took everything except the creatine" | do habit skip <creatine id>, then do routine done <routine id> |
| "skip the creatine today" | do habit skip <creatine id> (the batch leaves it alone) |
| "undo that", "I didn't take them yet" | do routine undo <run.id> <stepRunIds…> from the answer |
do habits today (routines, with their ids). With an AM
and a PM batch and no word for which, take the one due now: AM before noon, PM
after. Ask only if both are still open and it's unclear.do habit check-in "am supplements" instead.do routine done answers with taken (what it checked in). A habit already done
today isn't counted twice, a skipped one stays skipped, and saying it again
changes nothing. Reply in words ("Magnesium, D3 and creatine are in."), with no
counts or streaks.Retiring is not deleting. A habit that just happens now (or that no longer fits) moves to the Grown shelf with its plant in bloom and keeps all its history. It leaves Now and Today, sends no reminders and is never due; Progress keeps its past.
| They say | Do |
|---|---|
| "I'm done with the cold showers habit", "stop tracking flossing" | Offer first: "Retire Cold shower? It moves to Grown with its history." On a yes, do habit retire <id> |
| "bring back flossing", "track journaling again" | do habit unretire <id> |
| "what have I grown?", "which habits did I retire?" | do habits list --retired |
do habit unretire <id>.During a review the person asked for (the Weekly Review, "let's review my week"),
read do progress habits --json: each habit has quietWeeks, the finished weeks
in a row it was done on less than half of what was due. For a habit at 4 or
more, you may ask once, gently: "Reading has been quiet for a month. Still right
for you? You could keep it, pause it, or retire it to Grown." Then do what they
choose (do habit pause, do habit retire, or nothing). Never raise this outside
a review they started, never as a nudge, and never with numbers as pressure.
An Area can say what it's for (purpose, one line) and what "fine" looks like there
(standards, up to five plain sentences). A standard held by a habit, a recurring
task or a Metric says holding or worth a look with the reason in words; one
with no link is checked by hand in review. Never a score, never a percentage.
do areas get <id>: its habits, routines and recurring tasks) and offer a short
list with what would hold each.do areas standards add (the v1 endpoint). Each add shows in their change list
with Undo. Never write standards, or a purpose, into an Area unasked, and never
reword or remove theirs without being told to.do areas standards list <id> and answer in its words
("Move three times a week is worth a look: one so far this week"). Don't turn it
into a grade.do areas get 01J9Z2K7Q8ABCDEF0123456789
do areas standards add 01J9Z2K7Q8ABCDEF0123456789 "Move three times a week" --habit 01J9Z2K7Q8ABCDEF0123456790
do areas standards list 01J9Z2K7Q8ABCDEF0123456789
An Area is an ongoing responsibility (Health, House, robbeverhelst); it never finishes. Answer about Areas in words, never a score, and change one only when the member asks. The member's own Areas are theirs: never rest, archive, merge or convert one on your own; propose, then act on their yes.
do areas overview gives each
Area's attention against its own usual (Busier than usual, Steady, Quiet for
three weeks, Resting) with a reason, what's moving and what waits on them (Mine
by default; --owner all counts agents' work too). Name the quiet ones and any
standard worth a look (do areas standards list <id>), never a grade. Quiet is
a fact, not a fault.do progress (or --days 30). Its
areasHighlight is one sentence to use as is ("Most of your own week went to
Health and House. Wedding was busier than the week before."); the per-Area counts
(yours, agents', check-ins, blocks) are fine to quote here, the one place counts
belong.do areas focus <areaId>. Now, Tasks and
Calendar narrow to that Area until the end of their day, under a quiet bar; it
clears itself. "Show everything" → do areas focus --off. The panel's page
context tells you when they're focused. Never focus them unasked.do areas since <id> for what landed, what's new and what seems stuck
(in words), then ask the three questions (still what fine looks like? anything
stuck? anything to let go?) with buttons. do areas looked-at <id> only when they
say they've looked. No reminders, no "review overdue".do areas rest <id> --until <day> (add --pause-habits if they want its habits paused too). Its recurring
tasks pause, Focus, Review and filing leave it alone. do areas back <id> brings
it back early.do areas make-project <id> --into <areaId>
and keep the receipt it prints: do areas undo-make-project <id> --data '<receipt>' puts everything back./areas/<id>), then the Area of a Project the
task belongs to.do areas overview
do progress --days 7
do areas focus 01J9Z2K7Q8ABCDEF0123456789
do areas since 01J9Z2K7Q8ABCDEF0123456789
do areas rest 01J9Z2K7Q8ABCDEF0123456789 --until 2027-01-15 --pause-habits
When you are the Space's agent (your key has agents:converse and acts as the Agent the
Space's agent connection names), post what you'd
tell the member into their thread with do agent post (or do echo post):
--thread it goes to your home thread with the Space's first owner.
--thread build names one of their standing threads; an unknown slug is
thread_not_found. Suggest a new standing thread to them, never make one.needs_you only when it waits on
them, told when they asked to be told, for_later for things to read later,
silent when there's nothing to say.--kind card under one stable --external-id (card:repos) and post it again
on each change: it edits in place instead of adding a message.do agent post --thread build --kind card --external-id card:repos "15 repos. Nothing needs you."
do agent post --thread build --level needs_you "do #861 is ready for your review."
When you work with an Agent's key (you are Echo, or a desk of Echo's) and make a
task with a member as Owner ("owner":"member:robbe@example.com"), it is
Proposed by Echo: it waits at the top of their Inbox with Accept / Not now / Let
go, and stays off their Today, Now and lists until they accept. Your own work
("owner":"me/finance") is never a proposal.
source.label ("From the KBC statement email") and the Desk you
write from in fromDesk (finance). The Inbox shows the reason as the why.scheduledAt, dueAt, areaId); Accept keeps
them.proposed:false from an untrusted Desk is refused (403).do tasks accept <id>; "not now" →
do tasks dismiss <id> (back in their Inbox in a week); "drop it" → do task cancel <id> (Let go; asks them to confirm first). Your accept shows in their change list
with Undo.do tasks list --proposed.do tasks create --data '{"title":"Pay the dentist invoice","owner":"member:robbe@example.com","fromDesk":"finance","dueAt":"2026-10-21","source":{"kind":"email","label":"From the Tandarts Peeters email, 112 euro"}}'
do tasks list --proposed
do tasks accept 01J9Z2K7Q8ABCDEF0123456789
When the member mentions someone in passing ("talked to Sam over the fence, he
started a new job, his moka pot broke"), write it as one do people note for
that person: each thing it means is an item. Items: moment (life_update,
seen, talked, favour_done, …; sensitive:true for health, a pregnancy before
it's public, money trouble), gift_idea (title), follow_up (label, date:
"ask how the surgery went" two days after), key_date (birthday or
anniversary, date, yearKnown:false when there's no year). Post the receipt it
returns as one card with Undo (do people undo <id>), Edit, and for a
sensitive item Don't keep the detail (do contact-moments update <id> --data '{"summary":null}').
contactId, or by person ("Sam", "Sam's sister", or "my
sister" when the member says it: "my" is the member whose key you hold). Several
matches come back as a conflict listing the candidates: ask once ("Sam Example
or Sam from work?") and send it again with the contactId. No match: ask whether
to add them, then do contacts create "<name>" with what you know as flags
(--email, --phone, --birthday YYYY-MM-DD, --circles <id>, --notes). Fix a
detail with do contacts update <id> --phone "+32 …"; none clears one.do people brief <contact id> gives last seen, the latest life updates, open follow-ups and
gift ideas, in one line.seen moment for a calendar event.rhythmSnoozedUntil without comment.do people note --data '{"person":"Sam","items":[{"type":"moment","kind":"life_update","summary":"started a new job"},{"type":"gift_idea","title":"Moka pot"},{"type":"moment","kind":"seen"}]}'
do people undo 01J9Z2K7Q8ABCDEF0123456789
do people find "Sam's sister"
do people brief 01J9Z2K7Q8ABCDEF0123456789
do contacts update 01J9Z2K7Q8ABCDEF0123456789 --data '{"rhythmSnoozedUntil":"2026-11-01"}'
Only with a key the member opened Family tree for (people:family-tree); any
other key gets a 403. The tree names relatives who never chose to be in do: never
repeat a relative's details outside the member's own thread, and never turn an
ancestor into a Contact.
do people tree import <file.ged> (with
--source-name / --source-email when you know who sent it).do people tree list <name> to find the ref, then do people tree me <ref>.do people tree matches lists what the tree noticed. Ask the member about each
one ("Is Nora in the tree your Contact Nora?") and only then --accept <id> or
--reject <id>. A no is never offered again.do people tree import family.ged --source-name "Aunt Test"
do people tree me I12
do people tree matches --accept suggest:I2:01J9Z2K7Q8ABCDEF0123456789
When the member asks "plan my week" (or on their planning evening), propose it with
do plans propose rather than moving tasks yourself. Nothing is written until they
accept it on Review › Weekly (or tell you "yes" in chat). Contract and payloads:
docs/agents/plan-proposals.md.
kind:"week" with startsOn the Monday (or kind:"day" with the day). One item
per task: day (a planned day, their zone), or at (a time, ISO, inside the week)
with durationMinutes when the task has an estimate; action:"someday" or
action:"let_go" for what should leave the week. Give each a short reason.do tasks list --drifted and ask about those once.kind:"day" and startsOn today: it shows
as a preview at the top of their Now (Accept / Not today).do plans accept <id>; "not that one" →
do plans dismiss <id> <itemId>; "undo" → do plans undo <id>.do plan rules list --for <userId> (fixed times,
keep-free windows, what a word means, whose something is, how often). Keep them;
the planner anchors fixed times and leaves keep-free windows empty on its own.do plan rules add "<their words>" --rule '<json>' (or no --rule for a plain
note), then post a receipt with its id (receipt: { nodeId }) so they can Undo it.
One rule per thing they said; don't invent rules they didn't state.
Payloads: docs/agents/planning-rules.md.do plans propose --data '{"kind":"week","startsOn":"2026-10-19","note":"A light week: the domain first.","items":[{"taskId":"01J9Z2K7Q8ABCDEF0123456789","day":"2026-10-20","reason":"Deadline Friday"}]}'
do plans list --kind week --status open
Your own tasks ("owner":"me/finance") live in the member's Tasks › With Echo,
never in their Today. Each reads as one word: Working on it (open), Waiting on
you / Waiting on someone (Waiting on set), Ready for you, Done today.
do tasks ready <id> --note "Draft ready: 38 hours, 3,420 euro — <link>".
Put the link in the note (or in source.url / a worktree link); the member's
Review opens it. It reaches their Now under Needs you.waitingOn to
them (Waiting on you).do tasks list --ready, or one desk's
work with do tasks list --owner agent:Echo/finance.do tasks ready 01J9Z2K7Q8ABCDEF0123456789 --note "Recap drafted — https://docs.google.com/document/d/…"
do tasks list --ready --owner agent:Echo/clients