The agent flow
What happens between pressing Run now and a draft post existing — with the gates, the state machine and the gaps.
docs/AGENT_FLOW.md
Six agents do jobs a person would otherwise do by hand. None of them changes anything on their own: each one works, stops, and asks. This is what happens between pressing Run now and a draft post existing.
The rule underneath all of it is the third non-negotiable in PROJECT_BRIEF.md — nothing publishes without a recorded approval. Every gate below exists to make that structurally true rather than a convention.
The whole path
Two things worth reading off that diagram.
The proposal is written before anybody decides. An agent's whole output lands in the run row
while its status is awaiting_approval. Nothing about that touches your content, your budget or
a platform — so a rejected run costs model tokens and nothing else.
apply.ts is a separate step from execute.ts. Running the agent and applying its result
are different code paths, reached by different requests, minutes or days apart. That separation
is the gate: there is no branch inside the agent where it could write early.
The state machine
state.ts is the only writer of status, and it refuses an illegal move rather than
recording one.
The important edge is the one that is not there: running → applied. An auto-apply mode is
one array entry away in TRANSITIONS, which is exactly why its absence is written down in the
code rather than left to be inferred.
The six agents
| Agent | Kind | Reads | Approving it does |
|---|---|---|---|
| Scout | marketing_scout | published posts + engagement, and the unpublished pipeline | records the read, unlocks the Ideator |
| Ideator | marketing_ideate | an approved Scout run | records the ideas, unlocks the Drafter |
| Drafter | content_draft | an approved Ideator run | creates draft posts |
| Optimiser | campaign_optimisation | ad campaigns + their metric history | files a change into a second review |
| Planner | plan_generation | connected accounts, KPI snapshots, existing budgets | proposes a budget/cadence |
| Site audit | site_audit | crawled pages | records findings; can reach your site |
Only the Drafter creates content. The others record, propose, or unlock — which is why the card for each one states what approving it does, rather than leaving everybody to learn it once by doing it.
The content chain
Three of the six are a sequence. Each needs the previous one approved, not merely run.
gateState decides whether a step can start by looking for an approved parent run. The other
three agents depend on nothing and can run at any time.
The gate is why a freshly seeded database cannot demonstrate the chain in one click: you have to approve a Scout to see the Ideator light up. That is the flow.
Who can do what
| Action | Gate |
|---|---|
| Start a run | organisation on Growth or above; project visible to the caller; rate limit 30/window |
| See a run | the project's own scope — another tenant's run is a 404, not a 403 |
| Approve or reject | permission agents:approve |
| Cancel | any caller who can see the run |
Starting a run is billed (incrementUsage(org, "agent_runs", 1)) and deliberately not
role-gated beyond project visibility — see the gaps below.
Where it runs
npm run dev starts only the API and the web app. The worker is a separate process, so on a
default dev machine every run sits in queued forever and the page shows In progress with
nothing behind it. Start it explicitly:
docker compose up -d # postgres, rabbitmq, minio
cd apps/api && npx tsx --env-file-if-exists=../../.env src/worker.ts
GET /api/v1/health reports queue and worker; both must read up.
Trying it locally
npm run seed # org + owner
npm run seed:agents # history for every agent to read
seed:agents gives each agent something deliberately uneven to find — a standout post, a
decaying campaign, two broken pages — because a clean fixture produces a correct run with nothing
in it, which proves the agent ran but not that it works. It is deterministic and tagged
[fixture], so re-running replaces its own rows and never touches hand-made ones.
The Ideator and Drafter are not seeded. Approve a Scout to unlock the Ideator, approve that to unlock the Drafter — the gate is the part worth testing.
Known gaps
Written down because a flow document that only describes the happy path is how the holes survive.
A run whose dispatch fails is stuck forever. domain.create publishes to the queue and
swallows the error, leaving the row queued on the grounds that it "stays visible as such". It
does not: queued looks identical to a run about to be picked up, nothing retries it, and
nothing ages it out. The publisher solved the same problem with reapStalePublishing; the agents
have no equivalent.
Starting a run has no role check. domain.create verifies the plan and that the project is
visible, and nothing else — so a client or viewer on a Growth organisation can start runs and
spend the billed quota. Approving is properly gated (agents:approve); starting is not.
Scraped pages are treated as trusted. The Site audit reads content from somebody else's website and feeds it to a model with no taint flag on it.
No per-run budget. Runs cost model tokens with no ceiling beyond the rate limit.