Drive Miro from your shell. Bulk-migrate stickies between boards, audit board access, or script Miro operations in CI. One verb per Miro REST endpoint, JSON in and out, with a local SQLite mirror for offline search.
- Shell-first. Every endpoint is a flag-driven command. No hand-rolled JSON envelopes, no pagination boilerplate, no curl + jq pipelines.
- Agent-friendly.
--json,--dry-run,--select, and--agentare built in so the output is predictable for scripts and LLMs. - Offline workflows.
miro-cli syncmirrors boards and items into a local SQLite store;miro-cli queryruns SQL and FTS5 search against it without spending API quota. - Bulk verbs.
items bulk-create,items bulk-update,items bulk-delete, andstickies create-gridcollapse N HTTP calls into one for workshop and migration flows. - Safe defaults. Destructive verbs refuse to run without
--yes(or--agent, which implies it).--idempotentmakes create retries safe.
| You want to... | Use |
|---|---|
| Script Miro from bash, CI, or a Makefile | miro-cli (this repo) |
| Drive Miro from Claude Code as a skill | miro-cli + the bundled SKILL.md |
| Drive Miro from a Claude Desktop / MCP-compatible agent | miro-mcp-server |
| Render Miro data as interactive UI in chat (cards, tables, SVG graphs) | miro-mcp-apps |
| Embed Miro into a TypeScript or Python app | The official Miro SDKs |
| Issue a one-off API call to test a payload | miro-cli <verb> --dry-run, or curl |
The CLI, the MCP server, and the MCP Apps server are complements, not
alternatives: same author, overlapping API coverage, different runtimes.
Use whichever ones fit. They all read the same MIRO_ACCESS_TOKEN.
go install github.com/olgasafonova/miro-cli/cmd/miro-cli@latestThis drops a miro-cli binary in $GOPATH/bin (typically ~/go/bin). Make
sure that directory is on your PATH.
Download the appropriate archive for your platform from the
latest release
and extract the miro-cli binary into a directory on your PATH. On macOS,
clear the Gatekeeper quarantine: xattr -d com.apple.quarantine miro-cli.
On Unix, mark it executable: chmod +x miro-cli.
brew install olgasafonova/tap/miro-cliGet an access token from the Miro developer portal and export it:
export MIRO_ACCESS_TOKEN="your-token-here"Or pass --token on every invocation.
miro-cli boards list
miro-cli boards get --board-id <id>
miro-cli stickies create --board-id <id> --content "Hello, Miro" --color yellowRun miro-cli --help for the full command reference, or
miro-cli <group> --help for the verbs under any group.
Twenty-two resource groups today. The tables below summarize each group at
a glance; run miro-cli <group> --help for the full verb list and flags.
| Group | Purpose | Key verbs |
|---|---|---|
boards |
Board lifecycle plus a few search / content helpers | list, get, create, update, delete, copy, find, search, content, summary, picture, share, diagram, audit |
members |
Board access control | list, get, update, remove |
Each group below ships create / get / update / delete against its
own Miro REST resource. The table only calls out non-standard verbs.
| Group | Resource path | Extras |
|---|---|---|
stickies |
/v2/boards/{id}/sticky_notes |
create-grid (bulk row-major layout) |
shapes |
/v2/boards/{id}/shapes |
create-flowchart (v2-experimental stencils) |
texts |
/v2/boards/{id}/texts |
— |
frames |
/v2/boards/{id}/frames |
— |
cards |
/v2/boards/{id}/cards |
— |
app-cards |
/v2/boards/{id}/app_cards |
— |
connectors |
/v2/boards/{id}/connectors |
list |
embeds |
/v2/boards/{id}/embeds |
— |
documents |
/v2/boards/{id}/documents and /v2/boards/{id}/docs |
upload (multipart from disk), update-from-file (replace bytes), create-doc (Markdown rich-text) |
images |
/v2/boards/{id}/images |
upload, update-from-file |
codewidgets |
/v2-experimental/boards/{id}/code_widgets |
read-only list (experimental) |
mindmap |
/v2-experimental/boards/{id}/mindmap_nodes |
list |
tables |
/v2/boards/{id}/data_table_formats |
read-only list and get |
items is the catch-all for verbs that don't care about the underlying
type. Bulk verbs are here because Miro's bulk endpoint accepts a typed
array, not per-resource arrays.
| Verb | What it does |
|---|---|
items list / items list-all |
Cursor-paginated list, or fetch everything until the cursor exhausts |
items get |
Get any item by ID (returns the right shape based on type) |
items update / items delete |
Cross-type partial update + delete |
items bulk-create |
Create up to 20 items per call, mixed types |
items bulk-update |
PATCH many items in one logical call (serial under the hood) |
items bulk-delete |
DELETE many items in one logical call |
items get-within-frame |
List items inside a frame |
items get-by-tag |
List items carrying a tag |
items get-tags / attach-tag / detach-tag |
Tag membership for a single item |
| Group | Purpose | Verbs |
|---|---|---|
tags |
Tag definitions on a board | list, get, create, update, delete |
groups |
Group items so they move/resize as one | list, get, get-items, create, update (full PUT), delete (ungroup, items survive) |
| Command | What it does |
|---|---|
sync |
Mirror boards + items into a local SQLite store (incremental by default) |
query |
Run a read-only SQL or FTS5 query against the store |
See the Local Store section below for the full workflow.
| Group | Purpose | Notes |
|---|---|---|
audit |
Enterprise audit log (list-logs) |
Last 90 days; CSV export for older data |
exports |
Org-scoped board export jobs (eDiscovery) | create-job, list-job-tasks, get-job-status, get-job-results, get-task-link, update-job |
Both require Enterprise-plan tokens with the matching scopes.
# Human-readable table (default in terminal, JSON when piped)
miro-cli boards get
# JSON for scripting and agents
miro-cli boards get --json
# Filter to specific fields
miro-cli boards get --json --select id,name,status
# Dry run, show the request without sending
miro-cli boards get --dry-run
# Agent mode, JSON + compact + no prompts in one flag
miro-cli boards get --agentmiro-cli ships a local SQLite mirror of your boards and items so you can
run ad-hoc SQL and full-text search without burning API calls or waiting
for network round-trips. The default store path is
$XDG_DATA_HOME/miro-cli/store.db (or ~/.local/share/miro-cli/store.db);
override it for any command with --store-path.
# Incremental: re-fetches a board's items only when modifiedAt has advanced
# past the stored watermark. First run does a full sweep.
miro-cli sync
# Force a full re-fetch (after a schema change, or when you suspect drift)
miro-cli sync --full
# Pin the watermark for one run, useful when a previous sync was interrupted
miro-cli sync --since 2026-05-14T00:00:00ZConflict policy is last-write-wins: the API is the source of truth, and rows are upserted by id. The watermark is stamped at the start of a successful run, so a mid-run failure leaves the previous watermark in place and the next run resumes.
# Read-only SELECT against the store. Output is JSON when piped,
# tab-separated table when stdout is a terminal.
miro-cli query "SELECT id, name FROM boards ORDER BY modified_at DESC LIMIT 10"
# Items on a board, by type
miro-cli query "SELECT type, COUNT(*) FROM items WHERE board_id = 'b1' GROUP BY type"The connection is opened with mode=ro and PRAGMA query_only=ON;
non-SELECT input is rejected before execution. --limit (default 1000)
caps the rows returned per query, pass --limit 0 to disable the cap.
An items_fts FTS5 virtual table is kept in lockstep with items via
triggers. It indexes the textual fields Miro models as data.content,
data.title, and data.description: sticky notes, cards, text widgets,
shapes, app cards, and frame titles.
# Find every item mentioning "roadmap" across all synced boards
miro-cli query "SELECT item_id, board_id, item_type FROM items_fts WHERE items_fts MATCH 'roadmap'"
# Phrase match, adjacent tokens in any indexed field
miro-cli query 'SELECT item_id FROM items_fts WHERE items_fts MATCH "Q3 review"'
# Join back to items for richer columns
miro-cli query "SELECT i.id, i.type, i.position_x, i.position_y
FROM items_fts f JOIN items i ON i.id = f.item_id
WHERE items_fts MATCH 'design'"The tokenizer is unicode61 (FTS5's default) and does not stem, so fox
will not match foxes. Use the * suffix for prefix matching: MATCH 'fox*'.
This CLI is designed for AI agent consumption:
- Non-interactive. Never prompts; every input is a flag.
- Pipeable.
--jsonwrites structured output to stdout; errors go to stderr. - Filterable.
--select id,name,statusreturns only the fields you need. - Previewable.
--dry-runprints the request without sending it. - Idempotent create.
--idempotentmakes create-then-retry safe. - Confirmable.
--yes(or--agent, which implies it) is required for destructive verbs. - Offline-friendly.
miro-cli syncmirrors boards + items locally;miro-cli queryruns SQL and FTS5 search against the mirror without API round-trips. - Agent-safe by default. No ANSI colors or formatting in output.
Exit codes: 0 success, 2 usage error, 3 not found, 4 auth error,
5 API error, 7 rate limited, 10 config error.
This repo ships a SKILL.md that drives the CLI from Claude Code. Install
the binary first (see Install above), then either drop
SKILL.md into your project's .claude/skills/miro-cli/ directory or load
it via your skills-installer of choice. The skill verifies the binary is on
$PATH before running any command.
For Miro MCP tools (board operations, stickies, frames, diagrams), use the separate miro-mcp-server project instead. This CLI is shell + skill only.
miro-cli reads credentials from two sources, in precedence order:
--token <value>on any commandMIRO_ACCESS_TOKENenvironment variable
A config file is not currently supported.
Check what the CLI resolved without touching a board:
miro-cli auth status # is a token configured, and from where?
miro-cli auth status --verify # also confirm the token is currently valid
miro-cli auth status --json # machine-readable {token_present, source, verified, status}auth status exits 0 when a token is present (and, with --verify, valid),
10 when none is configured, and 4 when --verify is rejected by the API.
Print the installed build with miro-cli --version or miro-cli version --json.
Authentication errors (exit code 4)
- Verify the environment variable is set:
echo $MIRO_ACCESS_TOKEN - Confirm the CLI sees a valid token:
miro-cli auth status --verify --json - Try passing the token explicitly on one command:
miro-cli --token <value> boards list - Generate a fresh token at https://miro.com/app/settings/user-profile/apps
Not found errors (exit code 3)
- Check the resource ID is correct
- Run the
listverb under the relevant group to see available items
Miro Developer Platform background, in case this is your first time touching the REST API: