idea-to-prod: from a Notion card to a merged PR, with an agent doing the work
How a six-state Notion pipeline spawns a Spunto worker, hands it to an AI coding agent, and lets it implement, test, and open its own PR — plus the bridge that lets you redirect it from Slack mid-run.
Mathieu
Founder, Spunto
Most "ticket to code" automations stop at spawning an environment. This is what happens after: a pipeline where a single card carries a task from an idea to a merged pull request, with an AI agent doing the implementation, the testing, and the PR itself — on top of nothing more than a worker, a status field, and a couple of webhooks.
Six statuses on one field, on one Notion database, are the whole state machine:
pending setup → running → implementing → to be tested → in review → completedcanceled at any point tears the worker down regardless of the current status.
Two actors share the states:
- The orchestrator — a small webhook-driven service — only touches the edges. It reacts to
runningby spawning a worker, and to a merged PR by moving the card tocompletedand tearing the worker down. - Everything in between —
implementing, testing, advancing toto be tested/in review, opening the PR — is the agent's job, running inside the worker itself.
That split is deliberate — more on why at the end.
A card just needs a title and a description; concatenated, they become the agent's first prompt. A project field maps the card to a target repo plus an org/project pair to spawn the worker in — leave it empty and the pipeline still spawns a worker, but skips everything repo-specific: no clone, no scripted agent session, just a bare environment.
Flipping the status to running fires a webhook. From there the orchestrator does exactly four things:
1. POST /orgs/{org}/projects/{project}/workers → spawn
2. poll GET .../workers/{id} until state == "ready"
3. checkout a dedicated branch in the worker → idea/{cardId}-{slug}
4. open a persistent terminal and launch the agent,
with the card's title + description as its first promptIt writes the worker URL back on the card and flips the status to implementing. That's the entire surface of the orchestrator for a running card — no test runner, no build step, no push, nothing repo-specific baked in.
Every repo wired into the pipeline carries a short, repo-specific runbook at its root — how to reset to a clean environment, how to run the real test suite, what counts as proof, how to advance the card. The agent's first move, before writing any code, is to read that file.
The loop that follows is the same shape for every repo:
- Implement the request on the checked-out branch.
- Reset to a clean environment — not "restart and keep my local state", a real from-scratch environment, so what gets tested is what a reviewer starting from zero would actually see.
- Test for real — drive the feature end to end (a worker actually spawned and taken to completion, a UI flow actually clicked through in a live browser), not a partial suite or an old screenshot.
- Attach proof — a short video is the default; for a backend-only change, the test suite's output is enough.
- If any of that fails, loop back to Implement — the card stays in
implementing, it never advances on a red test.
Only once all four hold does the agent flip the card to to be tested, commit, push, and open the PR itself — then in review.
A GitHub webhook on the merge event is the only remaining trigger. It reads the branch name back to the card id, flips the card to completed, and tears the worker down. Nothing on the Notion side needs to happen after the merge — the loop closes itself.
The one gap in an otherwise self-driving pipeline: once the agent is deep in implementing, you may want to redirect it — without opening a terminal into the worker yourself.
The constraint: an interactive coding-agent session doesn't expose a live "inject a message" channel from the outside — the only documented entry point is what happens when the session pauses. Blocking that pause to wait on an external message freezes the whole session, which isn't what you want mid-implementation.
The workaround reuses a primitive that's already there for a different purpose: a background command that, when it finishes, wakes the agent up with its output.
- A small script opens a socket connection to the chat backend and blocks, waiting for the next human message.
- It's launched as a background task — the coding session keeps running normally in the meantime.
- The moment a message arrives, the script exits.
- The background task finishing is itself the wake signal — the agent is re-invoked with that message as new input.
- The agent handles it, replies, and re-arms the same listener for the next message.
No public webhook, no extra always-on process — just a listener that trades "stay open" for "exit on message", so the exit itself becomes the notification.
The orchestrator only owns state transitions it can verify unambiguously from the outside: worker spawned, PR merged. Everything that requires judgment — is this actually tested, is this actually done — stays with the agent, guided by a plain-text runbook per repo instead of a hardcoded test command.
Adding a new repo to the pipeline is writing one file, not touching the orchestrator's code at all.
Mathieu
Founder, SpuntoBuilding Spunto — self-hosted dev environments for engineers who want their compute to work for them, not against them.
