An open-source AI agent that writes three LinkedIn posts a week in your voice.
You give it a profile that describes how you write and what you care about. Every week the agent reads what is new in your field, drafts three posts in your style, and runs each one through quality gates so nothing fake or generic gets through. It writes the drafts. You read them, edit for five minutes, and post. The agent never posts for you.
It writes drafts. It refuses to write bad ones. It shows its work.
- Finds what is worth posting about. Pulls real sources from the last seven days in the topics you list (launches, announcements, trends).
- Writes in your voice, not a generic one. It reads your own past posts as voice samples and mirrors your rhythm, not LinkedIn boilerplate.
- Checks itself before it ships. Three gates run on every draft: a fact gate (every claim must cite a source the agent actually read), a voice gate (banned phrases, dashes, broken rhythm), and an LLM judge scored against your best posts. A draft that fails is held back with the reason, not published.
- Shows its work. Every model call is a Langfuse span, so a whole run is one readable trace you can open and inspect.
- Costs about $2 a month and runs on free tiers plus a GitHub Actions cron. No server.
The agent is a LangGraph
StateGraph. Each node is small and single-purpose: it reads the shared
brand.yaml, does one job, writes its result onto graph state, and emits a
Langfuse span. The graph wiring lives in
packages/engine/src/graph.ts.
| Node | Model | Job |
|---|---|---|
| scout | Haiku | Find sources from the last 7 days (web search, RSS fallback). |
| strategist | Sonnet | Pick one angle per day, pinned to that day's pillar in brand.yaml. |
| draft | Sonnet x3 | Write three drafts in parallel. Each emits claims[] with source URLs. |
| critic | Sonnet x3 | Read each draft like a target reader. Returns approve / fix-soft / fix-block. |
| gate | none | Deterministic. Fact gate, voice gate, then the LLM judge. Decides publish or skip. |
Two decision points make the "when not to publish" logic explicit:
- The retry loop. A conditional edge from
criticback todraft. If any day comes backfix-blockand there is retry budget left (max_retry_loops, default 2), that day is redrafted. Otherwise the run moves on to the gates. - The publish/skip fork. The
gatenode runs the fact gate, voice gate, and judge. A draft that passes all three is written todrafts/YYYY-WW/{day}.md. One that fails is written to{day}.SKIPPED.mdwith the reason.
Other technical notes:
brand.yamlis the single source of truth. Voice rules, banned phrases, rhythm bands, per-day pillars, agent models, budgets, and gate modes all live there. To change strategy you edit that file, not code. The engine never hardcodes a voice; the profile path comes from--profile.- Gates are deterministic. They never call an LLM and never throw on a bad
draft. They return
{ pass, ... }and the caller decides. - Cost is tracked on graph state. The run aborts if it crosses the profile's
budgets.cost_usd_per_runcap. - Every LLM call goes through
observe()so it lands in a Langfuse span with token and cost metadata. A run is one trace, one span per node.
See ARCHITECTURE.md for the full map.
flowchart TD
P["brand.yaml<br/>voice rules, pillars, budgets, your past posts"] --> S
subgraph RUN["one LangGraph run = one Langfuse trace"]
S["scout - Haiku<br/>find sources, last 7 days"] --> ST["strategist - Sonnet<br/>one angle per day"]
ST --> DR["draft x3 - Sonnet<br/>parallel, each cites its sources"]
DR --> CR["critic x3 - Sonnet<br/>reads each draft like a real reader"]
CR -->|"fix-block: redraft, up to 2 retries"| DR
CR -->|approve| G{"gates<br/>fact + voice + judge"}
end
G -->|pass| PUB["drafts/YYYY-WW/day.md"]
G -->|fail| SKIP["drafts/YYYY-WW/day.SKIPPED.md<br/>with the reason"]
PUB --> H["you: review Sunday,<br/>edit 5 min, post manually"]
- Set up once. Fork the repo, add your Anthropic API key, and write your
brand.yaml: your topics, your voice rules, your per-day pillars. Drop a few of your own past posts into the profile so the agent learns your tone. - It runs on a schedule. A GitHub Actions cron runs the agent for you. No
server to keep alive. (Run it by hand any time with
pnpm pipeline.) - The agent does the loop. scout to strategist to draft to critic to gates. Bad drafts get retried, then held back if they still fail.
- You get three drafts. Published drafts land in
drafts/YYYY-WW/. Skipped days sit next to them with the reason they were held back. - You review on Sunday. Read the three, edit for about five minutes, post the ones you like. Verify any numbers or facts first.
- You post. Manually, always. The agent never touches LinkedIn.
pnpm install
cp .env.example .env # add your Anthropic key (Langfuse optional)
pnpm pipeline --profile examples/sai-voice --dry-runA run prints week ...: N/3 published, $cost and writes to drafts/.
Make your own voice:
cp -r examples/_template examples/my-voice
# edit examples/my-voice/brand.yaml
pnpm pipeline --profile examples/my-voice- Demo profile:
examples/sai-voice - A sample week of drafts:
drafts/2026-W23(each scheduled run keeps its drafts as a private CI artifact, not a public branch) - Public Langfuse trace: a real run, span per node
v2 is live. The LangGraph engine, Langfuse tracing, Supabase run stats, and the read-only dashboard all ship. Three gates run in blocking mode: a deterministic fact gate (every claim must cite a scouted source), a deterministic voice gate (rhythm and banned-phrase rules), and an LLM judge scored against a golden corpus. See DESIGN.md section 14 and docs/v2/judge-calibration.md for how the gates were calibrated.
MIT licensed. The engine never posts to LinkedIn. A human always posts.