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
A published FlowForge workflow, an event arriving by webhook and the path the execution takes through the graph.

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

The webhook reaches the API, which writes the execution and the outbox in the same PostgreSQL transaction. The Worker dispatcher publishes the outbox to RabbitMQ, the engine consumes the queue, saves a checkpoint after each step and calls the external target with safe retries.

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

gabriel@maciel: ~