Skip to content

Repository files navigation

Publicist

A PR agency in a box — an earned-media campaign run by a small team of scheduled agents.

Website · Quickstart · Architecture · Apache 2.0


Publicist runs the unglamorous half of public relations: researching outlets, writing a pitch that could only have been sent to that outlet, sending it, handling the reply, chasing exactly twice, answering journalist source requests, and reporting to the client every Friday whether the week went well or badly.

It is not a bulk-email tool. Every guarantee in it exists to stop the system doing the one thing that destroys an earned-media campaign: contacting the same person twice, or contacting them with something generic.

pip install git+https://github.com/chasewhughes/publicist   # not on PyPI yet
publicist init ~/my-campaign
cd ~/my-campaign && publicist doctor
publicist run strategist

New campaigns default to a mail provider that sends nothing and writes would-be messages to logs/outbox/. You have to choose to go live.


The agency

Seven single-purpose agents run on a cron schedule, in the order a real agency works: research in the morning, outreach after it, oversight at the end of the day. None of them remembers anything.

flowchart TB
    subgraph agency["The five daily lanes — one agent each"]
        direction LR
        S["🔍 <b>Strategist</b><br/>07:30 daily<br/><i>researches outlets,<br/>writes pitch briefs</i>"]
        O["✉️ <b>Outreach</b><br/>08:00 daily<br/><i>sends pitches,<br/>answers replies</i>"]
        R["📰 <b>Source requests</b><br/>3× weekdays<br/><i>answers journalist<br/>queries, fast</i>"]
        B["🌐 <b>Bookings</b><br/>Mon/Wed/Fri<br/><i>works booking<br/>platforms</i>"]
        M["📊 <b>Manager</b><br/>19:00 daily<br/><i>QC, priorities,<br/>client reporting</i>"]
    end

    S -->|pitch brief| O
    O -.->|ESCALATE| M
    R -.->|ESCALATE| M
    B -.->|ESCALATE| M
    M -.->|guidance| S

    agency --> CRM[("🗄️ <b>Campaign CRM</b><br/>targets · threads · messages<br/>briefs · budget · approvals<br/><br/><i>the only memory</i>")]
    agency --> MAIL["📬 <b>Mail provider</b><br/>server-side threads"]

    M ==>|weekly brief<br/>approvals| CLIENT["👤 <b>The client</b>"]

    style CRM fill:#1f2937,stroke:#60a5fa,stroke-width:3px,color:#f9fafb
    style CLIENT fill:#065f46,stroke:#34d399,stroke-width:2px,color:#ecfdf5
    style agency fill:#0f172a,stroke:#334155,color:#e2e8f0
Loading

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. The manager pushes guidance back down the same way. A handoff written to disk survives a crash; a handoff held in a conversation does not.

Agent When What it does Can it send?
🔍 strategist daily, early Researches each outlet's recent work, picks the one angle that fits, writes a pitch brief No
✉️ outreach daily Sends pitches from those briefs, handles inbound replies, chases exactly twice Yes
📥 inbox daily, afternoon Inbound triage only — no new cold pitches Replies only
📰 requests 3× weekdays Answers journalist source requests before the query closes Yes
🌐 bookings Mon/Wed/Fri Works booking platforms through a real browser Yes
📊 manager daily, evening Reviews every lane, enforces the budget, and is the only agent that emails the client Client only
✍️ writer on demand Drafts a bylined article against an accepted commission No

Before any pitch goes out, the sending agent runs it past a critic persona — a veteran booker who rejects anything generic, over-claimed, or longer than 120 words — and revises until the verdict is SEND.

Every lane is stateless. It starts with no memory of yesterday, does one job, records what it did, and exits. All durable memory lives in a SQLite CRM and the mail provider's own thread store.

That constraint is the design. A long-running agent that holds a campaign in its context drifts, forgets, and eventually re-pitches someone it already spoke to. A short-lived process that is forced to reconstruct state from a database before it writes a word cannot drift, because there is nothing to drift from. The interesting engineering is not in the agents — it is in the contract that makes forgetting safe.

Read docs/architecture.md for the full argument, and src/publicist/prompts/STATE.md for the contract itself, which is the file the agents actually read.

The guarantees

These are enforced in code and tested, not merely requested in a prompt:

  • You cannot pitch the same target twice. crm claim is a conditional UPDATE; only one caller wins. A run that crashes between claiming and sending is recovered on the next run by checking whether a thread exists — never by guessing.
  • You cannot approach someone already approached on another channel. 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 treated as permission to try again through a different door.
  • You cannot exceed the sending ramp. A new domain that opens at forty cold emails a day gets filtered, permanently on any useful timescale.
  • Two lanes cannot run at once. A campaign-wide flock, released by the kernel if a process dies.
  • A reply cancels the chase. Logging an inbound message clears the follow-up timer in the same transaction.

What it will not do

  • Defeat bot protection. There is no CAPTCHA solver and no challenge bypass. When a platform puts up a wall, the run stops and asks a human to sign in once with publicist login. See docs/browser-lane.md for the reasoning.
  • Claim things that are not true. Every campaign has a positioning.md defining the outer bound of what may be said about the client. The lanes are told, repeatedly, that a pitch needing a bigger claim is a pitch that needs a different angle.
  • Hide what it is. When a journalist's request says they do not want AI-written submissions, the default policy is to disclose and offer a call instead — or to skip entirely when the wording is stronger than a preference.

What it plugs into

Claude Agent SDK required Runs every lane. Works with a Claude Code subscription login or an ANTHROPIC_API_KEY.
SQLite required The campaign CRM. One file, no server, trivially backed up.
AgentMail optional A hosted inbox with real server-side threading, so an agent can reply into a conversation months later.
Console provider default Sends nothing. Writes every would-be message to logs/outbox/.
Podcast Index optional Four million public feeds. The strategist's discovery engine. Free credentials.
Playwright optional The browser lane, for platforms that have no API.
cron optional Scheduling. publicist crontab prints the lines.

Providers are pluggable — adding SMTP, Postmark or SES means implementing four methods. See docs/providers.md.

Requirements: Python 3.11+ and access to Claude. Everything else is optional.

Docs

Quickstart From install to a first dry run
Architecture Why stateless lanes plus a CRM, and what it buys
Campaign setup Writing positioning.md, policies.md, and a target list
Operations Cron, logs, going live, what to check weekly
Browser lane Writing a platform adapter, and the line it will not cross
Providers Adding a mail provider

Status

Alpha. The orchestration core has been running a real campaign daily since July 2026; the packaging, the provider abstraction and the example campaign are new. Interfaces may change before 1.0.

License

Apache 2.0. See LICENSE.

About

A PR agency in a box — an earned-media campaign run by a team of stateless, scheduled agents.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages