A PR agency
in a box.
Seven scheduled agents research outlets, write the pitch, send it, handle the reply, and chase exactly twice. Open source. Self-hosted. Apache 2.0.
Seven desks · one filing cabinet · the whole agency
What context loss looks like
The problem
One long-running agent holding a campaign in its context fills up, summarizes its history, and drops the one line that matters: did we already email this person?
It does not fail loudly. It pitches someone a second time, warmly, having lost the memory that there was a first time.
The agency
Seven lanes, one job each, in the order a real agency works.
Research in the morning, outreach after it, oversight at the end of the day. The agents never talk to each other. The strategist hands work to outreach by writing a row in the database; any lane escalates to the manager by appending a line to a file.
Every lane is stateless. It starts with no memory of yesterday, reconstructs what it needs from the CRM, does one job, records it, and exits.
-
weekdays
Requests — answers journalists
Works journalist source-request digests, because a popular query collects hundreds of replies in the first hour.
-
daily
Strategist — reads the room
Researches each outlet's recent work, picks the one angle that fits, and writes a pitch brief. Sends nothing.
-
daily
Outreach — writes and sends
Sends pitches from those briefs, answers inbound replies, and follows up twice — day 5, then day 12 — after which the thread is closed.
-
Mon / Wed / Fri
Bookings — works the platforms
Drives booking platforms through a real browser, capped at ten messages a run. Off by default; it needs an adapter you write.
-
daily
Inbox — afternoon triage
Handles inbound mail only. No new cold pitches, no follow-ups.
-
daily
Manager — runs the agency
Reviews every lane, reads two or three of the day's outbound messages for quality, enforces the budget, and is the only lane that emails the client.
-
on demand
Writer — drafts the byline
Drafts a bylined article against an accepted commission and leaves it in
drafts/for review. Never sends. -
every approach
Critic — kills generic pitches
Runs inside every lane that writes to an outlet. A veteran booker persona that reviews the draft before it goes out: one idea, something real and recent of the recipient's, one credential, 120 words for an email and 90 for a platform message. The lane revises until the verdict is SEND.
Architecture
No lane reads another lane.
A handoff written to disk survives a crash. A handoff held in a conversation does not.
- requests 3× weekdays
- strategist 07:30
- outreach 08:00
- bookings Mon / Wed / Fri
- inbox 16:00
- manager 19:00
- writer on demand
no edges between them
Campaign CRM
sqlite · one file · the only memory
targets · threads · messages
briefs · budget · approvals
Mail provider
server-side threads
How it works
Three steps, then it runs itself.
-
STEP 1
Describe the client
Write
positioning.md— the outer bound of what may be claimed — pluspolicies.mdand a target list. -
STEP 2
Run a lane by hand
New campaigns default to a provider that sends nothing and writes every would-be message to
logs/outbox/. Read them before you go live. -
STEP 3
Put it on cron
publicist crontabprints the lines. From then on the agency runs itself and the manager reports on Fridays.
$ pip install git+https://github.com/chasewhughes/publicist
$ publicist init ~/my-campaign
$ cd ~/my-campaign && publicist crm init
$ publicist doctor
$ publicist run strategist
$ publicist crontab
The guarantees
Enforced in code and covered by tests, not requested in a prompt.
No target is pitched twice
crm claim is a conditional UPDATE, so only one caller can win. A run that crashes mid-send is recovered by checking whether a thread exists — never by guessing.
One door closes all the others
crm already-reached matches on normalized name, domain and address across every lane, and errs deliberately toward false positives. Silence from a target is not permission to knock again.
The sending ramp is not advisory
Eight cold emails a day in week one, rising to forty. A new domain that opens at forty gets filtered, permanently on any useful timescale.
Two lanes never run at once
A campaign-wide flock held for the whole run, released by the kernel if the process dies. A crashed lane cannot wedge the campaign.
A reply cancels the chase
Logging an inbound message clears the follow-up timer in the same transaction. Nobody who answered gets nudged.
What it plugs into
Two requirements. Everything else is optional.
Providers are pluggable — a new mail backend is four methods.
publicist crontab prints the lines.
Python 3.11+ and access to Claude, via a Claude Code subscription login or an ANTHROPIC_API_KEY.
Brand icons courtesy of Simple Icons (CC0-1.0). All marks are the property of their respective owners; their presence indicates interoperability, not endorsement or affiliation.
Read the code before you trust it.
It is alpha: the orchestration core has run a real campaign daily since July 2026, and the packaging is new. Apache 2.0, self-hosted, nothing to sign up for.