
The problem
AI agencies running content and SEO pipelines lose runs to silent failures, babysit them by hand, and manage approvals in spreadsheets and Slack.
Who it is for
AI agencies running content and SEO operations, executing dozens of pipelines a day. They were the first target customer.
What I built
A multi-tenant operations platform that runs pipelines (research, human approval, write, publish) as durable Temporal workflows. Runs survive worker crashes, pause at human approval gates and resume on a click, stream live progress to a dashboard, and leave a replayable audit trail. Each AI step is a specialist agent: a LangGraph.js sub-graph calling models through OpenRouter on the customer's own key.
Built with
- Frontend
- Next.js 16
- React 19
- React Flow
- TanStack Query
- Tailwind CSS v4
- Lenis
- Backend
- Fastify 5
- Node.js
- Better Auth
- Drizzle
- Zod
- pino
- Durable Execution
- Temporal (TS SDK)
- LangGraph.js
- Vercel AI SDK
- OpenRouter
- Data & Events
- PostgreSQL 16
- Redis 7
- Socket.IO
- Infra
- Docker Compose
- Turborepo
- pnpm workspaces
- Playwright
- Vitest
Key decision
Treat Temporal event history as the execution source of truth and model human approval as a durable signal rather than a polling workflow.
Proof
Scripted kill-the-worker and pause-resume demos drive the real product API end to end, proving crash recovery and approval handling.
In detail
- Approve to resume
- <2s
- Lost runs on crash
- 0
- Apps: web, server, worker
- 3
- Per-org encrypted key
- BYOK
Architecture
Three deployable apps over a shared core, with Temporal as the execution source of truth:
apps/web ──HTTP/WS──> apps/server ──client──> Temporal ──dispatch──> apps/worker
│ └─subscribe─> Redis <─publish── activities ─┘
└─read──────────> PostgreSQL <─projection writes─┘
activities ──LLM──> OpenRouter (AI SDK)
apps/web(Next.js dashboard) talks only to the server over HTTP and WebSocket with a bearer JWT. It owns no business logic and never touches Temporal, Postgres, or Redis.apps/server(Fastify) hosts Better Auth organizations, REST routes, the Temporal client, the WebSocket relay, and the Redis subscriber. It runs no AI work and no long-lived jobs.apps/worker(Temporal worker) keepsworkflows/as a deterministic sandbox andactivities/for every side effect: LangGraph agents, OpenRouter calls, Postgres and Redis writes.packages/sharedholds Zod schemas, types, and AES-256-GCM crypto.packages/dbholds the Drizzle schema and org-scoped repository factories shared by server and worker, so tenancy scoping lives in one codebase. Apps never import from each other.
Decision
Temporal history is the source of truth
Execution state lives in Temporal's event history and is never duplicated as
authoritative data. Postgres rows (workflow_runs, activity_log) are
read-side projections for the dashboard; no execution decision ever reads a
projection. Redis is ephemeral: a flush loses live tails and warm caches,
never durable state.
How a run works
Launch from a template or canvas
An operator opens a template (Blog Post Pipeline, SEO Brief, Competitor Research), fills a typed form, and hits launch. No code. The same
graph_speccan be authored visually on the React Flow canvas at/canvas, validated live by the shared Zod schema and versioned with immutable history.Durable execution begins
The generic interpreter workflow runs the versioned
graph_spec, each node a Temporal activity wrapping a LangGraph.js sub-graph with per-activity retry policies and timeouts. A crashed worker's in-flight activity is re-dispatched automatically.Pause at the approval gate
The run suspends on a Temporal signal plus
condition(), holding no worker slot. The approver sees the full research findings and draft in their queue and clicks Approve or Reject. A 24-hour timeout expires the gate; every decision is recorded with who, what, and when.Resume and stream
Approve resumes the run within two seconds; the writing step's LLM output streams token by token to the dashboard over Socket.IO. Reject ends the run gracefully.
Recover or resume-from-step
A failed run restarts from its last successful step using stored outputs, so research is not re-executed and tokens are not re-spent.
Key features
Durable workflow execution
The content pipeline runs as Temporal activities with exponential-backoff
retries. A SIGKILLed worker restarts and the run continues from its exact
position, with the retry visible in step history.
Human-in-the-loop gates
Approval gates are Temporal signals plus condition(): zero polling, no
worker slot held while suspended. The queue renders full context, resume
lands in under two seconds, and a 24-hour timeout marks the gate expired.
One engine, two surfaces
A single interpreter workflow executes a versioned, Zod-validated
graph_spec. Templates are code-curated catalog entries; the React Flow
canvas is a visual editor over the same spec. There is no second execution
path.
Real-time operations dashboard
Org-scoped run list with live status badges over WebSocket, a step timeline rail with live duration counters, streaming LLM output for the active step, and a per-step payload inspector.
Audit, replay, and resume
Immutable run history records every step's input, output, error, attempt count, and approval decision. Resume-from-step replays stored outputs so finished work never re-runs and tokens are never re-spent.
Organizations and teams
Better Auth organizations scope every page and API call. Owner and member roles, email-invitation accept flow, and a per-org OpenRouter key verified live against OpenRouter before it is stored encrypted.
Structural multi-tenancy
Every domain row carries a non-null org_id taken from verified JWT claims,
never a request body. A member of one org gets a 404 for every resource of
another, across every route and the WebSocket.
BYO key, self-hostable infra
There is no platform OpenRouter key: agents always run on the org's own key, decrypted only inside the worker activity. The stack is OSS-only (Postgres, Redis, Temporal OSS) with env-driven config.
Note
Core invariants
Seven invariants prevent silent, catastrophic failure: deterministic workflow code, idempotent activities, human waits as signals never activities, Temporal history as the source of truth, org-scoping on every row, customer keys as secrets everywhere, and self-hostable infrastructure.
Tradeoff
Determinism is not optional
Files in workflows/ must never do I/O, import Node built-ins, read env, or
call Date.now() / Math.random(). Temporal replays workflow code against
stored history, so any non-determinism corrupts recovery. Every side effect
is pushed into an idempotent activity.
Outcome
Conductor runs AI pipelines on Temporal with a deterministic workflow sandbox, idempotent activities, human approval gates that hold no resources while suspended, resume-from-step that never re-spends tokens, and multi-tenancy enforced from the first migration. Two scripted proofs double as sales assets: the kill-the-worker reliability demo and the pause-resume approval demo, both driving the real product API end to end. All planned code units ship; remaining work is operator-gated deployment and design-partner testing.
Challenges
- Durable execution on Temporal so a crashed worker's in-flight step is re-dispatched and the run continues from its exact position, never a lost run
- Human approval gates as Temporal signals plus condition(), holding zero worker slots while suspended instead of polling
- Deterministic workflow sandbox: workflows/ do no I/O, read no env, and never call Date.now() or Math.random(); all side effects live in idempotent activities
- Resume-from-step replays stored activity outputs, so finished steps never re-run and tokens are never re-spent
- Structural multi-tenancy from the first migration: every domain row carries org_id from verified JWT claims, cross-org IDs resolve to 404
- One generic interpreter workflow executes a versioned Zod-validated graph_spec shared by both templates and the React Flow canvas, so there is no second execution path
What I learned
- Temporal event history is the single execution source of truth; Postgres holds only read-side projections that no execution decision ever reads
- AES-256-GCM crypto kept outside the shared barrel export so node:crypto never leaks into the web bundle
- Bearer JWT verified statelessly via JWKS carries user id and active org id, avoiding a per-request database hit
- Redis is ephemeral only: pub/sub for status and token streams plus a short cache, so a flush loses live tails but never durable state
- Per-org OpenRouter key encrypted at rest and decrypted only inside the worker activity at call time, never logged or placed in Temporal or Redis payloads
- Template parameter schema generates its own launch form, so the form and the contract cannot drift

Have a role in mind?
Send me a message on X with the role and a link to the posting. My resume has the rest.