Substack Skill
Manages publishing and editing for the Alternative Partners Substack publication via the internal REST API. No Playwright, no browser — pure requests with a session cookie.
Auth
This skill requires a connect.sid session cookie from Substack. Store it securely and provide it as the SUBSTACK_SID environment variable (or equivalent in your secrets manager).
This is the connect.sid cookie. Valid for months unless you sign out of Substack in Chrome. To rotate: sign out of Substack → sign back in → open DevTools → copy substack.sid cookie value → update your secrets store.
The publisher module at publishers/substack.py handles auth automatically. Always use it rather than calling the API directly.
API Endpoints (alternativepartners.substack.com)
| Action | Method | Endpoint |
|---|
| Create draft | INLINECODE6 | INLINECODE7 |
| Publish draft |
POST |
/api/v1/drafts/{id}/publish |
| Update existing post |
PUT |
/api/v1/drafts/{id} |
| Fetch post by slug |
GET |
/api/v1/posts/{slug} |
| List posts |
GET |
/api/v1/posts?limit=N |
Key discovery (2026-03-20): PUT /api/v1/drafts/{id} works on already-published posts too — it edits them in place. The post ID is the same as the draft ID used to create it.
Does NOT exist: PUT /api/v1/posts/{id} returns 404. Always use the /drafts/{id} endpoint even for published posts.
Body Format
Substack uses ProseMirror JSON for post bodies. The publisher converts plain text → ProseMirror automatically.
Input format: Plain text with double-newline paragraph breaks.
Output format (internal): ProseMirror doc object, serialized as a JSON string and passed as draft_body.
CODEBLOCK0
Limitation: This produces plain paragraphs only. Bold, headers, lists, links require richer ProseMirror nodes — not yet implemented.
Common Operations
1. Publish a new post
CODEBLOCK1
Or via CLI from the pipeline directory:
CODEBLOCK2
2. Update / edit an existing post
Need the numeric post ID. Get it by fetching the post:
CODEBLOCK3
Then update:
CODEBLOCK4
3. Get a post's ID from its slug
The slug is the last segment of the Substack URL:
https://alternativepartners.substack.com/p/the-revops-ai-reality-check-nobodys
→ slug = INLINECODE22
CODEBLOCK5
4. Save as draft without publishing
CODEBLOCK6
Email Blast Behavior
INLINECODE23 endpoint is called with {"send_email": False} — posts go live on the web but do not trigger a subscriber email blast. This is intentional for automated/pipeline posts.
To send an email blast, Benjamin needs to manually click "Send" in the Substack editor UI. Do not change send_email to True without explicit confirmation.
Pipeline Integration
The AP Content Pipeline at ~/Documents/Codex/Content/ap-content-pipeline/ handles end-to-end publishing including veto window, soft-veto Slack notification, and scheduling. For single one-off posts, call the publisher directly. For managed pipeline runs, use publish_runner.py.
The pipeline also has a research_gate.py that runs a web search competitive sweep + LLM differentiation analysis before drafting. Posts in idea_inbox.json with research_status: "pending" will be researched before drafting. Requires a search API key configured in your environment.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|
| INLINECODE32 | Cookie expired | Rotate: sign out/in of Substack in Chrome, grab new substack.sid, update your secrets store |
| INLINECODE34 → 404 |
Wrong endpoint | Use
/api/v1/drafts/{id} for updates, not
/api/v1/posts/{id} |
|
POST /api/v1/drafts/{id}/publish fails | Post already published | That's OK — post is already live, return the known URL |
| Body renders as one giant paragraph | Missing double-newlines | Input body must use
\n\n between paragraphs |
|
substack.sid not found | Cookie env var not set | Ensure
SUBSTACK_SID is set in your environment before running |
Substack 技能
通过内部 REST API 管理 Alternative Partners Substack 出版物的发布和编辑。无需 Playwright,无需浏览器——纯 requests 配合会话 cookie。
认证
此技能需要来自 Substack 的 connect.sid 会话 cookie。请安全存储,并将其作为 SUBSTACK_SID 环境变量(或密码管理器中的等效项)提供。
这是 connect.sid cookie。除非您在 Chrome 中退出 Substack,否则有效期为数月。轮换方法:退出 Substack → 重新登录 → 打开开发者工具 → 复制 substack.sid cookie 值 → 更新您的密码存储。
publishers/substack.py 中的发布模块会自动处理认证。请始终使用该模块,而不是直接调用 API。
API 端点 (alternativepartners.substack.com)
| 操作 | 方法 | 端点 |
|---|
| 创建草稿 | POST | /api/v1/drafts |
| 发布草稿 |
POST | /api/v1/drafts/{id}/publish |
| 更新已有文章 | PUT | /api/v1/drafts/{id} |
| 通过 slug 获取文章 | GET | /api/v1/posts/{slug} |
| 列出文章 | GET | /api/v1/posts?limit=N |
关键发现(2026-03-20): PUT /api/v1/drafts/{id} 也适用于已发布的文章——它会直接编辑原文。文章 ID 与创建时使用的草稿 ID 相同。
不存在的端点: PUT /api/v1/posts/{id} 返回 404。即使对于已发布的文章,也始终使用 /drafts/{id} 端点。
正文格式
Substack 使用 ProseMirror JSON 格式作为文章正文。发布器会自动将纯文本转换为 ProseMirror 格式。
输入格式: 使用双换行符分隔段落的纯文本。
输出格式(内部): ProseMirror doc 对象,序列化为 JSON 字符串,作为 draft_body 传递。
python
def buildprosemirror_doc(body: str) -> dict:
paragraphs = [p.strip() for p in body.strip().split(\n\n) if p.strip()]
return {
type: doc,
content: [
{type: paragraph, content: [{type: text, text: p}]}
for p in paragraphs
]
}
限制:此方法仅生成纯文本段落。粗体、标题、列表、链接需要更丰富的 ProseMirror 节点——尚未实现。
常见操作
1. 发布新文章
python
from publishers.substack import publish_substack
url = publish_substack(
title=您的文章标题,
body=第一段落。\n\n第二段落。,
publish=True # False = 仅保存为草稿
)
或者通过管道目录下的 CLI 运行:
bash
cd ~/Documents/Codex/Content/ap-content-pipeline
python3 publishers/substack.py 标题 正文第一段。\n\n第二段。
2. 更新/编辑已有文章
需要数字形式的文章 ID。通过获取文章来获取:
bash
curl -s -b substack.sid=$SUBSTACK_SID \
https://alternativepartners.substack.com/api/v1/posts/{slug} \
| python3 -c import json,sys; d=json.load(sys.stdin); print(id:, d.get(id))
然后更新:
python
from publishers.substack import update_substack
url = update_substack(
post_id=191631753,
title=更新后的标题,
body=新的正文内容。\n\n第二段落。
)
3. 通过 slug 获取文章 ID
slug 是 Substack URL 的最后一段:
https://alternativepartners.substack.com/p/the-revops-ai-reality-check-nobodys
→ slug = the-revops-ai-reality-check-nobodys
bash
curl -s -b substack.sid=$SUBSTACK_SID \
https://alternativepartners.substack.com/api/v1/posts/此处填写SLUG \
| python3 -c import json,sys; d=json.load(sys.stdin); print(d.get(id))
4. 保存为草稿而不发布
python
url = publish_substack(title, body, publish=False)
返回:https://alternativepartners.substack.com/publish/post/{id}
邮件推送行为
publish 端点调用时使用 {send_email: False}——文章会在网站上发布,但不会触发订阅者邮件推送。这是为自动化/管道文章特意设计的。
如需发送邮件推送,Benjamin 需要在 Substack 编辑器中手动点击发送。未经明确确认,请勿将 send_email 改为 True。
管道集成
位于 ~/Documents/Codex/Content/ap-content-pipeline/ 的 AP 内容管道处理端到端发布,包括否决窗口、软否决 Slack 通知和排期。对于单次发布的文章,直接调用发布器。对于受管理的管道运行,请使用 publish_runner.py。
管道还有一个 researchgate.py,在起草前运行网络搜索竞争扫描 + LLM 差异化分析。ideainbox.json 中 research_status: pending 的文章将在起草前进行研究。需要在您的环境中配置搜索 API 密钥。
故障排除
| 症状 | 可能原因 | 解决方法 |
|---|
| 401 Unauthorized | Cookie 过期 | 轮换:在 Chrome 中退出/登录 Substack,获取新的 substack.sid,更新您的密码存储 |
| PUT /api/v1/posts/... → 404 |
端点错误 | 更新时使用 /api/v1/drafts/{id},而非 /api/v1/posts/{id} |
| POST /api/v1/drafts/{id}/publish 失败 | 文章已发布 | 没关系——文章已上线,返回已知 URL |
| 正文渲染为一个大段落 | 缺少双换行符 | 输入正文必须在段落之间使用 \n\n |
| 找不到 substack.sid | 未设置 Cookie 环境变量 | 确保运行前在环境中设置了 SUBSTACK_SID |