Paragraph Docs
Developers

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/cli
brew tap paragraph-xyz/tap && brew install paragraph

Authentication

Get your API key in the app at app.paragraph.com under Settings → API keys.

Authenticate

paragraph login

This 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 whoami

For 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 list

Credentials 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-cli

Command 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-slug

Extract a single field (raw value to stdout, pipeable):

paragraph post get <id> --field markdown > post.md
paragraph post get <id> --field title

Create 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 confirmation

Browse posts

paragraph post by-tag defi --limit 20
paragraph post feed --limit 10

Shortcuts

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 --yes

Content

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

Coins

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 address

Agent 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

  • delete requires --yes in non-TTY environments
  • login supports --with-token for stdin piping and --token for direct input
  • Destructive commands support --dry-run
  • Set PARAGRAPH_NON_INTERACTIVE=1 or CI=true to force CLI mode

Environment variables

VariablePurpose
PARAGRAPH_API_KEYAPI key (alternative to login)
PARAGRAPH_API_URLCustom API base URL
PARAGRAPH_NON_INTERACTIVESet to 1 to disable TUI
CISet 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 --version flags are used
  • stdout is not a TTY (e.g., piped output)
  • CI=true or PARAGRAPH_NON_INTERACTIVE=1 is set

Resources

On this page