flux-workflow

module
v1.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 7, 2026 License: MIT

README

flux-workflow

An embeddable, durable DAG workflow engine for Go.

flux-workflow runs directed-acyclic-graph workflows and owns their runtime state itself. You embed it as a library, point it at a database, register your tools and workflows, and submit tasks — the engine handles execution, persistence, crash recovery, and human-in-the-loop suspension. It is designed for AI / tool orchestration pipelines but knows nothing about any particular domain.

rt, _ := runtime.NewLocal("state.db", runtime.WithLocalTool(myTool))
rt.RegisterWorkflow(ctx, def)
res, _ := rt.Run(ctx, def, map[string]any{"name": "flux"})
fmt.Println(res.Status) // success

Why

flux-workflow
Embeddable A Go library, not a server. No sidecar to deploy for local use.
Durable Runtime state (tasks, node runtimes, events, await bindings) lives in your database — SQLite for local, Postgres for production.
Workflow-as-data Workflows are DAG definitions (JSON/DSL), versioned by content hash on registration.
Crash-safe Interrupted runs are recovered by a scanner; failed tasks can be retried from the failed subtree.
Human-in-the-loop await nodes suspend a run until an external signal (webhook, approval, async job) resumes it.
Pluggable infra Database, task queue, distributed lock, event bus and job queue are all injected.

Closest relatives: DBOS Transact (durable execution as a library), LangGraph (graph + checkpointer + interrupts), Temporal/Cadence (signals ≈ resume, event history ≈ persistent events) — but those are either not Go, or require a server.

Install

go get github.com/tuxi/flux-workflow

Requires Go 1.25+.

Quickstart

A complete, self-contained example (SQLite + in-memory queue, no external services) lives in examples/quickstart:

go run ./examples/quickstart
[sync ] task=... status=success output={"final":{...,"extras":{"greeting":"Hello, flux!"}}}
[async] task=... status=success output={"final":{...,"extras":{"greeting":"Hello, async!"}}}

It defines a greet tool, wires a start → greet → end workflow, and runs it both synchronously (Run) and asynchronously via background workers (Start + Submit).

The Runtime API

runtime.Runtime is the single entry point. Everything you need is a method on it.

Construction
// Local mode — self-contained: SQLite (WAL), in-memory queue / job queue / lock,
// no external dependencies. Ideal for embedding, tests, and single-process apps.
rt, err := runtime.NewLocal("state.db", runtime.WithLocalTool(myTool))

// Server mode — you inject the infrastructure.
rt, err := runtime.New(
    runtime.WithDB(pgDB),          // *gorm.DB (Postgres or SQLite)
    runtime.WithQueue(redisQ),     // repository.TaskQueue
    runtime.WithJobQueue(jobQ),    // engine.AsyncJobQueue
    runtime.WithEventBus(bus),     // *eventbus.EventBus
    runtime.WithLock(dLock),       // lock.DistributedLock
    runtime.WithTool(myTool),
)
Lifecycle & execution
Method Purpose
RegisterWorkflow(ctx, def) Persist a workflow definition; re-publishes a version when its content hash changes.
Run(ctx, def, input) Execute synchronously; returns when the DAG reaches a terminal or suspended state.
Start(ctx, opts...) Launch background workers (task / async / await-poll) + recovery scanner.
Submit(ctx, name, input) Enqueue a task by workflow name for asynchronous execution. Requires Start.
Status(ctx, taskID) Fetch current task state.
Resume(ctx, taskID, node, meta) Wake a suspended await node with an external result; continues the DAG. Idempotent.
Retry(ctx, taskID, from, patches) Manually recover a failed/canceled/suspended task; resets the failed subtree and re-enqueues.
Subscribe(eventType) Get a channel of *domain.TaskEvent plus an unsubscribe func.
Shutdown() Stop workers, wait for them to exit, close the owned DB.

Worker concurrency is tunable:

rt.Start(ctx,
    runtime.WithTaskWorkers(4),
    runtime.WithAsyncWorkers(4),
    runtime.WithAwaitPollWorkers(1),
)
Building a service layer on top

The facade covers the common path. To build your own HTTP/service layer, reach the underlying handles and use the core packages directly:

db  := rt.DB()            // *gorm.DB — construct any query repo from repository/query
bus := rt.EventBus()      // publish custom events / attach listeners
eng := rt.Engine()        // power-user ops: Replay, Redo, Cancel, Fork, ...
reg := rt.NodeRegistry()  // register custom node types

The runtime facade is deliberately thin: advanced operations stay on the core packages (engine, repository, eventbus) rather than being mirrored here.

Human-in-the-loop

An await node suspends the task. When the external event arrives, resume it:

res, _ := rt.Run(ctx, def, input)   // res.Status == "suspended"
// ... webhook / approval arrives ...
rt.Resume(ctx, res.TaskID, "wait_approval", map[string]any{"approved": true})

Workflows & node types

A workflow is a definition.WorkflowDefinition (from github.com/tuxi/flux): Nodes, Edges, and an Output mapping. Built-in node types:

Type Role
start / end DAG entry / exit.
tool Runs a registered tool.Tool (config.tool selects it).
await Suspends until an external signal / async completion resumes it.
map Fans out over a collection, one child sub-task per item.
loop Iterates a sub-workflow with carry-over state.
subworkflow Runs another registered workflow as a child.

Node inputs are expressions (input.name, nodes.greet.output.greeting); the workflow Output maps node outputs into the final result. Register your own node types via rt.NodeRegistry().

Architecture

runtime/                one-stop facade (this is the public API)
engine/                 DAG execution, resume, retry, replay, fork, cancel
workflow/, workflow/nodes/  builder + node implementations
repository/, repository/query/  storage interfaces + GORM implementation
eventbus/               layered event routing (transient / persistent / audit)
worker/                 task, async, await-poll workers + recovery scanner
domain/                 core entities

The core is dependency-light: go list -deps ./runtime pulls no web framework, cloud SDK, or LLM client. Redis-backed drivers are optional subpackages so pure local embedding never links go-redis:

  • engine/redisjobqueue — Redis Stream AsyncJobQueue
  • repository/query/redisqueue — Redis TaskQueue
  • pkg/lock/redislock — Redsync DistributedLock

Status

flux-workflow was extracted from a production AI product and is being hardened for general use. The engine core and the runtime facade are stable; some higher-level HTTP/presentation code is still being separated out. APIs may shift before a tagged release.

License

MIT

Directories

Path Synopsis
redisjobqueue
Package redisjobqueue provides a Redis Stream backed engine.AsyncJobQueue.
Package redisjobqueue provides a Redis Stream backed engine.AsyncJobQueue.
examples
quickstart command
Command quickstart is a self-contained demo of embedding the flux-workflow engine via the runtime package: no external DB, queue, or Redis required.
Command quickstart is a self-contained demo of embedding the flux-workflow engine via the runtime package: no external DB, queue, or Redis required.
pkg
lock/redislock
Package redislock provides a Redsync (Redis) backed lock.DistributedLock.
Package redislock provides a Redsync (Redis) backed lock.DistributedLock.
query/redisqueue
Package redisqueue provides a Redis backed repository.TaskQueue.
Package redisqueue provides a Redis backed repository.TaskQueue.
query/taskapi
Package taskapi hosts the business/HTTP-facing task read-model queries (pagination, list, detail) that return presentation dto types.
Package taskapi hosts the business/HTTP-facing task read-model queries (pagination, list, detail) that return presentation dto types.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL