Workflow is a high-performance, industrial-grade state management library for Go. It provides a robust framework for managing the lifecycle of complex domain objects, from simple linear state machines to sophisticated concurrent workflows based on Petri Net theory.
- 🎯 Dual-Mode Engine: Native support for both strict State Machines (single state) and flexible Workflows (concurrent states).
-
🪝 Granular Event Lifecycle: Hooks into every transition phase:
Guard,Leave,Transition,Enter,Entered,Completed, andAnnounce. -
🔌 Decoupled Persistence: Flexible
MarkingStoreinterface allows you to keep your domain models clean from library logic. - 🛠️ Multi-Format Definitions: Configure your workflows using a fluent Go API or declarative YAML/JSON.
-
🛡️ Production Hardened: Thread-safe registry,
$O(1)$ state lookups, and automated reachability validation to prevent deadlocks. -
📊 Observability: Built-in Graphviz DOT export for visualization and
slog-compatible structured logging.
go get github.com/oullin/workflowWhether it's a simple subscription or a complex multi-step pipeline, defining it is straightforward.
definition, _ := workflow.NewDefinitionBuilder().
AddPlace("draft").
AddPlace("payment_pending").
AddPlace("active").
SetInitialPlaces("draft").
AddTransition("collect_payment", []string{"draft"}, []string{"payment_pending"}).
AddTransition("activate", []string{"payment_pending"}, []string{"active"}).
Build()// Define your domain object
type Subscription struct { State string }
// 1. Setup the Store (Decoupled state management)
markingStore := &store.SingleState[*Subscription]{
Getter: func(s *Subscription) string { return s.State },
Setter: func(s *Subscription, state string) { s.State = state },
}
// 2. Initialize the Engine
// The last argument is an optional event dispatcher. Passing nil will create a default one.
engine, _ := workflow.New("subscription_flow", definition, markingStore, nil)
// 3. Apply Transitions
sub := &Subscription{State: "draft"}
_, err := engine.Apply(sub, "collect_payment", nil)
fmt.Println(sub.State) // Output: payment_pendingFor a complete working example with guards, audit trails, and a registry, see the examples/ directory.
workflow.New()creates a full Petri Net workflow engine where objects can occupy multiple places simultaneously.workflow.NewStateMachine()creates a strict state machine where objects exist in exactly one place at a time. Use this when your domain object has a single linear state field.
Instead of the fluent Go API, you can define workflows declaratively in YAML:
workflows:
subscription_paid:
type: state_machine
initial_marking: draft
places:
- name: draft
- name: payment_pending
- name: active
transitions:
collect_payment:
from: draft
to: payment_pending
activate:
from: payment_pending
to: active
guard: payment_captured && identity_verifiedThen load and use it:
definitions, err := config.LoadFile("workflows.yaml")
definition := definitions["subscription_paid"]
engine, err := workflow.NewStateMachine("subscription_paid", definition, markingStore, dispatcher)- State Machine: An object exists in exactly one place. Ideal for simple status flags.
- Workflow: Implements Petri Nets where an object can occupy multiple places simultaneously. Perfect for parallel approvals or complex multi-path processes.
Every transition triggers a meticulously ordered sequence of events, allowing you to inject logic exactly where it's needed:
- Guard 🛡️: Can we start? (Perfect for validation/authorization)
- Leave 🏃: Cleaning up the old state.
- Transition ⚙️: The internal state change is happening.
- Enter 🏁: Preparing the new state.
- Entered ✅: State is persisted (Post-commit hooks).
- Completed 🎉: The entire process is finished.
- Announce 📢: What's next? (Triggering subsequent workflows).
Block transitions dynamically based on your business rules:
dispatcher.OnGuard(workflow.EventNameGuardNamed("blog", "publish"), func(event *events.GuardEvent[*Article]) {
if event.Subject().Title == "" {
event.SetBlocked(true, "Title is required to publish")
}
})Track every transition automatically with the built-in audit trail:
trail := &audit.Trail[*Subscription]{}
trail.Attach("subscription_flow", dispatcher)Graphviz DOT is a text-based graph description language. Export your workflows to this format for documentation or debugging:
dumper := &workflow.GraphvizDumper{}
dotOutput := dumper.Dump(definition)
// You can then render this using Graphviz CLI:
// echo "$dotOutput" | dot -Tpng -o workflow.pngWe maintain high standards for code quality and reliability.
make fmt # Professional formatting
make lint # Static analysis
make test-unit # Comprehensive test suite
make test-coverage # 100% focused on critical paths- Author: Gus
This project is licensed under the MIT License - see the LICENSE file for details.