CLI
Manage posts, publications, subscribers, and coins from the command line. Built for humans, agents, and CI.
The Paragraph CLI is a command-line tool for managing posts, publications, subscribers, and coins. It works for both human and programmatic (agent) usage.
Prerequisites: Node.js version 18 or higher (not needed for the Homebrew install).
Install
npm install -g @paragraph-com/clibrew tap paragraph-xyz/tap && brew install paragraphAuthentication
Get your API key in the app at app.paragraph.com under Settings → API keys.
Authenticate
paragraph loginThis opens Paragraph's authorization page in your browser. Sign in, choose the
publication to connect, then approve the request. Always open the verification
URL returned by Paragraph rather than constructing one yourself. The stable
paragraph.com/api/auth URL redirects to the maintained writer app.
Or provide the key directly:
paragraph login --token <your-api-key>Verify
paragraph whoamiFor CI or agent environments, you can pipe the token or set an environment variable:
echo "<your-api-key>" | paragraph login --with-token
PARAGRAPH_API_KEY=<your-api-key> paragraph post listCredentials are stored in ~/.paragraph/config.json (mode 0600).
Quick examples
paragraph post create --title "My Post" --file ./draft.md
paragraph post publish my-post
paragraph post list
paragraph search post --query "ethereum"Run paragraph --help for all commands, or paragraph <command> --help for details on any command.
Using an AI agent like Claude Code or Cursor? Install the Paragraph CLI skill to teach it how to use the CLI:
npx skills add paragraph-xyz/skill --skill paragraph-cliCommand reference
Posts
List posts
paragraph post list
paragraph post list --status draft
paragraph post list --limit 50 --cursor <cursor>
# List posts from any publication (public, no auth required)
paragraph post list --publication <id-or-slug>Get a post
Accepts an ID, URL, or @publication/slug:
paragraph post get <post-id>
paragraph post get @yearn/some-post-slug
paragraph post get https://paragraph.com/@yearn/some-post-slugExtract a single field (raw value to stdout, pipeable):
paragraph post get <id> --field markdown > post.md
paragraph post get <id> --field titleCreate a post
Creates a draft by default:
paragraph post create --title "My Post" --text "# Hello World"
paragraph post create --title "My Post" --file ./draft.md
cat draft.md | paragraph post create --title "From Stdin"
paragraph post create --title "Post" --text "Content" --subtitle "Summary" --tags "web3,defi"Update a post
paragraph post update <id-or-slug> --title "New Title"
paragraph post update <id-or-slug> --file ./updated.md --tags "new,tags"Post lifecycle
paragraph post publish <id-or-slug>
paragraph post publish <id-or-slug> --newsletter # publish + email subscribers
paragraph post draft <id-or-slug> # revert to draft
paragraph post archive <id-or-slug>Preview and test
paragraph post publish <id-or-slug> --dry-run
paragraph post delete <id-or-slug> --dry-run
paragraph post test-email <id> # send test email (drafts only)Delete a post
paragraph post delete <id-or-slug>
paragraph post delete <id-or-slug> --yes # skip confirmationBrowse posts
paragraph post by-tag defi --limit 20
paragraph post feed --limit 10Shortcuts
Top-level shortcuts for common operations:
paragraph create --title "Quick Post" --text "Content"
paragraph update my-post-slug --title "Updated"
paragraph delete my-post-slug --yesContent
Short-form drafts: X posts and threads, LinkedIn posts, one-off emails, and X Articles. These commands only draft. Nothing is posted, emailed, or scheduled, and the writer sends it from the app. Long-form posts are paragraph post.
--kind is tweet, linkedin, newsletter, or x_article, and the body flags follow the kind: --tweet (repeatable, one per post in a thread), --subject and --preheader for a newsletter, --headline and --canonical-url for an Article. Long text comes from --text, --file, or stdin.
paragraph content create --kind tweet --title "Thread" --tweet "First." --tweet "Second."
paragraph content create --kind newsletter --title "October update" --subject "What we shipped" --file ./body.md
paragraph content list --kind tweet --status draft
paragraph content get <id>
paragraph content update <id> --text "Rewritten, and shorter."
paragraph content archive <id>
paragraph content restore <id>A piece with a queued send is locked: editing its body is refused (cancel the schedule in the app first), but renaming always works. lockedReason says why, or is null when the piece is editable.
Content groups
A group is one identity for a post and everything made out of it, which is what a writer sees as a single stacked row under Content. Seed the group from the post, then draft into it with --bucket:
paragraph content bucket create <post-id>
paragraph content create --kind tweet --title "Thread" --tweet "First." --bucket <bucket-id>
paragraph content bucket list
paragraph content bucket get <bucket-id>
paragraph content bucket for-post <post-id>bucket create is safe to repeat: a post that already has a group gets the same id back. content update <id> --bucket <bucket-id> groups a draft you made earlier. Taking a piece back out of a group is done in the app.
Publications
paragraph publication get @variantwriting
paragraph publication get blog.variant.fund
paragraph publication get <publication-id>Search
paragraph search post --query "ethereum"
paragraph search blog --query "web3"Subscribers
paragraph subscriber list --limit 100
paragraph subscriber count <publication-id>
paragraph subscriber add --email user@example.com
paragraph subscriber add --wallet 0x1234...abcd
paragraph subscriber import --csv subscribers.csvCoins
paragraph coin get <id-or-address>
paragraph coin popular --limit 10
paragraph coin search --query "test"
paragraph coin holders <id-or-address> --limit 50
paragraph coin quote <id-or-address> --amount <wei>Users
paragraph user get <user-id>
paragraph user get 0x1234... # by wallet addressAgent and programmatic usage
The CLI is designed for use by AI agents and scripts.
JSON output
All commands support --json. Data goes to stdout, status messages to stderr:
paragraph --json post list | jq '.data[0].title'
paragraph --json post get <id> | jq '.markdown'
paragraph --json search post --query "web3" | jq '.length'Paginated commands return:
{
"data": [{ "id": "...", "title": "..." }],
"pagination": { "cursor": "abc123", "hasMore": true }
}Single-item commands return the object directly:
{ "id": "...", "title": "...", "markdown": "..." }Structured errors
In --json mode, errors are structured JSON on stderr with a non-zero exit code:
{ "error": "Not found.", "code": "NOT_FOUND", "status": 404 }Error codes: UNAUTHORIZED, FORBIDDEN, NOT_FOUND, RATE_LIMITED, SERVER_ERROR, REQUEST_FAILED, CLIENT_ERROR, UNKNOWN.
Non-interactive safety
deleterequires--yesin non-TTY environmentsloginsupports--with-tokenfor stdin piping and--tokenfor direct input- Destructive commands support
--dry-run - Set
PARAGRAPH_NON_INTERACTIVE=1orCI=trueto force CLI mode
Environment variables
| Variable | Purpose |
|---|---|
PARAGRAPH_API_KEY | API key (alternative to login) |
PARAGRAPH_API_URL | Custom API base URL |
PARAGRAPH_NON_INTERACTIVE | Set to 1 to disable TUI |
CI | Set to true to disable TUI |
Interactive TUI
Running paragraph with no arguments launches an interactive terminal UI with menus, scrollable lists, and keyboard navigation.
The TUI is disabled automatically when:
--json,--help, or--versionflags are used- stdout is not a TTY (e.g., piped output)
CI=trueorPARAGRAPH_NON_INTERACTIVE=1is set