FlowForge
A workflow automation engine in .NET, in the spirit of n8n, built to stay correct when something fails halfway through.
- Context
- Personal project
- Started
- Type
- Backend
By the numbers
- HTTP endpoints
- 42
- C# tests, unit to integration
- 361
- end-to-end browser tests
- 38
- PostgreSQL tables
- 18
- recorded architecture decisions
- 17
- lines of test per line of code
- 1.1
Counted in the code in Oct 2026.
Architecture
Why I built it
At work I'm surrounded by automations: scheduled jobs, integrations, records generated from another system. Almost every problem is not "the API doesn't work", it's "the API worked, but something went down right after". I wanted to understand that kind of failure properly, not just wrap it in a try/catch.
FlowForge is the answer I put together: you draw a graph, publish a version, and every event that arrives by webhook becomes an independent execution, processed by a Worker with checkpoints, retries and recovery after a crash.
How it works inside
It is a modular monolith with two processes. The API accepts commands and events and writes the execution and its outgoing message (outbox) in the same PostgreSQL transaction. The Worker publishes that outbox to RabbitMQ, consumes the queue and runs the steps, saving a checkpoint after each one. PostgreSQL is the source of truth; RabbitMQ only carries messages.
What happens when something goes wrong
Each of these cases has an integration test running against real PostgreSQL and RabbitMQ.
- RabbitMQ is down when the webhook arrives: the API still answers 202, and the Worker publishes once the broker is back.
- The same event arrives twice at the same time: the same Idempotency-Key returns the same execution; a different body with the same key gets a 409.
- A Worker hangs and another takes over: a generation token (fencing) stops the old one from writing after it lost ownership.
- The Worker crashes right after sending a POST: the POST is not sent again. The attempt is marked as having an unknown external result instead of pretending nothing happened.
- An hours-long Delay is running: the wait is state in the database, not a sleeping thread, and it survives a restart.
What it does not promise
Exactly-once doesn't exist across external systems, and I chose to say so. FlowForge guarantees at-least-once with local deduplication and shows when the remote effect is unknown. Retries only happen where repeating is safe: idempotent methods or targets that accept an Idempotency-Key.
Where AI came in
I used AI along the way, and it let me move fast. But reviewing what it wrote is where I learned the most: some of the cases above only exist because a review found an execution that could get stuck for hours.
Try FlowForge right here
This is the path an event takes inside FlowForge. Take the queue down to see events waiting in the outbox, or send the same event twice.
Drag to rotate. Click the Webhook to send an event.Tap the buttons to send events and take the queue down.
3D graph with six steps: Webhook, API, Outbox, RabbitMQ, Worker and Target.
Log