Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Manage Todoist tasks, projects, labels, filters, sections, comments, reminders, and workspaces via the `td` CLI. Use when the user wants to view, create, update, complete, or organize Todoist items, or mentions tasks, inbox, today, upcoming, projects, labels, or filters.
.claude/skills/doist-todoist-cli/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 433% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 260% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 312% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 518% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 921% | 0% |
td <command> --help for available subcommands, flags, and usage examples where provided.td <command> --help for exact flags when you already know the command family.id:..., or a Todoist web URL as a reference.td task <ref>, td project <ref>, td workspace <ref>, td comment <ref>, and td notification <ref> default to view.--project, --task, and --workspace.p1 highest (API 4) through p4 lowest (API 1).curl the fileUrl and then Read the downloaded file — the vision pipeline can reject an image and leave it pinned in context, which breaks the rest of the session. Fetch with td attachment view <file-url> (or --json) when you actually need the content; the base64 output is plain text and safe to keep in context. Skip the fetch entirely unless the user asked for visual analysis — the Name, Size, and Type fields are usually enough.--json, but other output and pagination flags vary by family. Many list commands support subsets of --ndjson, --ids-only, --full, --raw, --limit <n>, --all, --cursor <cursor>, or --show-urls; check td <command> --help for the exact surface.--ids-only prints one stable result ID per line with no empty-state text. Any incomplete-page notice goes to stderr, so stdout remains pipeable. It is mutually exclusive with --json and --ndjson. Check td <command> --help for support and the type of ID returned.--json to return the created or updated entity.--dry-run to preview actions without executing them.--yes.--quiet / -q suppresses success messages. Create commands still print the bare ID for scripting (e.g. id=$(td task add "Buy milk" --quiet)).--no-spinner, --progress-jsonl, -v/--verbose, --accessible, --quiet, --user <id|email>. TD_USER is read as the fallback for --user.bashtd auth login td auth login --read-only td auth login --additional-scopes=app-management td auth login --read-only --additional-scopes=app-management td auth login --additional-scopes=backups td auth login --read-only --additional-scopes=backups td auth login --additional-scopes=billing td auth login --additional-scopes=app-management,backups td auth login --callback-port 9000 # override the OAuth callback port td auth login --no-browser-open # print the authorize URL instead of opening a browser td auth login --credential-store=plaintext # explicitly store the credential in plaintext td auth login --json # emit the new account record as JSON td auth login --ndjson # one-line newline-delimited JSON td auth token td auth token "$TOKEN" --credential-store=plaintext td auth status td auth status --json # full status payload as JSON (--ndjson also supported) TOKEN=$(td auth token view) TOKEN=$(td auth token view --user you@example.com) td auth logout td auth logout --json # emits `{"ok": true}` (--ndjson is silent)
td auth login, td auth status, and td auth logout all accept the standard --json / --ndjson machine-output flags. For login and status the body carries the account record (id, email, auth metadata, plus storedUsers and source from status); logout emits a {"ok": true} envelope under --json and stays silent under --ndjson. td auth login additionally accepts --callback-port <n> (default 8765, with a small fallback range when the port is busy) and --no-browser-open, which prints the authorization URL for manual copy-paste instead of opening a browser (useful on headless or remote hosts).
Opt-in OAuth scopes are requested via --additional-scopes=<list> (comma-separated). Run td auth login --help for the full list. Currently supported:
app-management — adds the dev:app_console scope (manage your registered Todoist apps — rotate secrets, edit webhooks, etc.). Required by td apps list and td apps view.backups — adds the backups:read scope (list and download Todoist backups). Required by td backup list and td backup download.billing — adds the billing:read_write scope, or billing:read when combined with --read-only (view subscription, plan, and pricing). Required by td billing subcommands.Combine freely with --read-only to keep data access read-only while still granting an opt-in scope (e.g. td auth login --read-only --additional-scopes=backups). When a command fails for lack of a scope, the error suggests a re-login command that preserves whichever flags were originally used.
Tokens are stored in the OS credential manager by default. If it is unavailable, credential writes fail without a plaintext fallback. Pass --credential-store=plaintext to td auth login or td auth token only when you explicitly accept plaintext config-file storage; every such write emits a warning to stderr. TODOIST_API_TOKEN takes precedence over stored credentials.
td auth token view writes the stored token to stdout for use in scripts. Always capture it into a shell variable (e.g. TOKEN=$(td auth token view)) — never invoke it bare in an agent transcript or piped to a shell that echoes its output, since that would leak the secret. Honors --user <id|email> (falling back to TD_USER) for multi-account installs and refuses when TODOIST_API_TOKEN is set in the environment (the token is already available there).
The CLI can hold credentials for multiple Todoist accounts at once.
bashtd auth login # adds the account; first one becomes default td accounts list # all stored accounts (with default marker) td accounts list --json # { accounts: [...], default } envelope; --ndjson streams one account per line td accounts use <id|email> # set the default account (alias: td accounts default; --json/--ndjson supported) td accounts current # show the active account (--json/--ndjson supported) td accounts remove <id|email> # delete an account and its token (--json/--ndjson supported) td --user <id|email> task list # one-off override for any command TD_USER=<id|email> td task list # same, for every nested call in a script td auth logout --user <id|email> # log out a specific account
td accounts is also available as td user / td users (back-compat aliases).
Resolution order: --user <ref> > TD_USER > user.defaultUser from config > the only stored account. With multiple accounts and no default, commands error and ask for --user (or td accounts use). <ref> matches an exact id or email (case-insensitive on email). TODOIST_API_TOKEN still bypasses the resolver entirely.
td today, td inbox, td upcoming, td completed, td activitytd task list/view/add/quickadd/update/reschedule/move/complete/uncomplete/delete/browse (alias: td task qa for quickadd)td project list/view/create/update/archive/unarchive/archived/delete/move/reorder/join/share/browse/collaborators/permissionstd project progress/health/health-context/activity-stats/analyze-healthtd label ..., td filter ..., td section ..., td folder ..., td workspace ...td comment ..., td notification ..., td reminder ...td template ..., td attachment view <file-url>, td backup ...td hc locales/search/viewtd stats, td settings ..., td config view, td accounts ..., td completion ..., td view <todoist-url>, td doctor, td update, td changelogtd extension create/install/list/upgrade/remove/exec (alias: td ext)td apps list/view (requires td auth login --additional-scopes=app-management)td backup list/download (requires td auth login --additional-scopes=backups)td billing subscription/plan/prices/pricing (requires td auth login --additional-scopes=billing)Tasks, projects, labels, and filters can be referenced by:
id:xxx - Explicit IDhttps://app.todoist.com/app/task/buy-milk-8Jx4mVr72kPn3QwB or https://app.todoist.com/app/project/work-2pN7vKx49mRq6YhT)Some commands require id: or URL refs (name lookup unavailable): task uncomplete, section archive/unarchive/update/delete/browse, comment update/delete/browse, notification view/accept/reject.
Reminder commands that take an ID (reminder get/update/delete, reminder location get/update/delete) only accept id:xxx or raw IDs — URLs are not supported for reminders.
bashtd today td inbox --priority p1 td upcoming 14 --workspace "Work" td completed list --since 2024-01-01 --until 2024-01-31 td completed list --search "meeting notes" td activity --type task --event completed
bashtd task add "Buy milk" --due tomorrow td task quickadd "Buy milk tomorrow p1 #Shopping" td task qa "Review PR @urgent +Alice" td task list --project "Work" --label "urgent" --priority p1 td task view "Buy milk" td task view "Plan sprint" --include-children # list direct subtasks, flagging ones that nest further td task add "Plan sprint" --project "Work" --section "Planning" --labels "urgent,review" td task update "Plan sprint" --deadline "2026-06-01" --assignee me td task reschedule "Plan sprint" 2026-03-20T14:00:00 td task move "Plan sprint" --project "Personal" --no-section td task complete "Plan sprint" td task uncomplete id:123456 td task delete "Plan sprint" --yes td task browse "Plan sprint"
Choosing between task add and task quickadd:
td task quickadd (alias td task qa) uses Todoist's natural-language parser. Inline syntax covers dates ("tomorrow at 2pm"), priority (p1–p4), project (#Project), labels (@label), sections (/Section), and assignee (+Person on shared projects). Prefer quickadd when all task attributes can be expressed inline and you do not need to set additional structured fields — it's one call and no name-resolution lookups are required.td task add when you need flags that Quick Add syntax can't express (--deadline, --description, --parent, --duration, --uncompletable, --order), when the text is being composed programmatically, or when you need explicit id: / URL references for project/section/parent.td task quickadd supports --stdin, --json, and --dry-run only; everything else is embedded in the text.td add <text> is a human shorthand for td task quickadd — same parser, same flag surface (--stdin, --json, --dry-run). Agents should prefer td task quickadd / qa for discoverability alongside the other task subcommands.--due on task add / task update is sent verbatim to the API as due_string — the CLI does not parse or rewrite it. The server's due_string parser handles simple inputs ("2026-06-01", "tomorrow", "every Monday") but does not unpack some more complex clauses (i.e. starting <date>).Useful task flags:
--stdin on task add reads the task description from stdin; on task quickadd (and the top-level td add) it reads the full natural-language text from stdin.--parent, --section, --project, --workspace, --assignee, --labels, --due, --deadline, --duration, and --priority cover most task workflows.--labels takes bare label names (--labels "urgent,review"). The @ prefix is display-only quickadd syntax, not part of a stored label name; a leading @ on a --labels value is stripped automatically, so @urgent and urgent resolve to the same label.td task complete --forever stops recurrence; td task update --no-due clears the due date, --no-deadline clears deadlines, and --no-labels removes all labels; td task move --no-parent and --no-section detach from hierarchy.--include-children on task view lists up to 25 direct subtasks, each flagged with whether it has subtasks of its own. A dated parent can hide an undated subtask that no date filter will surface, so check this before assuming a task is a leaf rather than guessing. Past 25, page the rest with td task list --parent id:<id> --all. Under --json it merges childCount, children, hasMoreChildren and childrenError into the task object; childrenError means the listing is incomplete, not empty.bashtd project list --personal td project list --search "Road" td project archived td project view "Roadmap" --detailed td project view "Roadmap" --raw # don't render the description markdown td project view "Roadmap" --include-children # list direct sub-projects, flagging ones that nest further td project collaborators "Roadmap" td project create --name "New Project" --color blue td project create --name "New Project" --description "Quarterly OKRs" td project create --name "Imported" --stdin # read the description from stdin td project update "Roadmap" --description "Updated scope" td project update "Roadmap" --favorite td project update "Roadmap" --folder "Engineering" td project update "Roadmap" --no-folder td project update "Roadmap" --parent "Engineering" td project update "Roadmap" --no-parent td project update "Roadmap" --parent "Engineering" --json td project update "Roadmap" --parent "Engineering" --dry-run td project reorder "Roadmap" --before "Marketing" td project reorder "Roadmap" --after "Marketing" td project reorder "Roadmap" --position 0 td project reorder "Roadmap" --position 2 --json td project reorder "Roadmap" --before "Marketing" --dry-run td project archive "Roadmap" td project unarchive "Roadmap" td project move "Roadmap" --to-workspace "Acme" --folder "Engineering" --visibility team --yes td project join id:abc123 td project share "Roadmap" alice@example.com td project share --project "Roadmap" alice@example.com td project share "Roadmap" alice@example.com --message "Join the planning" td project share "Roadmap" alice@example.com --json td project share "Roadmap" alice@example.com --dry-run td project share "Team Plan" bob@example.com --role guest --auto-invite td project delete "Roadmap" --yes td project progress "Roadmap" td project health "Roadmap" td project health-context "Roadmap" td project activity-stats "Roadmap" --weeks 4 --include-weekly td project analyze-health "Roadmap" td project archived-count --workspace "Acme" td project permissions td workspace list td workspace view "Acme" td workspace projects "Acme" td workspace users "Acme" --role ADMIN,MEMBER td workspace insights "Acme" --project-ids "id1,id2" td workspace create --name "Acme" td workspace update "Acme" --description "Acme Inc." --dry-run # admin-only td workspace delete "Old WS" --yes # admin-only td workspace user-tasks "Acme" --user alice@example.com td workspace activity "Acme" --json td workspace use "Acme" # persist a default; omitted refs on other workspace commands fall back to it td workspace use --clear # forget the stored default td folder list "Acme" td folder view "Engineering" td folder create "Acme" --name "Engineering" td folder update "Engineering" --name "Platform" --workspace "Acme" td folder delete "Engineering" --workspace "Acme" --yes
--include-children on project view lists up to 25 direct sub-projects, each flagged with whether it has sub-projects of its own, and merges the same childCount / children / hasMoreChildren / childrenError fields under --json. Workspace projects never have sub-projects — they nest under folders instead, so they always report none. A Parent: line is shown for any sub-project whether or not the flag is passed.
bashtd label list td label list --search "bug" td label view "urgent" td label create --name "urgent" --color red td label update "urgent" --color orange td label delete "urgent" --yes td label browse "urgent" td label rename-shared "oldname" --name "newname" td label remove-shared "oldname" --yes td filter list td filter view "Urgent work" td filter view "Urgent work" --sort priority --sort-order desc # override the view's sorting td filter view "Urgent work" --sort none # keep the raw API order td filter create --name "Urgent work" --query "p1 & #Work" --description "Everything blocking the release" td filter update "Urgent work" --query "p1 & #Work & today" td filter update "Urgent work" --description "Updated scope" # description-only update td filter update "Urgent work" --no-description # clear the description td filter delete "Urgent work" --yes td filter browse "Urgent work" td section list "Roadmap" td section list --search "Planning" td section list --search "Planning" --project "Roadmap" td section create --project "Roadmap" --name "In Progress" td section create --project "Roadmap" --name "QA" --description "Bugs to verify" td section create --project "Roadmap" --name "Imported" --stdin # read the description from stdin td section update id:123 --name "Done" td section update id:123 --description "Sprint backlog" # description-only update echo "" | td section update id:123 --stdin # empty stdin clears the description td section reorder "Review" --project "Roadmap" --before "Done" td section reorder "Review" --project "Roadmap" --after "In Progress" td section reorder --section "Review" --project "Roadmap" --position 0 --dry-run td section reorder "Review" --project "Roadmap" --position 2 --json td section archive id:123 td section unarchive id:123 td section delete id:123 --yes td section browse id:123
Saved filters can contain multiple comma-separated queries, each displayed as a separate filter section. td filter view preserves those sections and applies --limit to each one. Under --json, multi-section filters return { sections: [{ query, results, nextCursor }] }; under --ndjson, each line is one section with the same fields. Because each section has its own pagination cursor, use --all instead of --cursor for multi-section filters.
td filter view orders tasks the way the Todoist apps do: it applies the sorting saved on that filter's view, and falls back to Todoist's default hierarchy (priority, then date, then deadline, then project and task order; date first for filters that query dates). --sort overrides it with default, priority, date, deadline, date-added, name, project, assignee, workspace, or none for the raw API order, and --sort-order asc|desc sets the direction of whichever field is in play (default and none have no direction). Sorting is applied to the tasks that were fetched, so pair it with --all when a filter has more results than the limit.
Shared labels can appear in td label list and td label view, but standard update and delete actions only work for labels with IDs. Use td label rename-shared and td label remove-shared for shared labels.
bashtd comment list "Plan sprint" td comment list "Roadmap" --project td comment add "Plan sprint" --content "See attached" --file ./report.pdf td comment add "Plan sprint" --content "See attached" --file ./report.pdf --file-name "Quarterly report.pdf" td comment add "Plan sprint" --content "@Ana could you review?" --notify "Ana" td comment add "Plan sprint" --content "Note to self" --no-notify td comment update id:123 --content "Updated text" td comment delete id:123 --yes td comment browse id:123 td attachment view "https://files.todoist.com/..." td notification list --unread td notification view id:123 td notification accept id:123 td notification reject id:123 td notification read --all --yes td reminder list "Plan sprint" td reminder list --type time td reminder add "Plan sprint" --before 30m td reminder add "Plan sprint" --at "2026-06-01 09:00" --urgent # iOS full-screen alarm td reminder update id:123 --before 1h td reminder update id:123 --no-urgent # toggle urgency without changing time td reminder delete id:123 --yes td reminder get id:123 td reminder location add "Plan sprint" --name "Office" --lat 40.7128 --long -74.0060 --trigger on_enter --radius 100 # radius in meters td reminder location update id:456 --radius 200 # radius in meters td reminder location delete id:456 --yes td reminder location get id:456
td attachment view prints text attachments directly and encodes binary content as base64. Use --json for metadata plus content. Prefer this over curl + Read on Todoist file URLs — for images in particular, Read will try to decode the file through the vision pipeline, and if that fails the image stays pinned in conversation context and every retry hits the same error.
Comments notify only the people comment add is handed. Writing "@Ana" in the text notifies nobody — name her with --notify. Omit --notify to notify whoever the Todoist apps would (the task's assignee, assigner and creator on a first comment, or the previous comment's participants on a reply), or pass --no-notify to stay silent. Notification cannot be sent when editing a comment, only when adding one.
td comment view flags image attachments with a Hint line pointing at td attachment view. In --json mode the hint is written to stderr so stdout stays parseable — watch the tool output, not just the JSON body.
bashtd hc td hc --help td hc locale --set-default pt-br td hc search "filters" --ndjson # one article per line for scripts (--json also supported) td hc view https://www.todoist.com/help/articles/introduction-to-filters-V98wIH
td hc queries the Todoist online Help Center. Run td hc --help for locale discovery, article search, and article viewing details. td hc locale --set-default <locale> persists a preferred locale in ~/.config/todoist-cli/config.json under hc.defaultLocale; the --locale flag on individual subcommands still overrides it. td hc view accepts id:N, raw numeric article IDs, get.todoist.help URLs, and public www.todoist.com/help/articles/... marketing URLs (resolved to the underlying Zendesk article via slug search).
bashtd template export-file "Roadmap" --output template.csv td template export-url "Roadmap" td template create --name "New Project" --file template.csv --workspace "Acme" td template create --name "New Project" --file template.csv --file-name "Q2 plan.csv" td template import-file "Roadmap" --file template.csv td template import-file "Roadmap" --file template.csv --file-name "Q2 plan.csv" td template import-id "Roadmap" --template-id product-launch --locale fr
bashtd backup list td backup download "2024-01-15_12:00" --output-file backup.zip
The backup command surface requires the backups:read OAuth scope — re-run td auth login --additional-scopes=backups to grant it. Without the scope, calls fail with an AUTH_ERROR whose hint preserves any previously used flags (e.g. a read-only user sees td auth login --read-only --additional-scopes=backups).
bashtd apps list td apps list --json td apps view "Todoist for VS Code" td apps view id:9909 td apps view 9909 td apps view id:9909 --json td apps view id:9909 --include-secrets td apps view id:9909 --json --include-secrets td apps update id:9909 --name "My Renamed App" td apps update id:9909 --description "Does a useful thing" td apps update id:9909 --add-oauth-redirect https://example.com/callback td apps update id:9909 --remove-oauth-redirect https://example.com/callback --yes td apps update id:9909 --set-webhook-url https://example.com/webhook td apps update id:9909 --name "My App" --description "New blurb" --set-webhook-url https://example.com/webhook td apps delete id:9909 --yes
The apps command surface manages the user's registered Todoist developer apps (integrations). All apps subcommands require the dev:app_console OAuth scope — re-run td auth login --additional-scopes=app-management to grant it. Without the scope, calls fail with a MISSING_SCOPE error pointing at the same hint.
td apps list plain output leads with the display name and follows it with (id:N) (self-describing in --accessible mode), then an indented Client ID: <client_id> line, then the description. --json / --ndjson dump the full app payload (id, clientId, displayName, status, userId, createdAt, serviceUrl, oauthRedirectUri, description, icons, appTokenScopes).
td apps view <ref> accepts a name (fuzzy/case-insensitive), id:N, or a raw numeric id. Plain output shows display name as a header, then a labelled key/value block (id, status, users, created date, service URL, OAuth redirect, token scopes, icon URL, client id) followed by the description. Webhook configuration is always fetched (getAppWebhook — callback URL is user-supplied, not a secret). When the app has UI extensions, a UI extensions: section lists each one as <name> (<type>[: <sub-type>]) (type is context-menu/composer/settings; sub-type is the context-menu context project/task or the composer location task/comment), followed by an Install URL: line (https://app.todoist.com/app/install/<distribution_token>) — the link shared so others can install the integration. In --json / --ndjson the payload always carries uiExtensions, distributionToken, and installUrl (the last is null when there are no UI extensions). When --include-secrets is set, the command additionally fetches the app's secrets (client_secret), verification token, and test token.
All td apps update flags combine in a single invocation, which performs up to two API calls: an app-record patch (updateApp, carrying any of display name, description, OAuth redirect URIs) followed by a webhook URL swap (updateAppWebhook, a separate endpoint). The record patch runs first; if it succeeds and the webhook call then fails, the record change stays persisted (no rollback). Passing no flags errors with NO_CHANGES. The only mutually-exclusive pair is --add-oauth-redirect + --remove-oauth-redirect (they read-modify-write the same field) → CONFLICTING_OPTIONS.
--name <name> sets the app's display name (SDK displayName) and --description <description> sets its description; an empty --description "" clears the description, while an empty/whitespace --name is rejected with INVALID_OPTIONS.
--add-oauth-redirect <url> appends an OAuth redirect URI to the app, and --remove-oauth-redirect <url> takes one off (requires --yes to actually mutate, like td task delete). The URI is validated before any API call: https://<host>, http(s)://localhost[:port][/path], http(s)://127.0.0.1[:port][/path], or a custom-scheme URI (e.g. myapp://callback) are accepted; javascript, data, file, vbscript, and ftp custom schemes are rejected. Removals skip validation so users can clean up legacy malformed URIs. Adding a URI already set on the app fails with ALREADY_EXISTS; removing a URI that isn't on the app is a no-op (message, no API call). A real removal gates the whole invocation: without --yes nothing is performed (plain output prints a batch "would update" preview; --json throws CONFIRMATION_REQUIRED), so any name/description/webhook change in the same command is withheld too.
--set-webhook-url <url> swaps the callback URL on the app's existing webhook. The webhook holds a single URL, so this is a straight set; the current webhook's event list and version are read (getAppWebhook) and preserved — only the URL changes. It errors with NO_WEBHOOK if the app has no webhook configured yet (a webhook must exist before its URL can be changed, since creating one needs an event list). The URL must be a public https://<host> URL. Setting the URL to the value already configured is a no-op (message, no API call).
--dry-run prints one combined preview of every pending change. --json output shape depends on which surfaces the flags touched: a lone app-record change emits the bare app object, a lone webhook change emits the bare webhook object, and touching both emits { "app": <app>, "webhook": <webhook> }.
td apps delete <ref> deletes a registered app (resolved by name, id:N, or raw numeric id). This is destructive and irreversible: deleting an app immediately breaks it for everyone who uses it — any user who authorized the integration loses access, and the app cannot be restored. Always confirm with the user that they are sure before running with --yes. It requires --yes to actually delete; without it the command prints a Would delete app: … preview and makes no API call (same convention as td folder delete / td workspace delete). --dry-run prints the standard dry-run preview.
The OAuth client_id is public and always shown. The distribution token is not a secret (it is a shareable install link): in plain output it surfaces only via the Install URL line, which appears only when the app has UI extensions; in --json / --ndjson the distributionToken key is always present. The three sensitive credentials — client secret, verification token, test access token — are hidden by default. In plain mode each of those lines renders a (hidden — pass --include-secrets to reveal) hint; in --json / --ndjson the clientSecret, verificationToken, and testToken keys are omitted from the payload entirely. With --include-secrets, the values are rendered / emitted normally — in that mode a non-existent test token reads as (not created). Webhook configuration is always included when configured (callback URL, event list, version); a missing webhook renders as (not configured) in plain output and null in JSON.
bashtd billing # subscription (default subcommand) td billing subscription --json td billing plan td billing prices td billing pricing --formatted
The billing command surface is read-only and requires the billing OAuth scope — re-run td auth login --additional-scopes=billing to grant it. A normal login grants billing:read_write; adding --read-only narrows it to billing:read. Either satisfies these read commands. Without the scope, calls fail with a MISSING_SCOPE error whose hint preserves any previously used flags. All subcommands accept --json / --ndjson, which dump the raw SDK payload verbatim.
td billing subscription (the default subcommand) shows the current plan, status, activation method, expiration date, plan price, invoice credit balance, and billing-portal URLs when present. td billing plan shows Pro plan status, downgrade date, and the per-cycle price list. td billing prices lists available Pro and Teams prices by billing cycle. td billing pricing shows current and legacy pricing keyed by version; --formatted returns localized price strings instead of minor-unit numbers.
An extension is an executable named td-<name> that td runs as td <name> .... It can be written in any language; the contract is the process boundary, not a JavaScript API.
bashtd extension create goals # scaffold one (--template bash|node) td extension install owner/td-goals # from a GitHub release or a clone td extension install --pin v0.3.0 owner/td-goals td extension install ./td-scratch # a local directory, symlinked, for development td extension list # --json for the full record of each one td extension upgrade --all # --dry-run to preview; --force to pass a pin td extension remove goals # --force for a clone with uncommitted changes td extension exec goals list --json # bypass the built-in commands entirely
Two rules matter when calling one:
td goals --json is --json for goals, not for td. Global flags only apply before the name: td --user alice goals list.An extension is not reviewed or endorsed by Todoist, and runs with the user's own permissions.
bashtd stats td stats goals --daily 10 --weekly 50 td stats vacation --on td settings view td settings update --timezone "America/New_York" --time-format 24 --date-format intl td settings themes td config view td config view --json td config view --show-token td completion install zsh td completion install pwsh td completion uninstall td view https://app.todoist.com/app/task/buy-milk-abc123 td view https://app.todoist.com/app/today td doctor td doctor --offline td doctor --json td update --check td update --check --json td update --channel td update switch --stable td update switch --pre-release --json td changelog --count 10
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 11,540 | 14,128 | +22% | 1 | 1 | 0% | 718 | 8,999 | +1153% | 0 | 0 | — |
case-02 | fail→fail | 11,584 | 10,470 | -10% | 1 | 1 | 0% | 1,162 | 9,137 | +686% | 0 | 0 | — |
case-03 | fail→fail | 9,491 | 9,812 | +3% | 1 | 1 | 0% | 921 | 9,306 | +910% | 0 | 0 | — |
case-04 | pass→pass | 13,751 | 10,860 | -21% | 1 | 1 | 0% | 2,368 | 10,502 | +343% | 0 | 0 | — |
case-05 | pass→pass | 20,427 | 14,425 | -29% | 1 | 1 | 0% | 2,529 | 11,125 | +340% | 0 | 0 | — |
case-06 | pass→pass | 10,524 | 6,365 | -40% | 1 | 1 | 0% | 1,345 | 9,330 | +594% | 0 | 0 | — |
case-07 | pass→pass | 19,089 | 5,308 | -72% | 1 | 1 | 0% | 1,553 | 9,538 | +514% | 0 | 0 | — |
case-08 | fail→pass | 11,436 | 4,760 | -58% | 1 | 1 | 0% | 1,732 | 9,227 | +433% | 0 | 0 | — |
case-09 | pass→pass | 14,559 | 7,520 | -48% | 1 | 1 | 0% | 2,127 | 9,382 | +341% | 0 | 0 | — |
case-10 | fail→pass | 17,080 | 3,723 | -78% | 1 | 1 | 0% | 2,549 | 9,184 | +260% | 0 | 0 | — |
case-11 | fail→pass | 15,923 | 5,174 | -68% | 1 | 1 | 0% | 2,282 | 9,407 | +312% | 0 | 0 | — |
case-12 | fail→pass | 9,084 | 4,183 | -54% | 1 | 1 | 0% | 1,482 | 9,154 | +518% | 0 | 0 | — |
case-13 | fail→pass | 6,716 | 3,823 | -43% | 1 | 1 | 0% | 897 | 9,156 | +921% | 0 | 0 | — |
case-14 | fail→fail | 9,701 | 5,775 | -40% | 1 | 1 | 0% | 1,381 | 9,431 | +583% | 0 | 0 | — |
case-15 | fail→pass | 10,739 | 3,874 | -64% | 1 | 1 | 0% | 1,487 | 9,031 | +507% | 0 | 0 | — |
case-16 | pass→pass | 10,012 | 5,419 | -46% | 1 | 1 | 0% | 1,342 | 9,319 | +594% | 0 | 0 | — |
case-17 | pass→pass | 14,334 | 3,610 | -75% | 1 | 1 | 0% | 1,703 | 9,129 | +436% | 0 | 0 | — |
case-18 | fail→pass | 10,855 | 7,559 | -30% | 1 | 1 | 0% | 1,511 | 9,286 | +515% | 0 | 0 | — |
case-19 | fail→pass | 13,686 | 5,736 | -58% | 1 | 1 | 0% | 1,959 | 9,088 | +364% | 0 | 0 | — |
case-20 | fail→pass | 12,600 | 5,199 | -59% | 1 | 1 | 0% | 1,780 | 9,239 | +419% | 0 | 0 | — |
case-21 | pass→pass | 15,760 | 3,695 | -77% | 1 | 1 | 0% | 2,722 | 9,167 | +237% | 0 | 0 | — |
case-22 | pass→pass | 9,338 | 4,086 | -56% | 1 | 1 | 0% | 1,430 | 9,176 | +542% | 0 | 0 | — |
case-23 | pass→pass | 14,091 | 3,753 | -73% | 1 | 1 | 0% | 1,518 | 9,105 | +500% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 20 counted toward the lift figure. The other 3 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +39 percentage points is the difference between those two pass rates over the 20 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 9/3/2026 | +64% |
| gemini-3.6-flash | verified | 8/27/2026 | +50% |
| gemini-3.6-flash | verified | 8/22/2026 | +68% |
Other measured skills in the registry, with their headline benchmark lift.