/basecamp - Basecamp Workflow Command

SkillFiles & storage

Interact 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.

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/skills in skills/basecamp/SKILL.md and read by ahel’s review.

Full CLI coverage: 189 tracked in-scope 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:

  1. Choose the right output mode--jq when you need to filter/extract data; --json for full JSON; --md when presenting results to a human (see Output Modes below). Never pipe to external jq — use --jq instead.

  2. Parse URLs first with basecamp url parse "<url>" to extract IDs

  3. Comments are flat - reply to parent recording, not to comments

  4. Check context via .basecamp/config.json before assuming project

  5. 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 \n stays 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 (--data on api post/put, --body, --content, --description, --comment on todos sweep, --file on notes set). Each command's --agent help 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 help and 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 --edit where offered.
    • Trailing newlines are trimmed from stdin content, so printf 'x\n' | ... - posts x (this keeps boost create - inside its 16-rune limit).
  • Universal - support (and the stray-- guard) shipped in v0.10.0. Older CLIs do not support it consistently: comments create/update read stdin, while unsupported inputs may treat - as literal content or fail. For example, messages create "Title" - posts a body of -, which Markdown renders as an empty bullet list. When the CLI version is unknown, check basecamp --version first, or pass the content portably as "$(cat file.md)" and verify the posted content when it matters.
  1. Project scope is mandatory for most commands — via --in <project> or .basecamp/config.json. Cross-project exceptions: basecamp reports assigned for assigned work, basecamp assignments for structured assignment views, basecamp reports overdue for overdue todos, basecamp reports schedule for upcoming schedule across all projects, basecamp recordings <type> for browsing by type, basecamp notifications for notifications, basecamp gauges list for account-wide gauges, and the seven list commands covered in item 7.
  2. Account-wide listing. basecamp todos list --all-projects --json lists across every project; the same flag does the same on cards list, messages list, comments list, files list, forwards list, and checkins 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 N to raise the cap (it walks pages until N are collected) or --all for everything. --page N fetches exactly one page, but only on the paginated listings. The two overdue variants — basecamp todos list --all-projects --overdue and basecamp cards list --all-projects --overdue — come from unpaginated endpoints. They accept --limit and --all but reject --page, so do not generate --page against them.

Output Modes

Choosing a mode:

GoalFlagFormat
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--jsonJSON envelope: {ok, data, summary, breadcrumbs, meta}; errors: {ok:false, error, code, retryable, hint, meta}
Show results to a user--md / -mGFM tables, task lists, structured Markdown
Automation / scripting--agentSuccess: 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 me resolves 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)
  • --due on a listing is a different flag and does not take dates: it accepts only with, without, or overdue, and only account-wide. basecamp todos list --due tomorrow is rejected. For date-based listing use --overdue, --no-due-date, or basecamp assignments due <scope>
  • Project from .basecamp/config.json if --in not specified
  • Multiple identities use named profiles: basecamp profile create <name>, then select one with global --profile <name> or BASECAMP_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 likewise cards list, messages list, comments list, files list, forwards list, and checkins answers.

TaskCommand
List projectsbasecamp 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 bookmarksbasecamp bookmarks list --json
Bookmark somethingbasecamp bookmarks add <id-or-url> --json
Is it bookmarked?basecamp bookmarks check <id-or-url> --json (always exits 0)
Bubble a recording upbasecamp bubble-up add <id-or-url> --json
Schedule a bubble-upbasecamp bubble-up add <id-or-url> --at tomorrow --json
Pop a bubble-upbasecamp bubble-up remove <id-or-url> --json
My unpublished draftsbasecamp drafts list --json
Read my personal notebasecamp notes show --json
Replace my personal notebasecamp notes set "<content>" --json
Check-ins I owe answers tobasecamp checkins reminders --json
Add to Up Nextbasecamp assignments prioritize <id> --json
Recolor a calendarbasecamp calendars update <id-or-url> --color blue --json
Todo outside any listbasecamp todos create "<content>" --loose --in <project> --json
Assign todobasecamp assign <id> [id...] --to <person> --in <project> --json
Assign cardbasecamp assign <id> [id...] --card --to <person> --in <project> --json
Assign card stepbasecamp assign <id> [id...] --step --to <person> --in <project> --json
Create todobasecamp todos create "Task" --in <project> --list <list> --json
Create todolistbasecamp todolists create "Name" --in <project> --json
Complete todobasecamp todos complete <id> --json
List cardsbasecamp cards list --in <project> --json
Create cardbasecamp cards create "Title" --in <project> --json
Complete card`basecamp cards done <id
Move cardbasecamp cards move <id> --to <column> [--position N] --in <project> --json
Move card to on-holdbasecamp cards move <id> --on-hold --in <project> --json
Move card to another projectbasecamp cards move <id> --to-wormhole <wormhole_id> --in <project> --json (async teleport)
Post messagebasecamp messages create "Title" "Body" --in <project> --json
Post with @mentionbasecamp messages create "Title" "Hey @First.Last, ..." --in <project> --json
Post silentlybasecamp messages create "Title" "Body" --no-subscribe --in <project> --json
Post to chatbasecamp chat post "Message" --in <project> --json
List pings`basecamp notifications --json --jq '.data.reads[]?
Read ping threadbasecamp api get "/buckets/<circle_id>/chats/<chat_id>/lines.json" --agent
Post to ping threadbasecamp api post "/buckets/<circle_id>/chats/<chat_id>/lines.json" --data '{"content":"<p>message</p>"}' --json
Add commentbasecamp comments create <recording_id> "Text" --in <project> --json
Inspect comment / reply atomsbasecamp comments show <url> --jsonreply_target + mention in .data
List attachmentsbasecamp attachments list <id|url> --json
Download attachmentsbasecamp attachments download <id> --out /tmp/
Show + downloadbasecamp todos show <id> --download-attachments --json
Stream attachment to stdoutbasecamp attachments download <id> --file <name> --out -
Change history for an itembasecamp events <id|url> --json (when a card moved columns, when a todo was completed)
Searchbasecamp search "query" --json
Parse URLbasecamp url parse "<url>" --json
Upload filebasecamp files uploads create <file> [--vault <folder_id>] --in <project> --json
Download filebasecamp files download <id> --in <project>
Stream file to stdoutbasecamp files download <id> --out - --in <project>
Download storage URLbasecamp files download "https://storage.3.basecamp.com/.../download/report.pdf"
My assignmentsbasecamp assignments --json (priorities + non-priorities)
Overdue assignmentsbasecamp assignments due overdue --json
Completed assignmentsbasecamp assignments completed --json
Notificationsbasecamp notifications --json
Mark notification readbasecamp notifications read <id> --json
All bubble-ups (BC5)basecamp notifications bubbleups --json
Gauges (account-wide)basecamp gauges list --json
Gauge needlesbasecamp gauges needles --in <project> --json
Create needlebasecamp gauges create --position 75 --color green --in <project> --json
Account detailsbasecamp 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.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
70
Forks
7
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
basecamp-basecamp
Source
github.com/basecamp/skills