workflow

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Nov 29, 2025 License: MIT Imports: 5 Imported by: 0

README

Go Workflow

Disclaimer:
This workflow engine is currently under active development and is not yet considered production-ready. We welcome community feedback, contributions, and real-world testing to help mature the project and ensure its stability for production environments.

If you are interested in contributing, providing feedback, or trying out the engine, please get involved!

A flexible and extensible workflow engine for Go applications. This package provides a robust implementation of a Petri net-based workflow system that supports complex state transitions, event handling, and constraints. Inspired by Symfony Workflow Component.

Features

  • Petri net-based workflow engine
  • Support for multiple states and transitions
  • Event system for workflow lifecycle hooks
  • Constraint system for transition validation
  • Thread-safe workflow registry
  • Comprehensive test coverage
  • Mermaid diagram generation for visualization
  • Flexible, context-aware storage interface for workflow persistence
  • Options pattern for custom fields and schema generation
  • Reusable, pluggable history/audit layer with pagination and filtering
  • Workflow manager for lifecycle management
  • Support for parallel transitions and branching

Storage Layer (New)

The storage layer is now fully context-aware and supports custom fields via the options pattern. Only fields declared as custom fields are persisted.

Storage Interface

The package provides a flexible, context-aware storage interface for persisting workflow states and custom fields:

type Storage interface {
    LoadState(id string) (places []Place, context map[string]interface{}, err error)
    SaveState(id string, places []Place, context map[string]interface{}) error
    DeleteState(id string) error
}

You can implement your own storage backend by implementing this interface. The package includes a SQLite implementation with options for custom fields:

import "github.com/ehabterra/workflow/storage"

// Create a SQLite storage with custom fields
storage, err := storage.NewSQLiteStorage(db,
    storage.WithTable("workflow_states"),
    storage.WithCustomFields(map[string]string{
        "title": "title TEXT",
        "owner": "owner TEXT",
    }),
)
if err != nil { panic(err) }

// Generate and initialize schema
schema := storage.GenerateSchema()
if err := storage.Initialize(db, schema); err != nil { panic(err) }

// Save state with context
err = storage.SaveState("my-workflow", []workflow.Place{"draft"}, map[string]interface{}{"title": "My Doc", "owner": "alice"})

// Load state and context
places, ctx, err := storage.LoadState("my-workflow")
fmt.Println(places, ctx["title"], ctx["owner"])

History Layer (New)

The history layer is reusable, pluggable, and supports custom fields, pagination, and filtering.

History Interface
type HistoryStore interface {
    SaveTransition(record *TransitionRecord) error
    ListHistory(workflowID string, opts QueryOptions) ([]TransitionRecord, error)
    GenerateSchema() string
    Initialize() error
}
SQLite History Example
import "github.com/ehabterra/workflow/history"

historyStore := history.NewSQLiteHistory(db,
    history.WithCustomFields(map[string]string{
        "ip_address": "ip_address TEXT",
    }),
)
historyStore.Initialize()

// Save a transition with custom fields
historyStore.SaveTransition(&history.TransitionRecord{
    WorkflowID: "wf1",
    FromState:  "draft",
    ToState:    "review",
    Transition: "submit",
    Notes:      "Submitted for review",
    Actor:      "alice",
    CreatedAt:  time.Now(),
    CustomFields: map[string]interface{}{
        "ip_address": "127.0.0.1",
    },
})

// List history with pagination
records, err := historyStore.ListHistory("wf1", history.QueryOptions{Limit: 10, Offset: 0})
for _, rec := range records {
    fmt.Println(rec.FromState, rec.ToState, rec.Notes, rec.CustomFields["ip_address"])
}

Feature Checklist

Current Features ✅
  • Basic workflow definition and execution
  • Multiple states and transitions
  • Event system for workflow hooks
  • Constraint system for transitions
  • Thread-safe workflow registry
  • Mermaid diagram visualization
  • Workflow manager for lifecycle management
  • Storage interface for persistence
  • SQLite storage implementation
  • Support for parallel transitions and branching
  • Workflow history and audit trail (in examples)
  • Web UI for workflow management (in examples)

Note: The provided example includes a Web UI for workflow management, but does not expose a REST API. If you need a REST API, contributions or feature requests are welcome!

Planned Features 🚀
High Priority
  • YAML/JSON configuration support
  • Standalone web interface for workflow management
  • Enhanced REST API endpoints
  • Workflow validation system
  • Dynamic workflow definition loading
Medium Priority
  • Custom scripting for transition conditions
  • Workflow versioning
  • Workflow templates
  • Role-based access control
  • Workflow timeout and scheduling
Low Priority
  • Workflow statistics and analytics
  • Export/Import workflow definitions

Installation

go get github.com/ehabterra/workflow

Quick Start

Here's a simple example of how to use the workflow package with the new context-aware storage and options pattern:

package main

import (
    "database/sql"
    "fmt"
    "time"

    "github.com/ehabterra/workflow"
    "github.com/ehabterra/workflow/storage"
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    // Open SQLite DB (in-memory for demo)
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil { panic(err) }

    // Create storage with custom fields
    store, err := storage.NewSQLiteStorage(db,
        storage.WithTable("workflow_states"),
        storage.WithCustomFields(map[string]string{
            "title": "title TEXT",
        }),
    )
    if err != nil { panic(err) }
    if err := storage.Initialize(db, store.GenerateSchema()); err != nil { panic(err) }

    // Define workflow
    definition, err := workflow.NewDefinition(
        []workflow.Place{"start", "middle", "end"},
        []workflow.Transition{
            *workflow.MustNewTransition("to-middle", []workflow.Place{"start"}, []workflow.Place{"middle"}),
            *workflow.MustNewTransition("to-end", []workflow.Place{"middle"}, []workflow.Place{"end"}),
        },
    )
    if err != nil { panic(err) }

    // Create manager
    registry := workflow.NewRegistry()
    manager := workflow.NewManager(registry, store)

    // Create a new workflow with context
    wf, err := manager.CreateWorkflow("my-workflow", definition, "start")
    if err != nil { panic(err) }
    wf.SetContext("title", "My Example Workflow")
    if err := manager.SaveWorkflow("my-workflow", wf); err != nil { panic(err) }

    // Apply a transition
    err = wf.Apply([]workflow.Place{"middle"})
    if err != nil { panic(err) }
    if err := manager.SaveWorkflow("my-workflow", wf); err != nil { panic(err) }

    // Load workflow and context from storage
    loadedPlaces, loadedCtx, err := store.LoadState("my-workflow")
    if err != nil { panic(err) }
    fmt.Printf("Current places: %v, Title: %v\n", loadedPlaces, loadedCtx["title"])

    // Generate and print the workflow diagram
    diagram := wf.Diagram()
    fmt.Println(diagram)
}

Advanced Usage

Using the Workflow Manager

The workflow manager provides a high-level interface for managing workflow lifecycles and persistence:

// Create a registry and storage
registry := workflow.NewRegistry()
storage := workflow.NewSQLiteStorage("workflows.db")

// Create a workflow manager
manager := workflow.NewManager(registry, storage)

// Create a new workflow
wf, err := manager.CreateWorkflow("my-workflow", definition, "start")
if err != nil {
    panic(err)
}

// Get a workflow (loads from storage if not in registry)
wf, err = manager.GetWorkflow("my-workflow", definition)
if err != nil {
    panic(err)
}

// Save workflow state
err = manager.SaveWorkflow("my-workflow", wf)
if err != nil {
    panic(err)
}

// Delete a workflow
err = manager.DeleteWorkflow("my-workflow")
if err != nil {
    panic(err)
}
Adding Constraints

You can add constraints to transitions to control when they can be applied:

type MyConstraint struct{}

func (c *MyConstraint) Validate(event workflow.Event) error {
    // Add your validation logic here
    return nil
}

// Add the constraint to a transition
tr.AddConstraint(&MyConstraint{})
Using the Registry

The registry allows you to manage multiple workflows and is thread-safe for concurrent access:

registry := workflow.NewRegistry()

// Add a workflow
err := registry.AddWorkflow(wf)

// Get a workflow
wf, err := registry.Workflow("my-workflow")

// List all workflows
names := registry.ListWorkflows()

// Check if workflow exists
exists := registry.HasWorkflow("my-workflow")

// Remove a workflow
err = registry.RemoveWorkflow("my-workflow")

Thread Safety: The Registry is designed for concurrent access:

  • Read operations (Workflow, ListWorkflows, HasWorkflow) use read locks for optimal performance
  • Write operations (AddWorkflow, RemoveWorkflow) use write locks for data consistency
  • Concurrent reads and writes are properly synchronized
  • Race condition free - all operations are atomic

Example of concurrent usage:

registry := workflow.NewRegistry()

// Multiple goroutines can safely access the registry
go func() {
    for i := 0; i < 100; i++ {
        wf := createWorkflow(fmt.Sprintf("workflow-%d", i))
        registry.AddWorkflow(wf)
    }
}()

go func() {
    for i := 0; i < 100; i++ {
        name := fmt.Sprintf("workflow-%d", i)
        wf, err := registry.Workflow(name)
        if err == nil {
            // Process workflow
        }
    }
}()

go func() {
    for i := 0; i < 100; i++ {
        names := registry.ListWorkflows()
        // Process workflow list
    }
}()
Event Types

The workflow engine supports several event types:

  • EventBeforeTransition: Fired before a transition is applied
  • EventAfterTransition: Fired after a transition is applied
  • EventGuard: Fired to check if a transition is allowed
Context

You can attach context data to workflows:

wf.SetContext("key", "value")
value, ok := wf.Context("key")
Workflow Visualization

The package includes a Mermaid diagram generator for visualizing workflows. The generated diagrams can be rendered in any Mermaid-compatible viewer (like GitHub, GitLab, or the Mermaid Live Editor).

// Generate a Mermaid diagram
diagram := wf.Diagram()
fmt.Println(diagram)

Example output:

stateDiagram-v2
    classDef currentPlace font-weight:bold,stroke-width:4px
    start
    middle
    end
    start --> middle : to-middle
    middle --> end : to-end

    %% Current places
    class end currentPlace

    %% Initial place
    [*] --> start

Benchmarks

The package includes benchmarks for common operations. Run them with:

go test -bench=. ./...

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrTransitionNotAllowed = fmt.Errorf("transition not allowed")
	ErrInvalidPlace         = fmt.Errorf("invalid place")
	ErrInvalidTransition    = fmt.Errorf("invalid transition")
)

Common errors

Functions

This section is empty.

Types

type BaseEvent

type BaseEvent struct {
	// contains filtered or unexported fields
}

BaseEvent represents a workflow event

func NewEvent

func NewEvent(ctx context.Context, eventType EventType, transition *Transition, from []Place, to []Place, workflow *Workflow) *BaseEvent

NewEvent creates a new BaseEvent instance

func (*BaseEvent) Context

func (e *BaseEvent) Context() context.Context

Context returns the context.Context for the event

func (*BaseEvent) From

func (e *BaseEvent) From() []Place

From returns the source places of the transition

func (*BaseEvent) To

func (e *BaseEvent) To() []Place

To returns the target places of the transition

func (*BaseEvent) Transition

func (e *BaseEvent) Transition() *Transition

Transition returns the transition associated with the event

func (*BaseEvent) Type

func (e *BaseEvent) Type() EventType

Type returns the event type

func (*BaseEvent) Workflow

func (e *BaseEvent) Workflow() *Workflow

Workflow returns the workflow instance

type Constraint

type Constraint interface {
	Validate(Event) error
}

Constraint represents a validation constraint for a transition

type Definition

type Definition struct {
	Places      []Place
	Transitions []Transition

	// Default listeners for this workflow type
	Listeners map[EventType][]interface{}
}

Definition represents a workflow definition with places and transitions

func NewDefinition

func NewDefinition(places []Place, transitions []Transition) (*Definition, error)

NewDefinition creates a new workflow definition

func (*Definition) AddEventListener

func (d *Definition) AddEventListener(eventType EventType, listener EventListener)

AddEventListener adds a default event listener for a specific event type

func (*Definition) AddGuardEventListener

func (d *Definition) AddGuardEventListener(listener GuardEventListener)

AddGuardEventListener adds a default guard event listener

func (*Definition) AllPlaces

func (d *Definition) AllPlaces() []Place

AllPlaces returns all places (places) in the definition

func (*Definition) AllTransitions

func (d *Definition) AllTransitions() []Transition

AllTransitions returns all transitions in the definition

func (*Definition) Place

func (d *Definition) Place(place Place) bool

Place checks if a place exists in the definition

func (*Definition) RemoveEventListener

func (d *Definition) RemoveEventListener(eventType EventType, listener interface{})

RemoveEventListener removes a default event listener

func (*Definition) Transition

func (d *Definition) Transition(name string) *Transition

Transition returns a transition by name

type Event

type Event interface {
	Type() EventType
	Transition() *Transition
	From() []Place
	To() []Place
	Workflow() *Workflow
	Context() context.Context
}

Event defines the common interface for all event types

type EventListener

type EventListener func(Event) error

EventListener is a function that handles workflow events

type EventType

type EventType string

EventType represents the type of workflow event

const (
	// EventBeforeTransition is fired before a transition is applied
	EventBeforeTransition EventType = "before_transition"
	// EventAfterTransition is fired after a transition is applied
	EventAfterTransition EventType = "after_transition"
	// EventGuard is fired to check if a transition is allowed
	EventGuard EventType = "guard"
)

type GuardEvent

type GuardEvent struct {
	BaseEvent
	// contains filtered or unexported fields
}

GuardEvent represents a guard event in the workflow

func NewGuardEvent

func NewGuardEvent(ctx context.Context, transition *Transition, from []Place, to []Place, workflow *Workflow) *GuardEvent

NewGuardEvent creates a new Guard Event instance

func (*GuardEvent) IsBlocking

func (e *GuardEvent) IsBlocking() bool

IsBlocking returns whether the event is blocking

func (*GuardEvent) SetBlocking

func (e *GuardEvent) SetBlocking(blocking bool)

SetBlocking sets whether the event is blocking

type GuardEventListener

type GuardEventListener func(*GuardEvent) error

GuardEventListener is a function that handles guard events

type GuardListener

type GuardListener interface {
	HandleGuardEvent(*GuardEvent) error
}

GuardListener interface for handling guard events

type Listener

type Listener interface {
	HandleEvent(Event) error
}

Listener interface for handling events

type Manager

type Manager struct {

	// Dynamic listeners for all managed workflows
	Listeners map[EventType][]interface{}
	// contains filtered or unexported fields
}

Manager handles workflow instances and their persistence

func NewManager

func NewManager(registry *Registry, storage Storage) *Manager

NewManager creates a new workflow manager

func (*Manager) AddEventListener

func (m *Manager) AddEventListener(eventType EventType, listener EventListener)

AddEventListener adds a dynamic event listener for a specific event type

func (*Manager) AddGuardEventListener

func (m *Manager) AddGuardEventListener(listener GuardEventListener)

AddGuardEventListener adds a dynamic guard event listener

func (*Manager) CreateWorkflow

func (m *Manager) CreateWorkflow(id string, definition *Definition, initialPlace Place) (*Workflow, error)

CreateWorkflow creates a new workflow instance and saves it to storage

func (*Manager) DeleteWorkflow

func (m *Manager) DeleteWorkflow(id string) error

DeleteWorkflow removes a workflow instance and its state

func (*Manager) GetWorkflow

func (m *Manager) GetWorkflow(id string, definition *Definition) (*Workflow, error)

GetWorkflow gets a workflow instance from the registry or loads it from storage

func (*Manager) LoadWorkflow

func (m *Manager) LoadWorkflow(id string, definition *Definition) (*Workflow, error)

LoadWorkflow loads a workflow instance from storage

func (*Manager) RemoveEventListener

func (m *Manager) RemoveEventListener(eventType EventType, listener interface{})

RemoveEventListener removes a dynamic event listener

func (*Manager) SaveWorkflow

func (m *Manager) SaveWorkflow(id string, wf *Workflow) error

SaveWorkflow saves a workflow instance state to storage

type Marking

type Marking interface {
	// Places returns the current places
	Places() []Place
	// SetPlaces sets the current places
	SetPlaces(places []Place)
	// HasPlace checks if a place exists
	HasPlace(place Place) bool
	// AddPlace adds a place
	AddPlace(place Place) error
	// RemovePlace removes a place
	RemovePlace(place Place) error
}

Marking represents the current state of a workflow

func NewMarking

func NewMarking(places []Place) Marking

NewMarking creates a new marking instance

func UnmarshalMarkingJSON

func UnmarshalMarkingJSON(data []byte) (Marking, error)

UnmarshalMarkingJSON unmarshals JSON data into a Marking interface

type Place

type Place string

Place represents a state in the workflow

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry manages multiple workflows

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a new workflow registry

func (*Registry) AddWorkflow

func (r *Registry) AddWorkflow(wf *Workflow) error

AddWorkflow adds a workflow to the registry

func (*Registry) HasWorkflow

func (r *Registry) HasWorkflow(name string) bool

HasWorkflow checks if a workflow exists

func (*Registry) ListWorkflows

func (r *Registry) ListWorkflows() []string

ListWorkflows returns a list of all workflow names

func (*Registry) RemoveWorkflow

func (r *Registry) RemoveWorkflow(name string) error

RemoveWorkflow removes a workflow from the registry

func (*Registry) Workflow

func (r *Registry) Workflow(name string) (*Workflow, error)

Workflow returns a workflow by name

type Storage

type Storage interface {
	// LoadState loads the workflow's places and its context data for the given ID.
	LoadState(id string) (places []Place, context map[string]interface{}, err error)

	// SaveState saves the workflow's places and its context data for the given ID.
	SaveState(id string, places []Place, context map[string]interface{}) error

	// DeleteState removes the workflow state for the given ID.
	DeleteState(id string) error
}

Storage defines the interface for persisting workflow state. It is responsible for loading and saving the workflow's places (state) and its context data (custom fields).

type Transition

type Transition struct {
	// contains filtered or unexported fields
}

Transition represents a transition between places in the workflow

func MustNewTransition

func MustNewTransition(name string, from []Place, to []Place) *Transition

MustNewTransition is a helper that creates a new transition and panics on error. This is useful for defining transitions in a declarative way.

func NewTransition

func NewTransition(name string, from []Place, to []Place) (*Transition, error)

NewTransition creates a new transition

func (*Transition) AddConstraint

func (t *Transition) AddConstraint(constraint Constraint)

AddConstraint adds a constraint to the transition

func (*Transition) From

func (t *Transition) From() []Place

From returns the source places of the transition

func (*Transition) Metadata

func (t *Transition) Metadata(key string) (interface{}, bool)

Metadata returns the value for the given key from the transition metadata

func (*Transition) Name

func (t *Transition) Name() string

Name returns the transition name

func (*Transition) SetMetadata

func (t *Transition) SetMetadata(key string, value interface{})

SetMetadata sets metadata for the transition

func (*Transition) To

func (t *Transition) To() []Place

To returns the target places of the transition

type Workflow

type Workflow struct {
	// contains filtered or unexported fields
}

Workflow represents a workflow instance

func NewWorkflow

func NewWorkflow(name string, definition *Definition, initialPlace Place) (*Workflow, error)

NewWorkflow constructor

func (*Workflow) AddEventListener

func (w *Workflow) AddEventListener(eventType EventType, listener EventListener)

AddEventListener adds an event listener for a specific event type

func (*Workflow) AddGuardEventListener

func (w *Workflow) AddGuardEventListener(listener GuardEventListener)

AddGuardEventListener adds a guard event listener

func (*Workflow) Apply

func (w *Workflow) Apply(targetPlaces []Place) error

Apply applies a transition to the workflow

func (*Workflow) ApplyWithContext

func (w *Workflow) ApplyWithContext(ctx context.Context, targetPlaces []Place) error

ApplyWithContext applies a transition to the workflow with a context

func (*Workflow) Can

func (w *Workflow) Can(to []Place) error

Can check if transition to target places is possible

func (*Workflow) CanWithContext

func (w *Workflow) CanWithContext(ctx context.Context, to []Place) error

CanWithContext checks if transition to target places is possible with a context

func (*Workflow) Context

func (w *Workflow) Context(key string) (interface{}, bool)

Context returns the value for the given key from the workflow context

func (*Workflow) CurrentPlaces

func (w *Workflow) CurrentPlaces() []Place

CurrentPlaces returns the current places of the workflow

func (*Workflow) Definition

func (w *Workflow) Definition() *Definition

Definition returns the workflow definition

func (*Workflow) Diagram

func (w *Workflow) Diagram() string

Diagram generates a Mermaid state diagram for the workflow

func (*Workflow) EnabledTransitions

func (w *Workflow) EnabledTransitions() ([]Transition, error)

EnabledTransitions returns all transitions that can be applied in the current place

func (*Workflow) InitialPlace

func (w *Workflow) InitialPlace() Place

InitialPlace returns the initial place of the workflow

func (*Workflow) Marking

func (w *Workflow) Marking() Marking

Marking returns the current marking of the workflow

func (*Workflow) Name

func (w *Workflow) Name() string

Name returns the workflow name

func (*Workflow) RemoveEventListener

func (w *Workflow) RemoveEventListener(eventType EventType, listener interface{})

RemoveEventListener removes an event listener

func (*Workflow) SetContext

func (w *Workflow) SetContext(key string, value interface{})

SetContext sets a value in the workflow context

func (*Workflow) SetManager

func (w *Workflow) SetManager(m *Manager)

SetManager sets the manager pointer for this workflow

func (*Workflow) SetMarking

func (w *Workflow) SetMarking(marking Marking) error

SetMarking sets the workflow marking

Directories

Path Synopsis
examples
simple_flow command

Jump to

Keyboard shortcuts

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