- name
- pp-substack
- description
- Run your Substack growth and authoring loop from the command line — publish rich drafts, manage a multi-publication portfolio, and measure what drives growth. Trigger phrases: `post a substack note`, `schedule a week of substack notes`, `find substack swap partners`, `which of my notes drove subs`, `what's my engagement reciprocity`, `voice-match a substack note`, `best time to post on substack`, `create a substack draft`, `sync my substack portfolio`, `top posts across my publications`, `search my substack posts`, `use substack`, `run substack`.
- author
- user
- license
- Apache-2.0
- argument-hint
- <command> [args] | install cli|mcp
- allowed-tools
- Read Bash
- metadata
- {"openclaw":{"requires":{"bins":"[Truncated]"},"install":["[Truncated]"]}}
# Substack — Printing Press CLI
## Prerequisites: Install the CLI
This skill drives the `substack-pp-cli` binary. **You must verify the CLI is installed before invoking any command from this skill.** If it is missing, install it first:
1. Install via the Printing Press installer. It defaults binaries to `$HOME/.local/bin` on macOS/Linux and `%LOCALAPPDATA%\Programs\PrintingPress\bin` on Windows:
```bash
npx -y @mvanhorn/printing-press-library install substack --cli-only
```
2. Verify: `substack-pp-cli --version`
3. Ensure the reported install directory is on `$PATH` for the agent/runtime that will invoke this skill.
If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.5 or newer):
```bash
go install github.com/mvanhorn/printing-press-library/library/media-and-entertainment/substack/cmd/substack-pp-cli@latest
```
If `--version` reports "command not found" after install, the runtime cannot see the binary directory on `$PATH`. Do not proceed with skill commands until verification succeeds.
## When to Use This CLI
Reach for this CLI when an agent needs to operate a Substack publication end-to-end: posting Notes on a cadence, drafting and publishing long-form, engaging with niche writers, finding swap partners, and measuring which content actually drove subs. It is the right pick over WriteStack/StackSweller when you need agent-native plumbing (--json, --select, --dry-run, typed exit codes), offline-first analytics (every join runs locally over SQLite), or coverage of the writer surface those tools don't expose. Engage write actions (`engage like`, `engage restack`, `engage restack-with-comment`) print a curl-equivalent by default and only fire with `--send`; treat the print-curl output as a preflight, not a live action.
## Unique Capabilities
These capabilities aren't available in any other tool for this API.
### Local state that compounds
- **`growth attribution`** — Connect every Note you posted to the paid and free subscribers that actually arrived in the 24-hour window after, so you stop guessing which content drove growth.
_Pick this over a generic stats call when an agent needs to decide which Note formats to repeat next week._
```bash
substack-pp-cli growth attribution --days 30 --json --select rank,note_id,note_excerpt,subs_acquired,paid_subs_acquired
```
- **`engage reciprocity`** — See net-give/net-take per writer you engage with — who reciprocates your restacks/comments, who quietly free-rides on yours.
_Use when an agent is deciding whether to keep investing in a swap partner; surfaces relationships before they go stale._
```bash
substack-pp-cli engage reciprocity --days 30 --agent --select handle,outgoing,incoming,net,drift
```
### Algorithm-aware automation
- **`notes schedule --guard`** — Refuse to fire (or queue) a Note that lands less than 30 minutes after your last own-Note or violates your time-of-day rotation. Returns typed exit 2 with a JSON diagnosis.
_Stops an agent from accidentally torching its own reach by dumping a queue all at once._
```bash
substack-pp-cli notes schedule --at 2026-05-10T13:00:00Z --body "hook line\n\nbody" --guard --json
```
- **`growth best-time`** — Top day-of-week × hour cells ranked for whichever growth signal you pick (paid subs, likes, restacks, or comments) — not a single average.
_An agent picking when to schedule tomorrow's Notes can ask for the goal it's optimizing instead of guessing._
```bash
substack-pp-cli growth best-time --days 90 --for-goal subs --json --select day_of_week,hour,rate,sample_size
```
### Pattern intelligence
- **`discover patterns`** — Mechanically extracts which hook patterns (curiosity-gap colon, 3-sentence formula, em-dash reframe, question opener) actually rank in a niche, with restack/comment ratios.
_An agent drafting Notes can ask which hook shape currently outperforms in this niche before generating._
```bash
substack-pp-cli discover patterns --niche productivity --sort restacks --since 14d --agent --select pattern,sample_count,avg_restacks,avg_comments,top_example
```
- **`voice fingerprint`** — Measurable voice profile — sentence length, em-dash rate, colon-hook rate, hook-line ratios, vocabulary uniqueness — for any handle, with --diff to compare against another writer.
_An agent drafting Notes for a ghostwriter client can verify the output stays inside the client's voice envelope._
```bash
substack-pp-cli voice fingerprint --handle maya --diff devon --json --select metric,self,other,delta
```
### Network leverage
- **`recs find-partners`** — Score candidate publications for a Substack Recommendations swap by mutual-overlap density across followee + recommendation graphs.
_An agent running a weekly cross-promo pass can rank candidates instead of pitching cold._
```bash
substack-pp-cli recs find-partners --my-pub on --top 20 --json --select rank,handle,pub,overlap_score,shared_followees
```
- **`growth pod`** — Given a list of handles, render a member × member engagement matrix — last 30 days of restacks/comments/likes between every pair.
_An agent organizing a mutual-aid pod can see who's net-positive vs free-riding without a spreadsheet._
```bash
substack-pp-cli growth pod --members maya,devon,priya,jordan --days 30 --json
```
### Authoring with rich field control
- **`drafts create` / `drafts update`** — Full Substack draft API surface: 30+ flags covering title, subtitle, body (Markdown auto-converts to ProseMirror), section-id, type (newsletter/podcast/video/thread), audience, bylines, SEO metadata, social title, cover image, comment settings, podcast/video URLs, and visibility toggles. The only authoring path that gives agents field-level control without fighting a web editor.
_Use when an agent is constructing a complete long-form post from structured data — research summary, translated copy, ghostwritten piece — and needs paywall, SEO, and section placement set in one command._
```bash
export SUBSTACK_PUBLICATION=mypub
substack-pp-cli drafts create --title "Why X matters" \
--body-file ./post.md --audience only_paid \
--seo-title "X explained" --seo-description "How X affects Y" \
--cover-image https://substackcdn.com/.../cover.jpg --json
```
### Portfolio & analytics (local columnar store)
These commands read a **local SQLite store** populated by `portfolio sync`. The workflow is:
```
auth login --chrome → export SUBSTACK_PUBLICATION=<your-pub> → portfolio sync → portfolio / posts best / grep / subs churn / …
```
Custom-domain publications are supported: `auth login --chrome` captures the Creator-session cookie from the custom domain automatically.
- **`portfolio sync`** — The data-population command. Discovers every publication you own and writes posts, subscribers, and drafts into the local columnar store. Must be run before `portfolio`, `posts best`, `grep`, `schedule board`, `subs churn`, and `subs cross-sell` can return cross-publication data.
```bash
export SUBSTACK_PUBLICATION=mypub
substack-pp-cli portfolio sync --json
```
- **`portfolio`** — One-screen status of every publication you own: subscriber count, paid count, posts published, drafts pending, next scheduled. No tab-switching, no CSV exports.
```bash
substack-pp-cli portfolio --json
```
- **`posts best`** — Rank posts by views, likes, comments, or restacks within a window. `--cross-pub` aggregates across all your publications.
_Use when an agent is deciding which posts to twin into a new publication or surface in a weekly newsletter._
```bash
substack-pp-cli posts best --by restacks --window 30d --cross-pub --json
substack-pp-cli posts best --by views --limit 5 --publication mypub-en
```
- **`posts twin <slug> --to <pub>`** — Duplicate a published post into another publication you own as a draft. Preserves paywall markers, section mapping, and re-uploads images to the target CDN.
```bash
substack-pp-cli posts twin my-en-slug --to mypub-de --dry-run --json
substack-pp-cli posts twin my-en-slug --to mypub-de
```
- **`posts pair <en> <de>` / `posts pairs [--missing]`** — Record EN↔DE post pairings in a local table. `--missing` lists posts without a recorded twin — feed that list into `posts twin` to spin up the missing translations.
```bash
substack-pp-cli posts pair my-en-slug my-de-slug
substack-pp-cli posts pairs --missing --publication mypub-en --json
```
- **`grep <query>`** — FTS5 full-text search across synced posts, notes, and comments, ranked by bm25, returning snippets and source URLs. Optional `--scope`, `--publication`, and `--since` filters.
```bash
substack-pp-cli grep "yield curve" --json
substack-pp-cli grep "rate hike" --scope posts --publication mypub-en --since 2024-01-01
```
- **`schedule board`** — ASCII calendar of the next N days showing scheduled posts across every publication you own. Multi-publication editorial overview in one screen.
```bash
substack-pp-cli schedule board --days 30 --json
```
- **`subs churn`** — Diff subscriber snapshots: who newly subscribed, who unsubscribed, who upgraded free→paid, who downgraded paid→free. Run `--snapshot` at least once first to create a baseline.
```bash
substack-pp-cli subs churn --snapshot
substack-pp-cli subs churn --since 7d --json --publication mypub-paid
```
- **`subs cross-sell`** — Emails that pay on at least one of your publications but are free or absent on the others. Requires 2+ owned publications in the local store. The cross-sell list Substack's UI does not ship.
```bash
substack-pp-cli subs cross-sell --json --limit 100
```
## Command Reference
**categories** — Site-wide Substack category list — culture, technology, food, etc.
- `substack-pp-cli categories list` — List all Substack categories
- `substack-pp-cli categories list-publications` — List publications in a category
**comments** — Long-form post comments (distinct from Notes)
- `substack-pp-cli comments get` — Get a single comment by ID (same shape as a Note — Substack treats them uniformly)
- `substack-pp-cli comments list` — List comments on a post
**discover** — Discovery surfaces — search publications, embed metadata
- `substack-pp-cli discover` — Search Substack publications by query
**drafts** — Drafts CRUD + publish + schedule
- `substack-pp-cli drafts create` — Create a new draft
- `substack-pp-cli drafts delete` — Delete a draft
- `substack-pp-cli drafts get` — Get a draft by ID
- `substack-pp-cli drafts list` — List drafts
- `substack-pp-cli drafts prepublish` — Validate a draft for publication; returns blockers
- `substack-pp-cli drafts publish` — Publish a draft now
- `substack-pp-cli drafts schedule` — Schedule a draft for future publish (or unschedule with --post-date null)
- `substack-pp-cli drafts update` — Update an existing draft
**feed** — RSS feed for a publication
- `substack-pp-cli feed` — RSS XML feed (returns XML; use `--raw` to dump)
**images** — Image upload (data-URI JSON, not multipart)
- `substack-pp-cli images` — Upload an image; returns CDN URL. Body is data-URI JSON.
**inbox** — Authenticated reader feed (home feed) — Notes + posts surfaced for the current user
- `substack-pp-cli inbox home` — Authenticated home feed
- `substack-pp-cli inbox reader-posts` — Posts feed for current user
**notes** — Substack Notes — short-form posts (Substack treats Notes as comments internally)
- `substack-pp-cli notes create` — Post a new Note (POST /comment/feed). Body is ProseMirror JSON.
- `substack-pp-cli notes get` — Get a single Note by ID
- `substack-pp-cli notes list-by-profile` — List Notes by a profile (cursor pagination)
- `substack-pp-cli notes reply` — Reply to an existing Note (parent_id + ProseMirror body)
**grep** — Full-text search across synced posts, notes, and comments
- `substack-pp-cli grep <query>` — FTS5 search ranked by bm25, returning snippets and source URLs. Flags: `--scope posts|notes|comments|all`, `--publication`, `--since`, `--limit`
**portfolio** — Multi-publication status dashboard and data-population
- `substack-pp-cli portfolio` — One-screen status of every publication you own (subs, paid, posts, drafts, next scheduled). Run `portfolio sync` first.
- `substack-pp-cli portfolio sync` — Discover every publication you own and populate the local columnar store (publications/posts/subscribers/drafts). The prerequisite for all cross-publication analytics commands.
**posts** — Long-form posts and archives on a specific publication
- `substack-pp-cli posts archive` — Public archive of a publication's posts
- `substack-pp-cli posts best` — Rank cached posts by engagement metric (`--by views|likes|comments|restacks`, `--window`, `--cross-pub`, `--limit`, `--publication`)
- `substack-pp-cli posts get-by-slug` — Get a published post by URL slug
- `substack-pp-cli posts list-published` — List published posts on the publication (auth required)
- `substack-pp-cli posts pair <en-slug> <de-slug>` — Record an EN↔DE translation pairing in the local table
- `substack-pp-cli posts pairs` — List recorded post pairs; `--missing` shows posts without a twin; `--publication` filters to one pub
- `substack-pp-cli posts ranked-authors` — Ranked list of authors for a publication
- `substack-pp-cli posts twin <slug> --to <pub>` — Duplicate a published post into another publication you own as a draft (re-uploads images, preserves paywall markers)
**profiles** — Substack profiles — your own and other writers'
- `substack-pp-cli profiles from-linkedin` — Look up a Substack profile from a LinkedIn handle
- `substack-pp-cli profiles get-by-handle` — Get a public profile by handle (e.g. mvanhorn)
- `substack-pp-cli profiles get-by-id` — Get a public profile by numeric user ID
- `substack-pp-cli profiles handle-options` — Available handle suggestions for the current user
- `substack-pp-cli profiles posts` — All posts by an author across publications
- `substack-pp-cli profiles self` — Get the authenticated user's profile
**recommendations** — Substack Recommendations — outbound (publications I recommend)
- `substack-pp-cli recommendations <publication_id>` — List the publications a publication recommends
**sections** — Sections of a publication (newsletters can have multiple)
- `substack-pp-cli sections` — List sections + subscriptions
**settings** — Account settings + connectivity probe (used by doctor)
- `substack-pp-cli settings get` — Get account settings
- `substack-pp-cli settings ping` — Connectivity probe (non-destructive PUT used by doctor)
**schedule** — Cross-publication editorial scheduling
- `substack-pp-cli schedule board` — ASCII calendar of the next N days (`--days`) of scheduled posts across all owned publications
**subs** — Subscriber count, churn diff, and cross-sell analytics
- `substack-pp-cli subs authors` — List bylined authors of a publication
- `substack-pp-cli subs churn` — Diff subscriber snapshots (new/unsubscribed/upgraded/downgraded). Use `--snapshot` to create a baseline, then `--since` to diff. Flags: `--publication`, `--since`, `--snapshot`
- `substack-pp-cli subs count` — Get subscriber count (read off the launch-checklist payload)
- `substack-pp-cli subs cross-sell` — Emails paid on one publication but free/absent on others (requires 2+ owned pubs in local store). Flags: `--limit`
**tags** — Post tags
- `substack-pp-cli tags create` — Create a new tag
- `substack-pp-cli tags list` — List all tags for the publication
## Freshness Contract
This printed CLI owns bounded freshness only for registered store-backed read command paths. In `--data-source auto` mode, those paths check `sync_state` and may run a bounded refresh before reading local data. `--data-source local` never refreshes. `--data-source live` reads the API and does not mutate the local store. Set `SUBSTACK_NO_AUTO_REFRESH=1` to skip the freshness hook without changing source selection.
Covered paths:
- `substack-pp-cli categories`
- `substack-pp-cli categories get`
- `substack-pp-cli categories list`
- `substack-pp-cli categories search`
- `substack-pp-cli drafts`
- `substack-pp-cli drafts get`
- `substack-pp-cli drafts list`
- `substack-pp-cli drafts search`
- `substack-pp-cli inbox`
- `substack-pp-cli inbox get`
- `substack-pp-cli inbox list`
- `substack-pp-cli inbox search`
- `substack-pp-cli inbox-posts`
- `substack-pp-cli inbox-posts get`
- `substack-pp-cli inbox-posts list`
- `substack-pp-cli inbox-posts search`
- `substack-pp-cli posts`
- `substack-pp-cli posts get`
- `substack-pp-cli posts list`
Auf GitHub ansehen