Documentation
¶
Overview ¶
Package fluxo provides a lightweight, embeddable workflow engine for Go.
Fluxo is designed for backend services that need reliable asynchronous operations, background tasks, or long-lived workflows—without introducing external dependencies or heavy infrastructure. It runs fully in Go, supports multiple persistence backends, and integrates cleanly into existing codebases.
Core Concepts ¶
The Fluxo programming model is intentionally small and idiomatic:
- Engine
- Worker
- FlowBuilder
- StepFunc
- LocalRunner
These components form a complete workflow system with deterministic execution, durable state (when using persistent backends), and a clear mental model.
Engine ¶
The Engine stores workflow definitions, persists workflow state, manages execution plans, and provides APIs to:
- start workflows
- resume workflows after steps complete
- deliver signals
- read workflow state and history
Engines can be backed by different storage systems:
- In-memory (non-durable, best for tests)
- SQLite (embedded durability)
- Postgres
- Redis
- MongoDB
Each backend includes a matching task queue implementation so workers can reliably fetch work.
Engines are safe for use from background workers or from application code that wants to schedule workflows synchronously.
Worker ¶
A Worker pulls tasks from a configured queue and executes workflow steps. Workers run asynchronously and can be scaled horizontally.
Responsibilities include:
- polling task queues
- executing StepFuncs deterministically
- applying retry policies
- driving workflows forward to completion
Applications typically run one or more workers as background goroutines or as separate processes.
FlowBuilder ¶
FlowBuilder provides the ergonomic, declarative API used to define workflows. It supports common control-flow structures:
- Sequential steps
- Conditionals (If / Switch)
- Parallel execution (Parallel / ParallelMap)
- Loops (Loop / While, including typed variants)
- Timers and sleeps
- Signals
Example:
fluxo.New("Example").
Step("a", doA).
Step("b", doB).
Parallel("c",
fluxo.StepFunc("p1", work1),
fluxo.StepFunc("p2", work2),
)
Definitions created with FlowBuilder are registered into an Engine before use.
StepFunc ¶
A StepFunc is the fundamental executable unit of a workflow:
type StepFunc func(ctx context.Context, state *State) error
Steps are:
- deterministic: same inputs → same observable behavior
- idempotent: may be retried if a worker crashes
- isolated: they receive a state object representing workflow data
Typed helpers make it easy to work with structured Go values without manual marshaling.
LocalRunner ¶
LocalRunner bundles an in-memory engine, queue, and worker into a single, process-local helper useful for development and unit testing. It lets you:
- start workflows synchronously or asynchronously
- send signals
- wait for completion
LocalRunner is intentionally **not crash-durable**, but it provides the most convenient way to run and debug workflows during development.
Summary ¶
Fluxo’s goal is to give Go developers a workflow engine that feels like Go: easy to embed, easy to test, deterministic, and without operational overhead. Engines manage workflow state, Workers execute steps, FlowBuilder defines workflows, StepFuncs contain business logic, and LocalRunner provides a fast, developer-friendly runtime.
For examples, see the /examples directory or the project README.
local_runner.go
Example (FlowBuilder) ¶
Example_flowBuilder demonstrates defining and running a simple workflow using the high-level FlowBuilder API and an in-memory engine.
package main
import (
"context"
"fmt"
"log"
"github.com/petrijr/fluxo"
)
func main() {
ctx := context.Background()
flow := fluxo.New("Greeting").
Step("sayHello", sayHello).
Step("decorateMessage", decorateMessage)
eng := fluxo.NewInMemoryEngine()
if err := flow.Register(eng); err != nil {
log.Fatal(err)
}
inst, err := fluxo.Run(ctx, eng, flow.Name(), "Gopher")
if err != nil {
log.Fatal(err)
}
fmt.Printf("workflow %q finished with status %s and output %v\n",
inst.ID, inst.Status, inst.Output)
}
func sayHello(ctx context.Context, input any) (any, error) {
name, ok := input.(string)
if !ok {
return nil, fmt.Errorf("sayHello: expected string input, got %T", input)
}
msg := fmt.Sprintf("hello, %s", name)
log.Printf("[sayHello] %s", msg)
return msg, nil
}
func decorateMessage(ctx context.Context, input any) (any, error) {
msg, ok := input.(string)
if !ok {
return nil, fmt.Errorf("decorateMessage: expected string input, got %T", input)
}
out := fmt.Sprintf("*** %s ***", msg)
log.Printf("[decorateMessage] %s", out)
return out, nil
}
Output:
Example (LocalRunner) ¶
Example_localRunner demonstrates using LocalRunner to execute workflows with an in-process engine, queue, and worker.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/petrijr/fluxo"
)
func main() {
ctx := context.Background()
runner := fluxo.NewLocalRunner()
flow := fluxo.New("Greeting").
Step("sayHello", sayHello).
Step("decorateMessage", decorateMessage)
if err := flow.Register(runner.Engine); err != nil {
log.Fatal(err)
}
// Start one worker goroutine.
if err := runner.StartWorkers(ctx, 1); err != nil {
log.Fatal(err)
}
defer runner.Stop()
// Enqueue an asynchronous workflow start.
if err := runner.StartWorkflowAsync(ctx, flow.Name(), "Gopher"); err != nil {
log.Fatal(err)
}
// In a real application you'd wait on instance completion or poll;
// for example purposes, just give the worker a moment to run.
time.Sleep(500 * time.Millisecond)
}
func sayHello(ctx context.Context, input any) (any, error) {
name, ok := input.(string)
if !ok {
return nil, fmt.Errorf("sayHello: expected string input, got %T", input)
}
msg := fmt.Sprintf("hello, %s", name)
log.Printf("[sayHello] %s", msg)
return msg, nil
}
func decorateMessage(ctx context.Context, input any) (any, error) {
msg, ok := input.(string)
if !ok {
return nil, fmt.Errorf("decorateMessage: expected string input, got %T", input)
}
out := fmt.Sprintf("*** %s ***", msg)
log.Printf("[decorateMessage] %s", out)
return out, nil
}
Output:
Index ¶
- Constants
- Variables
- func RecoverStuckInstances(ctx context.Context, eng Engine) (int, error)
- type BasicMetrics
- type BasicMetricsSnapshot
- type ChildWorkflowSpec
- type CompositeObserver
- type ConditionFunc
- type Config
- type Engine
- type FlowBuilder
- func (b *FlowBuilder) Definition() WorkflowDefinition
- func (b *FlowBuilder) If(name string, cond ConditionFunc, thenStep, elseStep StepFunc) *FlowBuilder
- func (b *FlowBuilder) Loop(name string, times int, body StepFunc) *FlowBuilder
- func (b *FlowBuilder) MustRegister(eng Engine)
- func (b *FlowBuilder) Name() string
- func (b *FlowBuilder) Parallel(name string, steps ...StepFunc) *FlowBuilder
- func (b *FlowBuilder) Register(eng Engine) error
- func (b *FlowBuilder) Step(name string, fn StepFunc) *FlowBuilder
- func (b *FlowBuilder) StepWithRetry(name string, fn StepFunc, retry RetryPolicy) *FlowBuilder
- func (b *FlowBuilder) StepWithRetryBuilder(name string, fn StepFunc, rb RetryBuilder) *FlowBuilder
- func (b *FlowBuilder) Switch(name string, selector SelectorFunc, branches map[string]StepFunc, ...) *FlowBuilder
- func (b *FlowBuilder) WaitForAnySignal(stepName string, names ...string) *FlowBuilder
- func (b *FlowBuilder) WaitForSignal(stepName, signalName string) *FlowBuilder
- func (b *FlowBuilder) While(name string, cond ConditionFunc, body StepFunc) *FlowBuilder
- type InstanceListOptions
- type LocalRunner
- func (r *LocalRunner) SignalAsync(ctx context.Context, instanceID, name string, payload any) error
- func (r *LocalRunner) StartWorkers(ctx context.Context, concurrency int) error
- func (r *LocalRunner) StartWorkflowAsync(ctx context.Context, workflowName string, input any) error
- func (r *LocalRunner) Stop()
- type LoggingObserver
- type NoopObserver
- type Observer
- type ParallelResult
- type Queue
- type RetryBuilder
- type RetryPolicy
- type SelectorFunc
- type Status
- type StepDefinition
- type StepFunc
- func IfStep(cond ConditionFunc, thenStep, elseStep StepFunc) StepFunc
- func LoopStep(times int, body StepFunc) StepFunc
- func ParallelMapStep(mapper StepFunc) StepFunc
- func ParallelStep(steps ...StepFunc) StepFunc
- func SleepStep(d time.Duration) StepFunc
- func SleepUntilStep(t time.Time) StepFunc
- func StartChildrenStep(specsFn func(input any) ([]api.ChildWorkflowSpec, error)) StepFunc
- func SwitchStep(selector SelectorFunc, branches map[string]StepFunc, defaultStep StepFunc) StepFunc
- func TypedLoop[I any](times int, body func(context.Context, I) (I, error)) StepFunc
- func 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 WaitForAnyChildStep(getIDs func(input any) []string, pollInterval time.Duration) StepFunc
- func WaitForAnySignalStep(names ...string) StepFunc
- func WaitForChildrenStep(getIDs func(input any) []string, pollInterval time.Duration) StepFunc
- func WaitForSignalStep(name string) StepFunc
- func While(cond ConditionFunc, body StepFunc) StepFunc
- type TimeoutPayload
- type Worker
- type WorkerBundle
- type WorkflowDefinition
- type WorkflowInstance
- func GetInstance(ctx context.Context, eng Engine, id string) (*WorkflowInstance, error)
- func ListInstances(ctx context.Context, eng Engine, opts InstanceListOptions) ([]*WorkflowInstance, error)
- func Resume(ctx context.Context, eng Engine, id string) (*WorkflowInstance, error)
- func Run(ctx context.Context, eng Engine, name string, input any) (*WorkflowInstance, error)
- func Signal(ctx context.Context, eng Engine, id string, name string, payload any) (*WorkflowInstance, error)
Examples ¶
Constants ¶
const ( StatusPending = api.StatusPending StatusRunning = api.StatusRunning StatusWaiting = api.StatusWaiting StatusFailed = api.StatusFailed StatusCompleted = api.StatusCompleted )
Re-export status values for convenience.
Variables ¶
var ( NewLoggingObserver = api.NewLoggingObserver NewCompositeObserver = api.NewCompositeObserver )
Re-export common observer helpers.
Functions ¶
Types ¶
type BasicMetrics ¶
type BasicMetrics = api.BasicMetrics
Re-export key types so users don't need to dig into pkg/api.
type BasicMetricsSnapshot ¶
type BasicMetricsSnapshot = api.BasicMetricsSnapshot
Re-export key types so users don't need to dig into pkg/api.
type ChildWorkflowSpec ¶
type ChildWorkflowSpec = api.ChildWorkflowSpec
Re-export key types so users don't need to dig into pkg/api.
type CompositeObserver ¶
type CompositeObserver = api.CompositeObserver
Re-export key types so users don't need to dig into pkg/api.
type ConditionFunc ¶
type ConditionFunc = api.ConditionFunc
Re-export key types so users don't need to dig into pkg/api.
type Engine ¶
Re-export key types so users don't need to dig into pkg/api.
func NewInMemoryEngine ¶
func NewInMemoryEngine() Engine
NewInMemoryEngine returns an Engine backed entirely by in-memory stores.
func NewInMemoryEngineWithObserver ¶
NewInMemoryEngineWithObserver returns an in-memory Engine with the given Observer.
func NewSQLiteEngine ¶
NewSQLiteEngine returns an Engine that persists workflow instances in a SQLite database. Workflow definitions are kept in-memory.
type FlowBuilder ¶
type FlowBuilder struct {
// contains filtered or unexported fields
}
FlowBuilder provides a fluent API for defining workflows:
flow := fluxo.New("OnboardUser").
Step("createAccount", createAccount).
Step("sendWelcomeEmail", sendWelcomeEmail).
Step("waitActivation", fluxo.WaitForSignalStep("activated"))
if err := flow.Register(engine); err != nil {
log.Fatal(err)
}
inst, err := fluxo.Run(ctx, engine, flow.Name(), input)
func New ¶
func New(name string) *FlowBuilder
New creates a new workflow builder with the given name.
func (*FlowBuilder) Definition ¶
func (b *FlowBuilder) Definition() WorkflowDefinition
Definition returns the underlying WorkflowDefinition. Typically used when interacting with lower-level APIs.
func (*FlowBuilder) If ¶
func (b *FlowBuilder) If(name string, cond ConditionFunc, thenStep, elseStep StepFunc) *FlowBuilder
If adds a conditional branching step.
func (*FlowBuilder) Loop ¶
func (b *FlowBuilder) Loop(name string, times int, body StepFunc) *FlowBuilder
Loop adds a step that executes body a fixed number of times. The loop is executed as a nested step; retries/backoff (if any) apply to the entire loop execution.
func (*FlowBuilder) MustRegister ¶
func (b *FlowBuilder) MustRegister(eng Engine)
MustRegister is like Register but panics on error. Useful for initialization in main().
func (*FlowBuilder) Parallel ¶
func (b *FlowBuilder) Parallel(name string, steps ...StepFunc) *FlowBuilder
Parallel is a convenience for adding a step that runs sub-steps in parallel.
func (*FlowBuilder) Register ¶
func (b *FlowBuilder) Register(eng Engine) error
Register registers the built workflow with the given engine.
func (*FlowBuilder) Step ¶
func (b *FlowBuilder) Step(name string, fn StepFunc) *FlowBuilder
Step appends a basic step to the workflow.
func (*FlowBuilder) StepWithRetry ¶
func (b *FlowBuilder) StepWithRetry(name string, fn StepFunc, retry RetryPolicy) *FlowBuilder
StepWithRetry appends a step that uses the given retry policy.
func (*FlowBuilder) StepWithRetryBuilder ¶
func (b *FlowBuilder) StepWithRetryBuilder(name string, fn StepFunc, rb RetryBuilder) *FlowBuilder
StepWithRetryBuilder is a convenience wrapper around StepWithRetry that accepts a RetryBuilder.
func (*FlowBuilder) Switch ¶
func (b *FlowBuilder) Switch( name string, selector SelectorFunc, branches map[string]StepFunc, defaultStep StepFunc, ) *FlowBuilder
Switch adds a multi-branch step based on a selector and branch map.
func (*FlowBuilder) WaitForAnySignal ¶
func (b *FlowBuilder) WaitForAnySignal(stepName string, names ...string) *FlowBuilder
WaitForAnySignal adds a step that waits for any of the given signal names.
func (*FlowBuilder) WaitForSignal ¶
func (b *FlowBuilder) WaitForSignal(stepName, signalName string) *FlowBuilder
WaitForSignal adds a step that waits for a named signal.
func (*FlowBuilder) While ¶
func (b *FlowBuilder) While(name string, cond ConditionFunc, body StepFunc) *FlowBuilder
While adds a looping step that executes body while cond(input) is true. The loop is executed as a nested step; retries/backoff (if any) apply to the entire loop execution.
type InstanceListOptions ¶
type InstanceListOptions = api.InstanceListOptions
Re-export key types so users don't need to dig into pkg/api.
type LocalRunner ¶
type LocalRunner struct {
// Engine is the in-memory workflow engine used by this runner.
Engine Engine
// Queue is the in-memory task queue used by the Worker.
Queue taskqueue.Queue
// Worker processes tasks from Queue using Engine.
Worker *worker.Worker
// contains filtered or unexported fields
}
LocalRunner bundles an in-memory Engine, an in-memory task queue, and a Worker to provide a simple "local runner" for development and debugging.
Typical usage:
runner := fluxo.NewLocalRunner()
flow := fluxo.New("my-flow").Step(...)
flow.MustRegister(runner.Engine)
// Synchronous run (no queue/worker involved):
inst, err := fluxo.Run(ctx, runner.Engine, flow.Name(), input)
// Asynchronous run:
_ = runner.StartWorkers(ctx, 2)
_ = runner.StartWorkflowAsync(ctx, flow.Name(), input)
...
runner.Stop()
func NewLocalRunner ¶
func NewLocalRunner() *LocalRunner
NewLocalRunner constructs a LocalRunner backed by an in-memory engine, in-memory queue, and a Worker with default config.
This is intended for local development, tests, and simple single-process deployments.
func (*LocalRunner) SignalAsync ¶
SignalAsync enqueues a task to deliver a signal to a workflow instance. The instance will process the signal when a worker picks up the task.
func (*LocalRunner) StartWorkers ¶
func (r *LocalRunner) StartWorkers(ctx context.Context, concurrency int) error
StartWorkers starts 'concurrency' worker goroutines that continuously call Worker.ProcessOne(ctx) until the context is cancelled via Stop.
If StartWorkers is called more than once without Stop, it returns an error.
func (*LocalRunner) StartWorkflowAsync ¶
StartWorkflowAsync enqueues a task to start the given workflow asynchronously. The workflow must already be registered on LocalRunner.Engine.
func (*LocalRunner) Stop ¶
func (r *LocalRunner) Stop()
Stop cancels all worker goroutines started by StartWorkers and waits for them to exit.
type LoggingObserver ¶
type LoggingObserver = api.LoggingObserver
Re-export key types so users don't need to dig into pkg/api.
type NoopObserver ¶
type NoopObserver = api.NoopObserver
Re-export key types so users don't need to dig into pkg/api.
type ParallelResult ¶
type ParallelResult = api.ParallelResult
Re-export key types so users don't need to dig into pkg/api.
type RetryBuilder ¶
type RetryBuilder struct {
// contains filtered or unexported fields
}
RetryBuilder provides a fluent way to construct RetryPolicy values for use with FlowBuilder.StepWithRetry.
func Retry ¶
func Retry(maxAttempts int) RetryBuilder
Retry creates a RetryBuilder with the given maxAttempts.
maxAttempts <= 0 is treated as 1 (no retries).
func (RetryBuilder) Immediate ¶
func (r RetryBuilder) Immediate() RetryBuilder
Immediate disables any sleep between retries. Retries will still respect MaxAttempts.
func (RetryBuilder) Policy ¶
func (r RetryBuilder) Policy() RetryPolicy
Policy returns the underlying RetryPolicy to be passed to FlowBuilder.StepWithRetry.
func (RetryBuilder) WithConstantBackoff ¶
func (r RetryBuilder) WithConstantBackoff(delay time.Duration) RetryBuilder
WithConstantBackoff configures a constant backoff between retries.
This is equivalent to an exponential backoff with multiplier 1.0 and no max cap.
func (RetryBuilder) WithExponentialBackoff ¶
func (r RetryBuilder) WithExponentialBackoff(initial time.Duration, multiplier float64, max time.Duration) RetryBuilder
WithExponentialBackoff configures exponential backoff:
- initial is the delay before the first retry.
- multiplier > 1 grows the delay each attempt (default 2.0 if <= 0).
- max caps the delay; if <= 0, there is no cap.
Example:
Retry(3).WithExponentialBackoff(100*time.Millisecond, 2.0, 2*time.Second)
type RetryPolicy ¶
type RetryPolicy = api.RetryPolicy
Re-export key types so users don't need to dig into pkg/api.
type SelectorFunc ¶
type SelectorFunc = api.SelectorFunc
Re-export key types so users don't need to dig into pkg/api.
type StepDefinition ¶
type StepDefinition = api.StepDefinition
Re-export key types so users don't need to dig into pkg/api.
type StepFunc ¶
Re-export key types so users don't need to dig into pkg/api.
func IfStep ¶
func IfStep(cond ConditionFunc, thenStep, elseStep StepFunc) StepFunc
IfStep creates a conditional step composed of then/else branches.
func LoopStep ¶
LoopStep returns a step that executes body a fixed number of times. The entire loop is treated as a single engine step.
func ParallelMapStep ¶
ParallelMapStep runs a mapping step over a slice input in parallel.
func ParallelStep ¶
ParallelStep runs all provided step funcs in parallel and returns a []any of their outputs.
func SleepStep ¶
SleepStep returns a step that sleeps for the given duration and passes the input through.
func SleepUntilStep ¶
SleepUntilStep sleeps until a given timestamp or returns ctx.Err.
func StartChildrenStep ¶
func StartChildrenStep(specsFn func(input any) ([]api.ChildWorkflowSpec, error)) StepFunc
StartChildrenStep starts child workflows and returns their IDs.
func SwitchStep ¶
func SwitchStep(selector SelectorFunc, branches map[string]StepFunc, defaultStep StepFunc) StepFunc
SwitchStep dispatches to a branch based on a selector.
func TypedLoop ¶
TypedLoop returns a step that executes a strongly-typed body a fixed number of times.
func TypedStep ¶
TypedStep wraps a strongly-typed function into a StepFunc. Example:
fluxo.TypedStep(func(ctx context.Context, s MyState) (MyState, error) { ... })
func TypedWhile ¶
TypedWhile returns a step that repeatedly executes a strongly-typed body while cond(input) is true.
func WaitForAnyChildStep ¶
WaitForAnyChildStep waits until any of the children completes.
func WaitForAnySignalStep ¶
WaitForAnySignalStep waits for one of the allowed signal names.
func WaitForChildrenStep ¶
WaitForChildrenStep waits for all given child workflow IDs to complete.
func WaitForSignalStep ¶
WaitForSignalStep waits for a single named signal, returning its payload.
func While ¶
func While(cond ConditionFunc, body StepFunc) StepFunc
While returns a step that repeatedly executes body while cond(input) is true. The entire loop is treated as a single engine step.
type TimeoutPayload ¶
type TimeoutPayload = api.TimeoutPayload
Re-export key types so users don't need to dig into pkg/api.
type WorkerBundle ¶
type WorkerBundle struct {
Engine Engine
Worker *workerpkg.Worker
// contains filtered or unexported fields
}
WorkerBundle wires together an Engine, a durable task queue, and a Worker that consumes tasks from that queue.
For now, we only provide a SQLite-backed bundle.
func NewSQLiteBundle ¶
NewSQLiteBundle constructs a durable Engine + Queue + Worker combo sharing the same SQLite database. Workflow instances and queued tasks are persisted in the provided *sql.DB.
Typical usage:
db, _ := sql.Open("sqlite", "file:fluxo.db?_journal=WAL")
bundle, err := fluxo.NewSQLiteBundle(db, worker.Config{MaxAttempts: 3})
// register workflows on bundle.Engine
// enqueue work via bundle.Worker
type WorkflowDefinition ¶
type WorkflowDefinition = api.WorkflowDefinition
Re-export key types so users don't need to dig into pkg/api.
type WorkflowInstance ¶
type WorkflowInstance = api.WorkflowInstance
Re-export key types so users don't need to dig into pkg/api.
func GetInstance ¶
GetInstance fetches an instance by ID.
func ListInstances ¶
func ListInstances(ctx context.Context, eng Engine, opts InstanceListOptions) ([]*WorkflowInstance, error)
ListInstances lists workflow instances according to the given options.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
approval
command
|
|
|
approval_http
command
|
|
|
builder
command
examples/builder/main.go
|
examples/builder/main.go |
|
localrunner
command
|
|
|
loop
command
|
|
|
observer
command
examples/observer/main.go
|
examples/observer/main.go |
|
parallel
command
|
|
|
retry
command
|
|
|
signal_timeout
command
|
|
|
sqlite_bundle
command
|
|
|
typed
command
examples/typed/main.go
|
examples/typed/main.go |
|
internal
|
|
|
pkg
|
|
|
api
Package api contains the core building blocks used by the fluxo workflow engine.
|
Package api contains the core building blocks used by the fluxo workflow engine. |
|
worker
Package worker provides the background worker implementation used to drive fluxo workflows forward.
|
Package worker provides the background worker implementation used to drive fluxo workflows forward. |