Fluxo is a fast, embeddable, deterministic workflow engine written in pure Go. It is designed as a practical alternative to Temporal/Camunda for teams that want workflow reliability without running workflow infrastructure.
Fluxo runs inside your Go service, supports multiple persistence backends, and uses a simple, ergonomic API.
-
Deterministic, retryable workflow execution
-
Pluggable persistence
- In-memory (testing/dev)
- SQLite
- PostgreSQL
- Redis
- MongoDB
-
Built-in asynchronous worker
-
Strongly-typed workflow support (via
TypedStep,TypedLoop,TypedWhile) -
Parallel, conditional, and looping control flow
-
Timers & signals
-
LocalRunner for in-process testing (non-durable)
Fluxo is a library β not a service. You embed it directly into your application.
go get github.com/petrijr/fluxoGo 1.21+ is recommended.
Define a workflow using the builder API and run it using an engine:
package main
import (
"context"
"log"
"github.com/petrijr/fluxo"
)
func createAccount(ctx context.Context, input any) (any, error) {
return map[string]any{"userID": "123"}, nil
}
func sendWelcomeEmail(ctx context.Context, input any) (any, error) {
state := input.(map[string]any)
log.Printf("sending welcome email to %s", state["userID"])
return state, nil
}
func main() {
ctx := context.Background()
flow := fluxo.New("OnboardUser").
Step("createAccount", createAccount).
Step("sendWelcomeEmail", sendWelcomeEmail)
eng := fluxo.NewInMemoryEngine()
if err := flow.Register(eng); err != nil {
log.Fatal(err)
}
inst, err := fluxo.Run(ctx, eng, flow.Name(), nil)
if err != nil {
log.Fatal(err)
}
log.Printf("workflow completed: id=%s status=%s", inst.ID, inst.Status)
}Fluxo supports multiple backends. Definitions are always in-memory; instances and execution history depend on your backend choice.
Use for tests or ephemeral/local execution:
eng := fluxo.NewInMemoryEngine()Embedded durability; ideal default for single-node services:
db, _ := sql.Open("sqlite", "file:fluxo.db?_journal=WAL")
eng, _ := fluxo.NewSQLiteEngine(db)db, _ := sql.Open("pgx", "postgres://user:pass@localhost:5432/fluxo")
eng, _ := fluxo.NewPostgresEngine(db)rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379" })
eng := fluxo.NewRedisEngine(rdb)client, _ := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
eng := fluxo.NewMongoEngine(client)Backend choice does not change workflow code.
Fluxo provides simple, composable workflow primitives.
flow.Step("a", stepA).Step("b", stepB)flow.If("check-limit",
func (input any) bool { return input.(int) < 100 },
fluxo.StepFunc(func (ctx context.Context, in any) (any, error) { return "ok", nil }),
fluxo.StepFunc(func (ctx context.Context, in any) (any, error) { return "too large", nil }),
)flow.Parallel("prepare",
stepFetchUser,
stepFetchSettings,
stepWarmCache,
)Fixed-count:
flow.Loop("repeat", 3, body)While-condition:
flow.While("until-ready",
func (input any) bool { return !input.(State).Ready },
body,
)Avoid any by using strongly-typed steps:
flow.Step("typed", fluxo.TypedStep(func(ctx context.Context, s Counter) (Counter, error) {
s.Value++
return s, nil
}))Typed looping:
flow.Step("loop", fluxo.TypedWhile(
func(s Counter) bool { return s.Value < 5 },
func (ctx context.Context, s Counter) (Counter, error) {
s.Value++
return s, nil
},
))Fluxo workers pull tasks from the task queue and execute them:
w := fluxo.NewWorker(eng, queue)
go w.Run(ctx)Workers can be horizontally scaled.
LocalRunner bundles engine + queue + worker for easy test setups.
runner := fluxo.NewLocalRunner()
runner.StartWorkers(ctx, 1)
runner.StartWorkflowAsync(ctx, "MyFlow", input)Fluxo exposes an Observer interface for logging and metrics.
obs := fluxo.NewLoggingObserver(nil) // uses slog.Default()
eng := fluxo.NewInMemoryEngineWithObserver(obs)metrics := &fluxo.BasicMetrics{}
eng := fluxo.NewSQLiteEngineWithObserver(db, metrics)
snapshot := metrics.Snapshot()Fluxo targets < 1ms overhead per step on typical hardware (excluding user logic). This is enforced via a performance regression test in the repository.
Actual performance varies with backend choice.
- Deterministic workflow planning
- At-least-once step execution
- Durable workflow state when using persistent backends
- Worker crash recovery (persistent backends only)
- No global saga/compensation framework
- No distributed transaction guarantees
- No cross-workflow coordination primitives
- No built-in admin UI or orchestration service
- LocalRunner is not durable and cannot recover from process crashes
These are intentionally not implemented yet but may come next:
- Saga helpers (compensation patterns)
- Better observability integrations (Prometheus, OpenTelemetry)
- Workflow versioning helpers
- CLI tooling for inspecting workflows
- Kafka/NATS queue backends
- More ergonomic DSL for workflow definitions
- Optional workflow visualization tooling
Issues, PRs, and feedback are welcome! This project is still evolving and contributions are encouraged.
MIT β see LICENSE.
This is the public API surface area for Fluxoβs MVP release.
func New(name string) *FlowBuilder
func Run(ctx context.Context, eng Engine, workflow string, input any) (*Instance, error)func NewInMemoryEngine() Engine
func NewInMemoryEngineWithObserver(o Observer) Engine
func NewSQLiteEngine(db *sql.DB) (Engine, error)
func NewSQLiteEngineWithObserver(db *sql.DB, o Observer) (Engine, error)
func NewPostgresEngine(db *sql.DB) (Engine, error)
func NewPostgresEngineWithObserver(db *sql.DB, o Observer) (Engine, error)
func NewRedisEngine(client *redis.Client) Engine
func NewRedisEngineWithObserver(client *redis.Client, o Observer) Engine
func NewMongoEngine(client *mongo.Client) Engine
func NewMongoEngineWithObserver(client *mongo.Client, o Observer) Enginefunc NewInMemoryQueue(capacity int) TaskQueue
func NewSQLiteQueue(db *sql.DB) (TaskQueue, error)
func NewPostgresQueue(db *sql.DB) (TaskQueue, error)
func NewRedisQueue(client *redis.Client) TaskQueue
func NewMongoQueue(client *mongo.Client) TaskQueuefunc NewWorker(eng Engine, q TaskQueue) *Worker
func NewWorkerWithConfig(eng Engine, q TaskQueue, cfg worker.Config) *WorkerKey methods:
func (w *Worker) Run(ctx context.Context) error
func (w *Worker) ProcessOne(ctx context.Context) (bool, error)type LocalRunner struct {
Engine Engine
Queue TaskQueue
Worker *Worker
}
func NewLocalRunner() *LocalRunner
func (r *LocalRunner) StartWorkers(ctx context.Context, n int) error
func (r *LocalRunner) StartWorkflowAsync(ctx context.Context, name string, input any) error
func (r *LocalRunner) SignalAsync(ctx context.Context, instanceID string, signal string, payload any) errortype Observer interface {
// lifecycle + metrics events
}
func NewLoggingObserver(logger *slog.Logger) Observer
func NewCompositeObserver(obs ...Observer) Observer
func NewNoopObserver() Observer
type BasicMetrics struct { /* counters */ }
func (m *BasicMetrics) Snapshot() BasicMetricsSnapshottype FlowBuilder struct {
// ...
}
func (b *FlowBuilder) Step(name string, fn StepFunc) *FlowBuilder
func (b *FlowBuilder) If(name string, cond ConditionFunc, then StepFunc, els StepFunc) *FlowBuilder
func (b *FlowBuilder) Parallel(name string, steps ...StepFunc) *FlowBuilder
func (b *FlowBuilder) Loop(name string, times int, body StepFunc) *FlowBuilder
func (b *FlowBuilder) While(name string, cond ConditionFunc, body StepFunc) *FlowBuilder
func (b *FlowBuilder) WaitForSignal(name, signal string) *FlowBuilder
func (b *FlowBuilder) Sleep(name string, dur time.Duration) *FlowBuilder
func (b *FlowBuilder) SleepUntil(name string, t time.Time) *FlowBuildertype StepFunc func (ctx context.Context, input any) (any, error)
type ConditionFunc func(input any) boolfunc TypedStep[I, O any](fn func(context.Context, I) (O, error)) StepFunc
func TypedWhile[I any](cond func (I) bool, body func (context.Context, I) (I, error)) StepFunc
func TypedLoop[I any](times int, body func (context.Context, I) (I, error)) StepFunc