/basecamp - Basecamp Workflow Command
SkillFiles & storageInteract with Basecamp via the Basecamp CLI. Full API coverage: projects, todos, cards, messages, files, schedule, check-ins, timeline, recordings, templates, webhooks, subscriptions, lineup, chat, pings, gauges, assignments, notifications, bookmarks, bubble-up, drafts, notes, calendars, and accounts. Use for ANY Basecamp question or action.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the /basecamp - Basecamp Workflow Command skill
What this skill tells your AI
The instructions your AI receives, as published by basecamp/basecamp-cli in skills/basecamp/SKILL.md and read by ahel’s review.
Full CLI coverage: 155 endpoints across todos, cards, messages, files, schedule, check-ins, timeline, recordings, templates, webhooks, subscriptions, lineup, chat, pings, gauges, assignments, notifications, and accounts.
Agent Invariants
MUST follow these rules:
-
Choose the right output mode —
--jqwhen you need to filter/extract data;--jsonfor full JSON;--mdwhen presenting results to a human (see Output Modes below). Never pipe to externaljq— use--jqinstead. -
Parse URLs first with
basecamp url parse "<url>"to extract IDs -
Comments are flat - reply to parent recording, not to comments
-
Check context via
.basecamp/config.jsonbefore assuming project -
Content fields accept Markdown and @mentions — message body and comment content accept Markdown syntax; the CLI converts to HTML automatically. Use Markdown formatting (lists, bold, links, code blocks, tables) for rich content. Four mention syntaxes are available (prefer deterministic for agents):
[@Name](mention:SGID)— zero API calls, embeds SGID directly (preferred for agents)[@Name](person:ID)— one API call, resolves person ID to SGID via pingable set@sgid:VALUE— inline SGID embed for pipeline composability@Name/@First.Last— fuzzy name resolution (may be ambiguous) For todos, documents, and cards, content is sent as-is — use plain text or HTML directly.
Table boundary: GFM tables round-trip: they render in message/comment bodies, display converts them back to pipe tables, and the TUI in-place editors open simple grids for editing. Only complex tables — merged cells (colspan/rowspan), captions, extra header rows, nested tables, attachments/images or block content inside cells, multi-paragraph or multi-line cells, or a table inside a blockquote or list — refuse to open, since a GFM pipe table can't represent those shapes (edit them on Basecamp web, or replace the whole field via
messages update/comments update/todos update --description, which take fresh content and are unaffected). Complex tables still display best-effort, flattened to a plain grid.Multiline / non-ASCII content: do not rely on bash ANSI-C quoting (
$'...\n...') — it is a bash/zsh extension. Under a POSIX/bin/sh(dash, busybox-ash, common in sandboxes) the$is passed through literally and posts a stray leading$, and\nstays a literal backslash-n. Pipe the content via stdin instead, using-as the content argument:printf '%s\n' '海报 mockup 方向稿:' '' '<bc-attachment ...>' | basecamp comments create <recording_id> - --in <project> --json-means "read from stdin" on every content input: content-kind positionals (comments create/update,messages create [body],cards create [body],todos create,docs documents create [content],chat post/update,boost create,checkins answer create/update,notes set) and content flags (--dataonapi post/put,--body,--content,--description,--commentontodos sweep,--fileonnotes set). Each command's--agenthelp lists its stdin inputs. Rules:- A pipe is never consumed implicitly — without
-it is ignored (or, where content is required and missing, the error teaches-). - Only one input can read stdin per invocation.
- A literal
-anywhere else (a title, a name, a path) errors when stdin is piped. Escape a positional after the--separator (basecamp projects create -- -); a flag value has no in-line escape — run the command without piped stdin.basecamp helpand shell completion are exempt: they write nothing to Basecamp, and completion legitimately receives-as the word being completed. -with nothing piped (interactive TTY) errors immediately instead of hanging; use a pipe, a heredoc (basecamp comments create <id> - <<'EOF'), or--editwhere offered.- Trailing newlines are trimmed from stdin content, so
printf 'x\n' | ... -postsx(this keepsboost create -inside its 16-rune limit).
-
Project scope is mandatory for most commands — via
--in <project>or.basecamp/config.json. Cross-project exceptions:basecamp reports assignedfor assigned work,basecamp assignmentsfor structured assignment views,basecamp reports overduefor overdue todos,basecamp reports schedulefor upcoming schedule across all projects,basecamp recordings <type>for browsing by type,basecamp notificationsfor notifications,basecamp gauges listfor account-wide gauges, and the seven list commands covered in item 7. -
Account-wide listing.
basecamp todos list --all-projects --jsonlists across every project; the same flag does the same oncards list,messages list,comments list,files list,forwards list, andcheckins answers. It overrides a configured project, and with no project in scope those commands already list account-wide rather than prompting. Flags that name something inside a single project are rejected there rather than silently ignored. Account-wide listings return the first 100 items by default — account-wide "all" is the whole account, not one project's worth. Use--limit Nto raise the cap (it walks pages until N are collected) or--allfor everything.--page Nfetches exactly one page, but only on the paginated listings. The two overdue variants —basecamp todos list --all-projects --overdueandbasecamp cards list --all-projects --overdue— come from unpaginated endpoints. They accept--limitand--allbut reject--page, so do not generate--pageagainst them.
Output Modes
Choosing a mode:
| Goal | Flag | Format |
|---|---|---|
| Filter/extract JSON data | --jq '<expr>' | Built-in jq filter (no external jq needed). Implies --json; filter runs on the envelope. |
| Filter in agent mode | --agent --jq '<expr>' | Filter runs on data-only payload (no envelope), matching --agent contract. |
| Full JSON output | --json | JSON envelope: {ok, data, summary, breadcrumbs, meta}; errors: {ok:false, error, code, retryable, hint, meta} |
| Show results to a user | --md / -m | GFM tables, task lists, structured Markdown |
| Automation / scripting | --agent | Success: raw JSON data (no envelope); errors: {ok:false,...} object; no interactive prompts |
Always pass --json or --md explicitly — auto-detection depends on config and may not produce the format you expect. Use --md when composing reports, summarizing data, or displaying results inline. --agent is for headless integration scripts.
Avoiding interactive prompts. The flags --agent/--json/--quiet/--ids-only/--count and the environment variable BASECAMP_NONINTERACTIVE=1 suppress interactive selection prompts. --md does not — if a required target is ambiguous (e.g. a project with multiple todosets and no --todoset), and the CLI is attached to a terminal, it will show a blocking picker. When you need Markdown output and no prompts, either pass the flag that names whatever is ambiguous (--todoset <id> for the todoset case above, or --in <project> / --list <id> when the project or list is ambiguous) or set BASECAMP_NONINTERACTIVE=1 in the environment. BASECAMP_NONINTERACTIVE disables all prompts (they become actionable errors instead) without changing the output format — an escape hatch for agents running under a PTY.
Other modes: --quiet (success: raw JSON, no envelope; errors: {ok:false,...}), --ids-only, --count, --stats (session statistics), --styled (force ANSI), -v / -vv (verbose/trace), --jq '<expr>' (built-in jq filter — see below).
CLI Introspection
Navigate unfamiliar commands with --agent --help — returns structured JSON describing any command:
basecamp todos --agent --help
{"command":"todos","path":"basecamp todos","short":"...","long":"...","usage":"...","notes":["..."],
"subcommands":[{"name":"sweep","short":"...","path":"basecamp todos sweep"}],
"flags":[{"name":"assignee","type":"string","default":"","usage":"..."}],
"inherited_flags":[{"name":"json","shorthand":"j","type":"bool","default":"false","usage":"..."}]}
Walk the tree: start at basecamp --agent --help for top-level commands, then drill into any subcommand. Commands carry domain-specific agent hints (e.g., "--assignee filters the account-wide listing only; within a project, fetch all and filter client-side").
Pagination
basecamp <cmd> --limit 50 # Cap results (default varies by resource)
basecamp <cmd> --all # Fetch all (may be slow for large datasets)
basecamp <cmd> --page 1 # First page only, no auto-pagination
--all and --limit are mutually exclusive. --page cannot combine with either.
Smart Defaults
--assignee meresolves to current user--due tomorrow/--due +3/--due "next week"— natural date parsing, when setting a due date (todos create,todos update,cards create, and so on)--dueon a listing is a different flag and does not take dates: it accepts onlywith,without, oroverdue, and only account-wide.basecamp todos list --due tomorrowis rejected. For date-based listing use--overdue,--no-due-date, orbasecamp assignments due <scope>- Project from
.basecamp/config.jsonif--innot specified - Multiple identities use named profiles:
basecamp profile create <name>, then select one with global--profile <name>orBASECAMP_PROFILE=<name>.
Quick Reference
Note: Most queries require project scope (via
--in <project>or.basecamp/config.json). Cross-project exceptions:basecamp reports assigned,basecamp assignments,basecamp reports overdue,basecamp reports schedule,basecamp recordings <type>,basecamp notifications,basecamp gauges list.Seven list commands also list account-wide:
basecamp todos list --all-projects --json, and likewisecards list,messages list,comments list,files list,forwards list, andcheckins answers.
| Task | Command |
|---|---|
| List projects | basecamp projects list --json |
| My todos (in project) | basecamp todos list --assignee me --in <project> --json |
| My todos (cross-project) | basecamp reports assigned --json (defaults to "me") |
| My schedule (cross-project) | basecamp reports schedule --json (upcoming events across all projects) |
| All todos (cross-project) | basecamp todos list --all-projects --json (grouped by project) |
| Overdue todos (in project) | basecamp todos list --overdue --in <project> --json |
| Overdue todos (cross-project) | basecamp todos list --all-projects --overdue --json (flat, oldest first) or basecamp reports overdue --json (bucketed by lateness) |
| All cards (cross-project) | basecamp cards list --all-projects --json (grouped by project) |
| Someone's todos (cross-project) | basecamp todos list --all-projects --assignee "Ann" --json (server-side filter) |
| Two people's todos (cross-project) | basecamp todos list --all-projects --assignee ann --assignee bob --json (matches either) |
| Someone's cards (cross-project) | basecamp cards list --all-projects --assignee "Ann" --json |
| Todos with no due date set (cross-project) | basecamp todos list --all-projects --due without --json |
| My bookmarks | basecamp bookmarks list --json |
| Bookmark something | basecamp bookmarks add <id-or-url> --json |
| Is it bookmarked? | basecamp bookmarks check <id-or-url> --json (always exits 0) |
| Bubble a recording up | basecamp bubble-up add <id-or-url> --json |
| Schedule a bubble-up | basecamp bubble-up add <id-or-url> --at tomorrow --json |
| Pop a bubble-up | basecamp bubble-up remove <id-or-url> --json |
| My unpublished drafts | basecamp drafts list --json |
| Read my personal note | basecamp notes show --json |
| Replace my personal note | basecamp notes set "<content>" --json |
| Check-ins I owe answers to | basecamp checkins reminders --json |
| Add to Up Next | basecamp assignments prioritize <id> --json |
| Recolor a calendar | basecamp calendars update <id-or-url> --color blue --json |
| Todo outside any list | basecamp todos create "<content>" --loose --in <project> --json |
| Assign todo | basecamp assign <id> [id...] --to <person> --in <project> --json |
| Assign card | basecamp assign <id> [id...] --card --to <person> --in <project> --json |
| Assign card step | basecamp assign <id> [id...] --step --to <person> --in <project> --json |
| Create todo | basecamp todos create "Task" --in <project> --list <list> --json |
| Create todolist | basecamp todolists create "Name" --in <project> --json |
| Complete todo | basecamp todos complete <id> --json |
| List cards | basecamp cards list --in <project> --json |
| Create card | basecamp cards create "Title" --in <project> --json |
| Complete card | `basecamp cards done <id |
| Move card | basecamp cards move <id> --to <column> [--position N] --in <project> --json |
| Move card to on-hold | basecamp cards move <id> --on-hold --in <project> --json |
| Move card to another project | basecamp cards move <id> --to-wormhole <wormhole_id> --in <project> --json (async teleport) |
| Post message | basecamp messages create "Title" "Body" --in <project> --json |
| Post with @mention | basecamp messages create "Title" "Hey @First.Last, ..." --in <project> --json |
| Post silently | basecamp messages create "Title" "Body" --no-subscribe --in <project> --json |
| Post to chat | basecamp chat post "Message" --in <project> --json |
| List pings | `basecamp notifications --json --jq '.data.reads[]? |
| Read ping thread | basecamp api get "/buckets/<circle_id>/chats/<chat_id>/lines.json" --agent |
| Post to ping thread | basecamp api post "/buckets/<circle_id>/chats/<chat_id>/lines.json" --data '{"content":"<p>message</p>"}' --json |
| Add comment | basecamp comments create <recording_id> "Text" --in <project> --json |
| Inspect comment / reply atoms | basecamp comments show <url> --json → reply_target + mention in .data |
| List attachments | basecamp attachments list <id|url> --json |
| Download attachments | basecamp attachments download <id> --out /tmp/ |
| Show + download | basecamp todos show <id> --download-attachments --json |
| Stream attachment to stdout | basecamp attachments download <id> --file <name> --out - |
| Change history for an item | basecamp events <id|url> --json (when a card moved columns, when a todo was completed) |
| Search | basecamp search "query" --json |
| Parse URL | basecamp url parse "<url>" --json |
| Upload file | basecamp files uploads create <file> [--vault <folder_id>] --in <project> --json |
| Download file | basecamp files download <id> --in <project> |
| Stream file to stdout | basecamp files download <id> --out - --in <project> |
| Download storage URL | basecamp files download "https://storage.3.basecamp.com/.../download/report.pdf" |
| My assignments | basecamp assignments --json (priorities + non-priorities) |
| Overdue assignments | basecamp assignments due overdue --json |
| Completed assignments | basecamp assignments completed --json |
| Notifications | basecamp notifications --json |
| Mark notification read | basecamp notifications read <id> --json |
| All bubble-ups (BC5) | basecamp notifications bubbleups --json |
| Gauges (account-wide) | basecamp gauges list --json |
| Gauge needles | basecamp gauges needles --in <project> --json |
| Create needle | basecamp gauges create --position 75 --color green --in <project> --json |
| Account details | basecamp accounts show --json |
URL Parsing
Parse URLs before acting on them — unless you're handing the URL to a command
that accepts a URL directly (show, comments show, comments thread,
attachments list/attachments download), which extract the IDs for you. Only comments show and
comments thread verify the URL's host and account before any fetch. For other
URL-accepting commands, only pass URLs from a trusted Basecamp host:
basecamp url parse extracts IDs but does not validate the URL's origin, so
parsing an attacker-controlled path yields trusted-looking IDs.
basecamp url parse "https://3.basecamp.com/2914079/buckets/41746046/messages/9478142982#__recording_9488783598" --json
Returns: account_id, project_id, type, recording_id, comment_id (from fragment).
URL patterns:
/buckets/27/messages/123- Message 123 in project 27/buckets/27/messages/123#__recording_456- Comment 456 on message 123/buckets/27/card_tables/cards/789- Card 789/buckets/27/card_tables/columns/456- Column 456 (for creating cards)/buckets/27/todos/101- Todo 101/buckets/27/uploads/202- Upload/file 202/buckets/27/documents/303- Document 303/buckets/27/schedule_entries/404- Schedule entry 404
Replying to comments:
# Comments are flat - reply to the parent recording_id, not the comment_id
basecamp url parse "https://...messages/123#__recording_456" --json
# Returns recording_id: 123 (parent), comment_id: 456 (fragment) - comment on 123, not 456
basecamp comments create 123 "Reply" --in <project>
# Or get the whole reply-ready context deterministically in one call:
basecamp comments thread "https://...messages/123#__recording_456" --json
# .data.reply_target.recording_id → where to post the reply
# .data.reply_target.account_id → the account that reply belongs to (build a fully-qualified command)
# .data.focus.author.mention.syntax → paste-ready [@Name](mention:SGID)
# .data.comments → surrounding discussion (default window of 41)
# --all returns every fetched comment; --window N sets the window size
# When the account came from the URL (none configured), the reply breadcrumb carries --account
Decision Trees
Finding Content
Need to find something?
├── Know the type + project? → basecamp <type> list --in <project> --json
│ (some groups have default list behavior; use --agent --help if unsure)
├── My assigned work? → basecamp assignments --json (priorities + non-priorities)
│ Or: basecamp reports assigned --json (traditional view, defaults to "me")
├── My overdue assignments? → basecamp assignments due overdue --json
├── My notifications? → basecamp notifications --json
├── Upcoming schedule? → basecamp reports schedule --json (cross-project)
├── Overdue across projects? → basecamp reports overdue --json
├── Browse by type cross-project? → basecamp recordings <type> --json
│ (types: todos, messages, documents, comments, cards, uploads)
│ Note: Defaults to active status; use --status archived for archived items
│ ⚠ No assignee data — cannot filter by person; use reports assigned instead
├── Full-text search? → basecamp search "query" --json
├── Have a comment URL, or a notification link targeting a comment? → basecamp comments thread <url> --json
└── Have a URL? → basecamp url parse "<url>" --json
Modifying Content
Want to change something?
├── Have URL? → basecamp url parse "<url>" → use extracted IDs
├── Have ID? → basecamp <resource> update <id> --field value
├── Change status? → basecamp recordings trash|archive|restore <id>
├── Complete todo? → basecamp todos complete <id>
├── Complete card? → basecamp cards done <id|url> --in <project>
└── Reply to a comment? → basecamp comments show <url> --jq '.data | {reply_target, mention}'
(one call, cheap atoms — the mention is machine-only, so use --jq/--json, not plain show)
or basecamp comments thread <url> when you need the surrounding discussion;
then basecamp comments create <reply_target.recording_id> <text>
Common Workflows
Link Code to Basecamp Todo
# Get commit info and comment on todo (use printf %q for safe quoting)
COMMIT=$(git rev-parse --short HEAD)
MSG=$(git log -1 --format=%s)
basecamp comments create <todo_id> "Commit $COMMIT: $(printf '%s' "$MSG")" --in <project>
# Complete when done
basecamp todos complete <todo_id>
Track PR in Basecamp
# Create todo for PR work
basecamp todos create "Review PR #42" --in <project> --assignee me --due tomorrow
# When merged
basecamp todos complete <todo_id>
basecamp chat post "Merged PR #42" --in <project>
Bulk Process Overdue Todos
# Preview overdue todos
basecamp todos sweep --overdue --dry-run --in <project>
# Complete all with comment
basecamp todos sweep --overdue --complete --comment "Cleaning up" --in <project>
Mentioning people (preferred — deterministic)
# 1. Look up the person
basecamp people pingable --jq '.data[] | select(.name == "Jane Smith")'
# => {"id": 42000, "attachable_sgid": "BAh7CEkiCG...", "name": "Jane Smith"}
# 2. Use SGID in Markdown mention syntax (zero API calls during post)
basecamp comments create 123 "Hey [@Jane Smith](mention:BAh7CEkiCG...), check this" --in <project>
# Or use person ID (one lookup during post)
basecamp comments create 123 "Hey [@Jane Smith](person:42000), check this" --in <project>
Mentioning people (interactive — may be ambiguous)
# Fuzzy matching: use @First.Last to reduce ambiguity
basecamp comments create <id> "@Jane.Smith, please review this" --in <project>
basecamp messages create "Update" "cc @Jane, @Alex" --in <project>
basecamp chat post "@Jane, done!" --in <project>
# Ambiguous names return an error with suggestions
# Use @First.Last for disambiguation
Move Card Through Workflow
# List columns to get IDs
basecamp cards columns --in <project> --json
# Complete a card (moves it to the Done column automatically)
basecamp cards done <card_id> --in <project>
# Move card to column
basecamp cards move <card_id> --to <column_id> --in <project>
# Move card to specific position in column (1-indexed)
basecamp cards move <card_id> --to <column_id> --position 1 --in <project>
# Move card to on-hold section of its current column
basecamp cards move <card_id> --on-hold --in <project>
# Move card to on-hold section of a specific column (numeric ID)
basecamp cards move <card_id> --to <column_id> --on-hold --in <project>
# Move card to on-hold section of a named column (requires --card-table)
basecamp cards move <card_id> --to "Column Name" --on-hold --card-table <table_id> --in <project>
Download File from Basecamp
basecamp files download <upload_id> --in <project> --out ./downloads
# Download attachment from a storage URL (no --in needed)
basecamp files download "https://storage.3.basecamp.com/123/blobs/abc/download/report.pdf"
# Stream to stdout (for piping)
basecamp files download <upload_id> --out - --in <project>
Working with Attachments (Multimodal Agent Workflow)
Messages, todos, cards, and documents may contain images and file attachments
(mockups, screenshots, annotated designs). Show commands surface these as
field-scoped collections — content_attachments and/or description_attachments
— keyed by which rich-text attribute contained them. The notice field hints at
the download command.
Step 1: Fetch the recording and check for attachments
basecamp todos show <id> --json
# Response includes description_attachments when attachments are present
# Messages/documents use content_attachments; cards may have both
# The notice field hints: "3 attachment(s) — download: basecamp attachments download <id>"
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 272
- Forks
- 21
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
basecamp- Source
- github.com/basecamp/basecamp-cli