workflow

package module
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 18 Imported by: 0

README

Go Workflow

CI Go Report Card Go Version License codecov

⚠️ A Note from the Developers:
This workflow engine is currently in active development. We're building it to be reliable and easy to use, but it's not quite ready for mission-critical production yet. We're a growing open-source project, and we truly welcome any feedback, contributions, or real-world testing you can offer to help us make it great.

If you like the idea of building smart, durable workflows in Go, please join us!

What is Go Workflow?

Go Workflow is a flexible engine for orchestrating steps, tasks, and data within your Go applications.

Our engine is rooted in Petri Net theory, which is just a fancy way of saying we use a rock-solid mathematical foundation to manage flow. When you model a complex process with many parallel paths, splits and joins are explicit, well-defined operations on a marking — not ad-hoc flags — and the model is in principle amenable to formal analysis. (Note: the library does not yet ship a static checker, so deadlock detection is on the roadmap, not a built-in guarantee.)

We're focused on portability and visualization, allowing you to change your complex processes easily without touching or recompiling your application code.

Inspired by Symfony Workflow Component.

Key Features

💻 Keep Your Code Clean & Flexible (Portability)
  • YAML Processes: Define your entire process structure and logic in simple YAML configuration files. This means your business teams can look at the flow, and your engineering team can deploy updates instantly.
  • Dynamic Decision Logic: Use an expressive guard language (powered by expr-lang/expr) to handle decisions like "If the amount is over $1000, send to manager approval". This logic lives in your config file, keeping it flexible and separate from your Go application code.
  • Context-Aware Storage: We built a flexible layer to reliably save the flow's current state and all its necessary data (like an order_id or user_role) to a database, ensuring long-running processes are safe from crashes.
✅ Built for Reliability (Petri Net Power)
  • Mathematically Sound: Our Petri Net core gives parallel paths and merging logic precise token semantics, which makes complex flows analyzable. A static validator/deadlock checker is planned but not yet shipped, so correctness of a given definition is still up to you today.
  • Thread-Safe Registry: The workflow registry uses proper locking to safely handle concurrent access from multiple goroutines.
  • Audit Trail Ready: Our pluggable history layer can record every transition, letting you track exactly who did what and when. Recording is opt-in: use the yaml.ApplyTransitionWithHistory helper or call the history store from your own event hooks — transitions are not logged automatically.
📊 Easy to Understand (Visualization)
  • Mermaid Diagram Generation: Quickly generate visual flowcharts from your definition. You can paste these diagrams into your documentation (like this README!) for instant visualization.
  • Workflow Manager: A clean, straightforward interface for loading, starting, and saving all your running workflow instances.
Technical Highlights
  • Support for multiple states and parallel transitions
  • Event system for workflow lifecycle hooks
  • Constraint system for custom validation logic
  • Comprehensive test coverage

Storage Layer

We designed the storage interface to be flexible. You tell us where to put the data and what extra information you need to store.

Storage Interface

The package provides a flexible, context-aware storage interface for persisting workflow markings and context data. Every method takes a context.Context so callers can apply cancellation and deadlines:

type Storage interface {
    // LoadState loads the workflow's marking and its context data for the given ID.
    LoadState(ctx context.Context, id string) (marking Marking, context map[string]any, err error)

    // SaveState saves the workflow's marking and its context data for the given ID.
    SaveState(ctx context.Context, id string, marking Marking, context map[string]any) error

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

You can implement your own storage backend by implementing this interface. The package includes SQLite and PostgreSQL implementations with options for custom fields (both also implement VersionedStorage for optimistic concurrency and expose SaveStateTx/LoadStateTx for transactional use):

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

// Create a SQLite storage with custom fields
store, 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 the schema
if err := storage.Initialize(db, store.GenerateSchema()); err != nil { panic(err) }

// Save the marking together with context data
marking := workflow.NewMarking([]workflow.Place{"draft"})
err = store.SaveState(ctx, "my-workflow", marking, map[string]any{"title": "My Doc", "owner": "alice"})

// Load the marking and context data
loaded, data, err := store.LoadState(ctx, "my-workflow")
fmt.Println(loaded.Places(), data["title"], data["owner"])

History Layer

The history layer is a pluggable audit trail, letting you track and query every single state change that happens in your processes. It supports custom fields, pagination, and filtering.

History Interface
type HistoryStore interface {
    SaveTransition(ctx context.Context, record *TransitionRecord) error
    ListHistory(ctx context.Context, workflowID string, opts QueryOptions) ([]TransitionRecord, error)
    GenerateSchema() string
    Initialize(ctx context.Context) error
}

Note that history is opt-in: transitions are not recorded automatically. Call SaveTransition yourself (for example from an EventAfterTransition listener), or use the yaml.ApplyTransitionWithHistory helper, which applies a transition and records it in one call.

SQLite History Example
import "github.com/ehabterra/workflow/history"

historyStore := history.NewSQLiteHistory(db,
    history.WithCustomFields(map[string]string{
        "ip_address": "ip_address TEXT",
    }),
)
if err := historyStore.Initialize(ctx); err != nil { panic(err) }

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

// List history with pagination
records, err := historyStore.ListHistory(ctx, "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
  • PostgreSQL storage implementation
  • Optimistic concurrency (VersionedStorage) and transactional building blocks (RunInTx, SaveStateTx/LoadStateTx)
  • Colored Petri Nets (CPN): multiple data-carrying tokens per place, per-token firing, and token-aware guards — see docs/guides/CPN_GUIDE.md
  • Host-driven timers: declare durations (after: 72h), the host owns the clock — timeouts and escalation with no internal scheduler — see docs/guides/TIMERS_GUIDE.md
  • Support for parallel transitions and branching
  • Workflow history and audit trail (in examples)
  • Web UI for workflow management (in examples)
  • YAML configuration support

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!

Building Smarter Workflows: The Roadmap 🚀

We're starting with the basics of Petri Nets and adding layers of powerful features inspired by industry tools, focusing on solving common developer problems.

High Priority: Making it Durable and Multi-Tasking
Feature Description The Problem We're Solving Petri Net Concept
Smart Tokens (Colored Petri Nets - CPN) — ✅ shipped How do you process a batch of 100 orders within one workflow instance? Tokens carry data. Tokens carry attributes (like an order ID), allowing one process to manage multiple concurrent items. See docs/guides/CPN_GUIDE.md.
Nested Workflows (HCPN) My process has 100 steps—it's too big to manage. Modularity. You can define a complex sub-flow (like "Payment Verification") once and drop it into any main process as a single, clean step.
Crash-Safe Storage (ACID) — ✅ shipped What if the server dies during a critical step? Data Integrity. Optimistic concurrency (VersionedStorage), transactional building blocks (RunInTx, SaveStateTx/LoadStateTx), and an atomic execute helper (Manager.Execute with WithTxSideEffect) commit a state change and its history record in one transaction.
Undo Button (Compensation/Rollback) I need a way to reliably "undo" a previous action if a later step fails (e.g., refunding a charge). Error Recovery. We'll track the history of completed tasks precisely, enabling safe, structured rollbacks.
Advanced Synchronization When two parallel paths finish, how do I make the third step wait only for the first path, but cancel the second one? Complex Merging. We're implementing advanced logic (like nested AND/OR/XOR conditions and discriminators) for robust handling of parallel flows.
Medium Priority: Handling Time and External Events
Feature Description The Problem We're Solving Petri Net Concept
Timeouts and Scheduled Steps (TPN/DPN) — ✅ shipped I need a task to wait exactly 30 minutes before starting, or to timeout after 24 hours. Time Awareness. Transitions declare durations (after: 72h); the host owns the clock (ListDue + FireDue cron), so there is no internal scheduler. See docs/guides/TIMERS_GUIDE.md.
Workflow Checker (Validation System) How do I know my new YAML definition won't cause a bug? Pre-Execution Guarantees. We'll use Petri Net math to check the flow for common issues like deadlocks before you deploy it.
Talk to Other Workflows (Message Correlation) I need Workflow A to wait for a signal from a completely separate Workflow B. Inter-Process Communication. Building a reliable way for instances to communicate and find each other using shared data keys.
Additional Planned Features
  • Build standalone web interface for workflow management
  • Enhance REST API endpoints
  • Add process variable scope management - local vs global variable scoping hierarchy
  • Add workflow versioning support
  • Create workflow templates system
  • Implement role-based access control
  • Add weighted transitions - token counting requirements for transition firing
  • Workflow statistics and analytics
  • Export/Import workflow definitions - complete YAML round-trip (export Go definitions back to YAML) and/or PNML (Petri Net Markup Language) support for interoperability with other Petri net tools

Understanding the Technical Foundation

This workflow engine is built on Petri net foundations and incorporates concepts inspired by BPMN to make it suitable for business process automation. The goal is not full BPMN compliance, but rather to enhance Petri nets with practical, business-reliable features.

Below are some of the advanced concepts we're working on. Don't worry if these seem complex—the basic workflow functionality is straightforward and well-documented above.

Colored Petri Nets (CPN)

What is a Token? In Petri nets, a token is a marker that sits in a place (state). A place can have 0, 1, or multiple tokens. The engine's unified marking supports both the simple boolean style (a place is either marked or not) and full colored tokens (multiple data-carrying tokens per place).

What is CPN? Colored Petri Nets allow tokens to carry data attributes (called "color"). This enables:

  • Per-token data: Each token in a place can have different attributes (e.g., order ID, customer name, amount)
  • Multiple tokens per place: One workflow instance can have multiple tokens in the same place, each with different data
  • Data-driven decisions: Transitions can evaluate token attributes to route tokens differently
  • Resource tracking: Tokens can carry resource assignments (e.g., which employee is handling this task)

Current Implementation (CPN is shipped):

  • Multiple workflow instances: You CAN have workflow-1, workflow-2, workflow-3, each in "pending" with different contexts
  • Multiple tokens per place: One workflow instance CAN hold multiple data-carrying tokens in the same place; transitions fire per token, and guards can inspect token attributes
  • Token queries: Marking exposes TokensAt, TokenCount, AllTokens, HasToken alongside the simple place set
  • See docs/guides/CPN_GUIDE.md and the runnable examples/cpn_batch_processing / examples/cpn_routing examples

CPN Example (within ONE workflow instance):

Workflow: "order-batch-1"
Place: "pending_review"
  ├─ Token 1: {order_id: "001", amount: 100, customer: "Alice"}
  ├─ Token 2: {order_id: "002", amount: 500, customer: "Bob"}  
  └─ Token 3: {order_id: "003", amount: 50, customer: "Charlie"}

Transition "route_by_amount" evaluates each token:
  - Token 1 (amount: 100) → "auto_approve"
  - Token 2 (amount: 500) → "manager_approval"  
  - Token 3 (amount: 50) → "auto_approve"

Advantages of Multiple Tokens per Workflow:

  1. Batch Processing & Atomic Operations: Process multiple items as a single unit. Example: "All 10 orders in this batch must be approved before shipping" - if one fails, the entire batch can be rolled back.

  2. Shared Workflow Context: All tokens share workflow-level data (e.g., batch ID, shipping date, warehouse location). Changes to workflow context affect all tokens simultaneously.

  3. Coordination & Synchronization: Tokens can wait for each other. Example: "Wait until all 3 documents are reviewed before proceeding" - enables complex synchronization patterns.

  4. Resource Efficiency: One workflow instance instead of N instances reduces overhead, storage, and management complexity. Single workflow ID to track instead of managing hundreds.

  5. Workflow-Level Constraints: Enforce rules across all tokens. Example: "Total batch value must not exceed $10,000" - can evaluate sum of all token amounts before allowing transition.

  6. Simplified State Management: One workflow state instead of tracking N separate states. Easier to query "show me all batches in review" vs "show me all individual orders in review".

  7. Parallel Processing with Shared State: Tokens can process in parallel while sharing workflow-level resources (e.g., shared approval queue, shared budget pool).

Example Use Cases:

  • Document Batch Processing: Process 100 invoices together, all must be validated before payment batch is created
  • Order Fulfillment: Ship multiple items together - all must be ready before shipping
  • Approval Workflows: Multiple documents need approval, but workflow-level rules apply (e.g., "if any document is rejected, reject entire batch")
  • Assembly Lines: Multiple parts (tokens) flow through assembly, but assembly-level constraints apply (e.g., "all parts must be quality-checked before assembly")

Relation to Weighted Transitions: Weighted transitions require multiple tokens to fire (e.g., "need 3 tokens in approval place"). CPN adds data to those tokens, so you can track WHICH 3 approvals you have (e.g., manager, director, finance).

Hierarchical Colored Petri Nets (HCPN)

What is HCPN? Hierarchical Colored Petri Nets allow workflows to contain other workflows as sub-processes, creating different levels of abstraction and refinement. This enables:

  • Sub-processes: A place or transition can expand into a complete nested workflow
  • Modular design: Break complex workflows into reusable, manageable components
  • Abstraction levels: View workflow at high level (summary) or drill down into details (sub-process)
  • Reusable fragments: Define common process patterns once and reuse across multiple workflows

BPMN-Inspired Concept: This concept is inspired by BPMN sub-processes, embedded sub-processes, and reusable process fragments. Essential for managing large enterprise workflows where complexity requires decomposition.

Example Use Case:

Main Workflow: "Order Processing"
  ├─ Place: "payment_processing" 
  │   └─ Expands to Sub-Workflow: "Payment Workflow"
  │       ├─ Validate card
  │       ├─ Charge payment
  │       └─ Send receipt
  ├─ Place: "inventory_check"
  │   └─ Expands to Sub-Workflow: "Inventory Workflow"
  │       ├─ Check stock
  │       ├─ Reserve items
  │       └─ Update availability
  └─ Place: "shipping"
      └─ Expands to Sub-Workflow: "Shipping Workflow"

Benefits:

  • Complexity Management: Break 100+ step workflows into logical modules
  • Reusability: Define "approval workflow" once, use in multiple parent workflows
  • Maintainability: Update sub-process in one place, affects all parent workflows
  • Team Collaboration: Different teams can work on different sub-processes independently
  • Testing: Test sub-processes in isolation before integrating into parent workflow
Timed Petri Nets (TPN/DPN)

Time Petri Nets (TPN): Use time intervals for transitions (e.g., "between 10-30 minutes"). Transitions can fire within a time window.

Duration Petri Nets (DPN): Use deterministic time values for transitions (e.g., "exactly 30 minutes"). Transitions require a fixed duration before completion.

BPMN-Inspired Concept: Inspired by BPMN Timer Events, which introduce delays or scheduled execution. Shipped as host-driven timers: a transition declares a duration (after: 72h in YAML, or SetTimeoutAfter), tokens record when they entered a place, and Workflow.Due(now) is a pure function of the marking and the host's clock. There is no internal scheduler — a host cron scans the fleet with Manager.ListDue and advances due instances with Manager.FireDue, which makes it restart-safe by construction (state in the database, clock in the host). See docs/guides/TIMERS_GUIDE.md.

Business Process Reliability Features

The following features are inspired by BPMN and designed to make the engine production-ready for enterprise use:

  • Transactional Persistence: Ensures state changes are atomic (all or nothing) and durable, so your processes survive crashes and restarts
  • Message Correlation: Allows different workflow instances to communicate with each other and wait for external signals
  • Compensation/Rollback: Tracks what happened so you can safely undo previous steps if something goes wrong
  • Variable Scope Management: Proper data isolation between tasks (local variables) and shared workflow data (global variables)

Installation

go get github.com/ehabterra/workflow

YAML Configuration (New!)

You can define your entire process in a simple YAML file with expression-based guards, metadata, and storage configuration. See the YAML documentation for complete details.

Quick Example:

workflow:
  name: blog_publishing
  initial_marking: draft
  transitions:
    - name: to_review
      from: [draft]
      to: [reviewed]
      guard: "workflow.Context('word_count') <= 500 and hasRole('author')"
      notes: "Submitted for review"
      actor: "author"
import "github.com/ehabterra/workflow/yaml"

config, _ := yaml.LoadConfig("workflow.yaml")
loader := yaml.NewLoader()
wf, _ := loader.LoadWorkflow(config, "blog-post-1")

Expression-Based Guards

The workflow engine supports powerful expression-based guards using the expr-lang/expr expression language. This lets you write conditions like "only allow this transition if the user is a manager and the amount is over $1000" directly in your YAML configuration. Expressions are evaluated at runtime to determine if a transition is allowed.

Basic Usage

Expressions are defined in YAML configuration or programmatically:

transitions:
  - name: publish
    from: [reviewed]
    to: [published]
    guard: "hasRole('editor') or hasRole('admin')"
import "github.com/ehabterra/workflow"

// Programmatic usage
constraint, _ := workflow.NewExpressionConstraint(
    "workflow.Context('amount') > 1000 and hasRole('manager')",
)
transition.AddConstraint(constraint)
Available Variables

Expressions have access to the following variables:

  • workflow - The workflow instance (access context, places, etc.)
  • transition - The transition name (string)
  • from - Source places ([]Place)
  • to - Target places ([]Place)
  • All workflow context values - Accessible directly by key
Helper Functions

The expression environment includes several built-in helper functions:

hasRole(role string) bool

Checks if the workflow has a specific role. Roles are stored in workflow context as roles (can be string, []string, or []interface{}).

guard: "hasRole('admin')"
guard: "hasRole('editor') or hasRole('manager')"
wf.SetContext("roles", []string{"author", "editor"})
hasPermission(permission string) bool

Checks if the workflow has a specific permission. Permissions are stored in workflow context as permissions.

guard: "hasPermission('publish')"
in(value, list) bool

Checks if a value exists in a list.

guard: "workflow.Context('status') in ['active', 'pending']"
Expression Examples

Simple role check:

guard: "hasRole('admin')"

Multiple conditions:

guard: "workflow.Context('amount') > 1000 and hasRole('manager')"

Complex logic:

guard: "hasRole('editor') or (hasRole('author') and workflow.Context('word_count') <= 500)"

Using context values:

guard: "workflow.Context('status') == 'active' and workflow.Context('priority') > 5"

Combining conditions:

guard: "(hasRole('admin') or hasRole('editor')) and workflow.Context('approved') == true"

List operations:

guard: "workflow.Context('tags') contains 'urgent'"
guard: "workflow.Context('category') in ['tech', 'news', 'opinion']"
Custom Environment Builder

You can provide a custom environment builder to add your own variables and functions:

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

loader := yaml.NewLoaderWithEnv(func(event workflow.Event) map[string]interface{} {
    env := make(map[string]interface{})
    
    // Add custom variables
    env["user"] = getUserFromContext(event)
    env["request"] = getRequestFromContext(event)
    env["currentTime"] = time.Now()
    
    // Add custom functions
    env["isBusinessHours"] = func() bool {
        now := time.Now()
        return now.Hour() >= 9 && now.Hour() < 17
    }
    
    env["isWeekend"] = func() bool {
        weekday := time.Now().Weekday()
        return weekday == time.Saturday || weekday == time.Sunday
    }
    
    env["hasAccess"] = func(resource string) bool {
        // Your custom access control logic
        return checkAccess(event, resource)
    }
    
    return env
})

// Now you can use these in expressions:
// guard: "isBusinessHours() and hasAccess('publish')"
// guard: "user.Department == 'Engineering' and not isWeekend()"
Expression Constraints

Expressions can also be used programmatically as constraints:

import "github.com/ehabterra/workflow"

// Create expression constraint
constraint, err := workflow.NewExpressionConstraint(
    "workflow.Context('balance') >= workflow.Context('amount')",
)
if err != nil {
    // Handle compilation error
}

// Add to transition
transition.AddConstraint(constraint)

// Or with custom environment
constraint, err := workflow.NewExpressionConstraintWithEnv(
    "customFunction(workflow.Context('data'))",
    customEnvBuilder,
)
Expression Safety

The expr library provides several safety guarantees:

  • Memory-Safe: No memory vulnerabilities
  • Side-Effect-Free: Expressions only compute outputs from inputs
  • Always Terminating: Prevents infinite loops
  • Type-Safe: Static type checking at compile time
Error Handling

If an expression fails to compile or evaluate, the transition will be blocked:

constraint, err := workflow.NewExpressionConstraint("invalid syntax")
if err != nil {
    // Expression compilation failed
    log.Printf("Expression error: %v", err)
}

// At runtime, if evaluation fails, the transition is blocked
err = wf.Apply([]workflow.Place{"published"})
if err != nil {
    // Transition blocked - check if it was due to expression
}

Quick Start

Here's a simple example showing how to create a workflow, persist it to storage, and apply transitions. This example demonstrates the context-aware storage and options pattern:

package main

import (
    "context"
    "database/sql"
    "fmt"

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

func main() {
    ctx := context.Background()

    // 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 data
    wf, err := manager.CreateWorkflow(ctx, "my-workflow", definition, "start")
    if err != nil { panic(err) }
    wf.SetContext("title", "My Example Workflow")
    if err := manager.SaveWorkflow(ctx, "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(ctx, "my-workflow", wf); err != nil { panic(err) }

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

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

Advanced Usage

Using the Workflow Manager

The workflow manager provides a straightforward interface for creating, loading, and saving workflows. It handles the coordination between the registry (in-memory cache) and storage (database persistence):

ctx := context.Background()

// Create a registry and a storage backend
db, err := sql.Open("sqlite3", "workflows.db")
if err != nil {
    panic(err)
}
store, err := storage.NewSQLiteStorage(db)
if err != nil {
    panic(err)
}
if err := storage.Initialize(db, store.GenerateSchema()); err != nil {
    panic(err)
}
registry := workflow.NewRegistry()

// Create a workflow manager (options like WithoutRegistryCache are available)
manager := workflow.NewManager(registry, store)

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

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

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

// Delete a workflow
err = manager.DeleteWorkflow(ctx, "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

We're building this engine in the open and would love your help! If you're interested in database durability, expressive language parsing, or Petri Net theory, please take a look at our issues or submit a Pull Request. Every contribution, big or small, helps make this project better.

License

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

Documentation

Overview

Package workflow is a Petri-net based workflow engine for Go.

A workflow is defined by a set of places (states) and transitions between them. The current state of a running instance is its marking — the set of places that currently hold a token. Applying a transition consumes tokens from its source places and produces tokens in its target places, which makes parallel splits and synchronizing joins first-class rather than bolted on.

Core types

  • Definition describes the static shape of a process: its [Place]s and [Transition]s. Build one with NewDefinition.
  • Workflow is a live instance of a Definition. Create one with NewWorkflow (or via a Manager) and drive it with Apply / ApplyTransition and their WithContext variants.
  • Marking holds the current places of an instance.
  • Registry is a thread-safe in-memory collection of workflows.
  • Manager coordinates a Registry with a Storage backend for persistence.

Guards and events

Transitions can carry constraints that decide whether they may fire. Guard logic is commonly expressed with the expr-lang expression language (see the yaml sub-package and NewExpressionConstraint). Lifecycle events (EventBeforeTransition, EventAfterTransition, EventGuard) let callers hook into transitions; every Event carries a context.Context.

Persistence

Storage and the history store persist workflow state and an audit trail. Both interfaces take a context.Context as their first argument so callers can apply cancellation and deadlines. A SQLite implementation ships in the storage and history sub-packages; you can supply your own by implementing the interfaces.

Configuration and visualization

Workflows can be defined programmatically or loaded from YAML (see the yaml sub-package). Workflow.Diagram renders a Mermaid state diagram for documentation.

Status

Implemented and tested: the unified colored-token marking (a place can hold multiple data-carrying tokens; simple boolean workflows are the single-token special case), per-token transition firing, token-aware guards, SQLite and PostgreSQL storage backends with optimistic concurrency (VersionedStorage) and transactional building blocks, SQLite and PostgreSQL history stores, the strict YAML loader with the polymorphic initial_marking key, and Mermaid diagram generation.

Not yet available (tracked in ROADMAP.md): timers/scheduling, a signals API, static validation (deadlock/soundness checking), hierarchical workflows (HCPN), compensation/rollback, and observability hooks.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrTransitionNotAllowed is returned when a transition exists but its guards
	// or the current marking do not permit it to fire.
	//
	// It is the general condition; the two concrete reasons — ErrNotEnabled and
	// ErrGuardRejected — both satisfy errors.Is(err, ErrTransitionNotAllowed), so
	// existing callers that test the general sentinel keep working while new
	// callers can distinguish the cause.
	ErrTransitionNotAllowed = errors.New("transition not allowed")

	// ErrNotEnabled is returned when a transition cannot fire because the current
	// marking does not enable it — one or more of its input places is unmarked.
	// This is the "try again later / already advanced" case: for example, a
	// webhook redelivery that re-fires a transition an earlier delivery already
	// took. It satisfies errors.Is(err, ErrTransitionNotAllowed).
	ErrNotEnabled = &blockedError{msg: "transition not enabled in current marking"}

	// ErrGuardRejected is returned when a transition is enabled by the marking but
	// a guard constraint or a blocking guard listener refused it — the "forbidden
	// / conditions not met" case (e.g. insufficient permissions, amount over a
	// limit). It satisfies errors.Is(err, ErrTransitionNotAllowed).
	ErrGuardRejected = &blockedError{msg: "transition rejected by guard"}

	// ErrTransitionNotFound is returned when no transition matches the requested
	// name or target places.
	ErrTransitionNotFound = errors.New("transition not found")

	// ErrInvalidPlace is returned for a place that is not defined in the workflow.
	ErrInvalidPlace = errors.New("invalid place")

	// ErrInvalidTransition is returned when a transition definition is invalid.
	ErrInvalidTransition = errors.New("invalid transition")

	// ErrPlaceNotFound is returned when a place is not present in the current marking.
	ErrPlaceNotFound = errors.New("place not found")

	// ErrWorkflowNotFound is returned when a workflow cannot be located in the
	// registry or storage.
	ErrWorkflowNotFound = errors.New("workflow not found")

	// ErrWorkflowExists is returned when adding a workflow whose name is already
	// registered.
	ErrWorkflowExists = errors.New("workflow already exists")

	// ErrInvalidWorkflow is returned when a workflow or its definition is nil or
	// otherwise malformed.
	ErrInvalidWorkflow = errors.New("invalid workflow")

	// ErrInvalidDefinition is returned when a workflow definition is nil or invalid.
	ErrInvalidDefinition = errors.New("invalid definition")

	// ErrInvalidMarking is returned when a marking is nil or invalid.
	ErrInvalidMarking = errors.New("invalid marking")

	// ErrInvalidExpression is returned when a guard expression is empty or fails to
	// compile.
	ErrInvalidExpression = errors.New("invalid expression")

	// ErrConflict is returned by a VersionedStorage when a save is rejected because
	// the stored version no longer matches the expected version — i.e. another writer
	// updated the workflow first (optimistic-concurrency conflict).
	ErrConflict = errors.New("version conflict")

	// ErrInvalidToken is returned when a token is malformed (for example it has an
	// empty ID).
	ErrInvalidToken = errors.New("invalid token")

	// ErrTokenNotFound is returned when a token cannot be found in a place.
	ErrTokenNotFound = errors.New("token not found")

	// ErrDefinitionMismatch is returned by the Manager when a persisted instance
	// was created against a different workflow definition than the one supplied to
	// load it (the stored definition fingerprint differs). It guards against
	// silently running an old marking against an incompatible definition. Supply a
	// migration handler (WithDefinitionMigration) to load such instances anyway.
	ErrDefinitionMismatch = errors.New("workflow definition mismatch")
)

Sentinel errors returned by the workflow engine. Callers should test for these with errors.Is rather than comparing error strings, since most are returned wrapped with additional context (via fmt.Errorf("...: %w", err)).

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, tokens []Token, workflow *Workflow) *BaseEvent

NewEvent creates a new BaseEvent instance. tokens are the tokens involved in the firing (may be nil for a boolean firing).

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) Tokens added in v0.8.0

func (e *BaseEvent) Tokens() []Token

Tokens returns the tokens involved in this firing.

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
	// contains filtered or unexported fields
}

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) *ListenerHandle

AddEventListener adds a default event listener for a specific event type It returns a handle that can be used to remove the listener later

func (*Definition) AddGuardEventListener

func (d *Definition) AddGuardEventListener(listener GuardEventListener) *ListenerHandle

AddGuardEventListener adds a default guard event listener It returns a handle that can be used to remove the listener later

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) Fingerprint added in v0.8.0

func (d *Definition) Fingerprint() string

Fingerprint returns a stable SHA-256 hash of the definition's structure: its places and, for every transition, the name, input places, output places, and guard expression string (as stored in the "guard" metadata). It is order- independent — places and transitions are canonically sorted first — so two definitions built in different orders but describing the same net share a fingerprint. Every component is length-prefixed before hashing, so no choice of place or transition name (including separator-looking characters) can make two structurally different definitions collide.

The Manager stamps this on each persisted instance and compares it on load to catch a definition changing under running instances (see ErrDefinitionMismatch). Note: only expression guards recorded in transition metadata are captured; programmatic Go constraints without a "guard" metadata string are not part of the hash.

func (*Definition) ListenerCount added in v0.8.0

func (d *Definition) ListenerCount(eventType EventType) int

ListenerCount returns the number of listeners registered for eventType.

func (*Definition) Place

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

Place checks if a place exists in the definition

func (*Definition) RemoveListener added in v0.8.0

func (d *Definition) RemoveListener(handle *ListenerHandle)

RemoveListener removes a listener using its handle This is the recommended way to remove listeners as it's reliable and efficient

func (*Definition) Transition

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

Transition returns a pointer to the transition with the given name, or nil if none matches. It points into the definition's own slice, so mutations through it (e.g. SetTimeoutAfter, SetMetadata) actually apply to the definition.

type DefinitionMigrationFunc added in v0.8.0

type DefinitionMigrationFunc func(ctx context.Context, id, storedFingerprint, currentFingerprint string) error

DefinitionMigrationFunc is called by the Manager when a persisted instance's stored definition fingerprint differs from the definition supplied to load it. Returning nil lets the load proceed (the caller has confirmed the change is safe, or has migrated the marking); returning an error aborts the load with that error. storedFingerprint is empty for instances saved before fingerprints were recorded.

type DueStorage added in v0.8.0

type DueStorage interface {
	VersionedStorage

	// SaveVersionedStateWithDue behaves like SaveVersionedState but also records
	// the instance's next-due time (nil clears it, so a workflow that reaches a
	// timer-free state drops out of ListDue). The Manager calls it in place of
	// SaveVersionedState so the due index is maintained on every save.
	SaveVersionedStateWithDue(ctx context.Context, id string, marking Marking, context map[string]any, expectedVersion int64, due *time.Time) (newVersion int64, err error)

	// ListDue returns the IDs of instances whose stored next-due time is
	// non-null and at or before `before`, ordered by due time ascending (then by
	// ID) for stable pagination. A zero limit means no limit. Instances with no
	// running timer (NULL due) are never returned.
	ListDue(ctx context.Context, before time.Time, limit int) ([]string, error)
}

DueStorage is an optional interface a VersionedStorage backend may implement to maintain a per-instance "next due" index and scan the fleet for instances whose deadline has elapsed. It is the storage primitive behind host-driven timers (M4): a host cron finds the due instances with ListDue and advances them with Manager.FireDue, turning a fleet-wide deadline ("escalate if not approved in 3 days") into a single indexed query instead of loading and inspecting every instance.

Storage never interprets time itself: the next-due wall-clock is computed by the Manager from the workflow definition (which storage does not know) via the marking's token entry times, and handed to storage on every save. A nil due means no timer is currently running for the instance — the backend stores SQL NULL and such an instance never matches ListDue.

The interface embeds VersionedStorage because the fleet-timer model is inherently multi-writer (many hosts may scan the same fleet); optimistic concurrency is what keeps concurrent FireDue calls from clobbering each other.

type Event

type Event interface {
	Type() EventType
	Transition() *Transition
	From() []Place
	To() []Place
	// Tokens returns the tokens involved in this firing: exactly one for per-token
	// firing (ApplyTransitionForToken), the consumed colored tokens for a
	// whole-marking firing, or empty for a purely boolean firing.
	Tokens() []Token
	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 ExecuteOption added in v0.8.0

type ExecuteOption func(*executeConfig)

ExecuteOption configures a single Manager.Execute call.

func WithMaxRetries added in v0.8.0

func WithMaxRetries(attempts int) ExecuteOption

WithMaxRetries sets how many times Execute retries the whole load-fn-save cycle when the save hits an optimistic-concurrency conflict. The default is 5. Raise it for instances with many concurrent writers.

func WithTxSideEffect added in v0.8.0

func WithTxSideEffect(effect TxSideEffect) ExecuteOption

WithTxSideEffect registers a write committed atomically with the state save — the crash-consistent way to append an audit/history record or an outbox row for a firing. Requires the Manager's storage to implement TransactionalStorage (the SQLite and Postgres backends do); Execute fails otherwise rather than silently losing atomicity.

Effects registered here differ from listener side effects: an effect shares the save's transaction (state and effect commit or roll back together), while a listener's external side effects happen immediately and are not undone by a later conflict or crash. The canonical use is a history record built inside fn (capturing the fired transition) and written by the effect:

var record *history.TransitionRecord
err := mgr.Execute(ctx, id, def,
    func(wf *workflow.Workflow) error {
        if err := wf.ApplyTransition("approve"); err != nil {
            return err
        }
        record = &history.TransitionRecord{WorkflowID: id, Transition: "approve", ...}
        return nil
    },
    workflow.WithTxSideEffect(func(ctx context.Context, tx any) error {
        return hist.SaveTransitionTx(ctx, tx.(*sql.Tx), record)
    }),
)

type ExpressionConstraint added in v0.8.0

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

ExpressionConstraint evaluates an expression string to determine if transition is allowed. Uses expr-lang/expr for safe expression evaluation.

func NewExpressionConstraint added in v0.8.0

func NewExpressionConstraint(exprStr string) (*ExpressionConstraint, error)

NewExpressionConstraint creates a new expression constraint. The expression should return a boolean value. If the expression returns false or evaluates to an error, the transition is blocked.

func NewExpressionConstraintWithEnv added in v0.8.0

func NewExpressionConstraintWithEnv(exprStr string, envBuilder func(Event) map[string]any) (*ExpressionConstraint, error)

NewExpressionConstraintWithEnv creates a new expression constraint with a custom environment builder. This allows you to provide custom functions and variables for expression evaluation.

func (*ExpressionConstraint) Validate added in v0.8.0

func (c *ExpressionConstraint) Validate(event Event) error

Validate evaluates the expression and returns error if transition should be blocked.

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, tokens []Token, workflow *Workflow) *GuardEvent

NewGuardEvent creates a new Guard Event instance. tokens are the tokens involved in the firing (may be nil for a boolean firing); for per-token firing this is the single token being advanced, which the guard can inspect.

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 ListOptions added in v0.8.0

type ListOptions struct {
	Limit  int
	Offset int
}

ListOptions controls pagination for enumerating persisted workflow IDs. A zero Limit means no limit.

type ListableStorage added in v0.8.0

type ListableStorage interface {
	Storage

	// ListIDs returns persisted workflow IDs ordered by ID for stable pagination.
	ListIDs(ctx context.Context, opts ListOptions) ([]string, error)
}

ListableStorage is an optional interface a Storage backend may implement to enumerate the IDs of persisted workflows. It is the primitive a host needs to scan the fleet — for example a cron sweeping instances awaiting a deadline — without dropping to raw SQL. The Manager exposes it via ListWorkflowIDs.

type Listener

type Listener interface {
	HandleEvent(Event) error
}

Listener interface for handling events

type ListenerHandle added in v0.8.0

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

ListenerHandle represents a handle to remove a listener This provides a reliable way to remove listeners without needing the exact function value

type Manager

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

Manager handles workflow instances and their persistence.

The Manager reserves the context key "__workflow_def_fingerprint" for the definition fingerprint it stamps on every save: a user value stored under that key is overwritten on save and stripped on load.

func NewManager

func NewManager(registry *Registry, storage Storage, opts ...ManagerOption) *Manager

NewManager creates a new workflow manager.

func (*Manager) AddEventListener

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

AddEventListener adds a dynamic event listener for a specific event type It returns a handle that can be used to remove the listener later

func (*Manager) AddGuardEventListener

func (m *Manager) AddGuardEventListener(listener GuardEventListener) *ListenerHandle

AddGuardEventListener adds a dynamic guard event listener It returns a handle that can be used to remove the listener later

func (*Manager) CreateWorkflow

func (m *Manager) CreateWorkflow(ctx context.Context, 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(ctx context.Context, id string) error

DeleteWorkflow removes a workflow instance and its state

func (*Manager) EvictWorkflow added in v0.8.0

func (m *Manager) EvictWorkflow(id string)

EvictWorkflow drops a workflow from the in-memory registry cache without touching storage. Call it after saving when a long-lived Manager would otherwise accumulate instances or serve a stale cached copy; a subsequent GetWorkflow/LoadWorkflow then reads fresh from storage. With registry caching disabled it is a no-op.

func (*Manager) Execute added in v0.8.0

func (m *Manager) Execute(ctx context.Context, id string, definition *Definition, fn func(*Workflow) error, opts ...ExecuteOption) error

Execute atomically advances a persisted instance: it loads the instance fresh from storage, runs fn against it (fn typically applies one or more transitions), and saves it back under optimistic concurrency. If the save conflicts with a concurrent writer (ErrConflict), the whole cycle retries on fresh state up to a bounded number of times.

This is the recommended way to react to an external event (a webhook, a UI action, a timer) because it removes the load / fire / save / retry boilerplate and always operates on current state — even with registry caching disabled or across replicas. fn must be safe to re-run, since a conflict re-invokes it on reloaded state; a transition that is no longer enabled on reload returns ErrNotEnabled from within fn, which Execute surfaces to the caller.

Side-effect semantics: fn (and any listeners it triggers) may run more than once across retries, and a listener's external side effects are not rolled back by a later conflict — make them idempotent, or perform them after Execute returns. Writes that must be crash-consistent with the state change (history records, outbox rows) belong in a WithTxSideEffect option, which commits them in the same transaction as the save.

func (*Manager) FireDue added in v0.8.0

func (m *Manager) FireDue(ctx context.Context, id string, definition *Definition, now time.Time, opts ...ExecuteOption) ([]string, error)

FireDue advances a persisted instance by firing every transition whose timer has elapsed as of now, returning the names of the transitions that actually fired, in firing order.

It is the per-instance half of the host-driven timer model (M4): a host cron finds due instances with Manager.ListDue and calls FireDue on each, so a fleet-wide "escalate if not approved in 3 days" needs no internal scheduler. The state lives in the database and the clock lives in the host, which makes the whole mechanism restart-safe by construction.

FireDue loads the instance fresh and pins the workflow clock to now, so tokens produced by the firing are stamped with the host's evaluation time and every downstream deadline is measured from it (deterministic and testable with a fixed clock). It then fires due transitions one at a time, re-evaluating the due set after each firing because firing changes the marking; a due transition whose guard rejects it — or that an earlier firing in the same pass has since disabled — is skipped rather than treated as an error, so only an unexpected error aborts.

The save runs under the same optimistic-concurrency retry loop as Execute, so several hosts scanning the same fleet cannot clobber each other. FireDue is idempotent: once nothing is overdue it fires nothing, and after a firing that leaves no running timer the instance drops out of ListDue.

Extra ExecuteOptions (e.g. WithMaxRetries, WithTxSideEffect) are forwarded to the underlying Execute.

func (*Manager) GetWorkflow

func (m *Manager) GetWorkflow(ctx context.Context, id string, definition *Definition) (*Workflow, error)

GetWorkflow gets a workflow instance from the registry or loads it from storage. With registry caching disabled (WithoutRegistryCache) it always loads fresh.

func (*Manager) ListDue added in v0.8.0

func (m *Manager) ListDue(ctx context.Context, before time.Time, limit int) ([]string, error)

ListDue returns the IDs of persisted instances whose next-due time is at or before `before`, ordered by due time ascending — the instances a host cron should advance with FireDue. A zero limit means no limit; drain a batch with FireDue before rescanning, or page by raising `before`.

It requires a DueStorage backend (the SQLite and Postgres backends qualify) and returns an error wrapping errors.ErrUnsupported otherwise. This is the scan half of the host-driven timer model: pass the host's own clock as `before` (typically time.Now) so the whole fleet's deadlines are evaluated against one authoritative clock.

func (*Manager) ListWorkflowIDs added in v0.8.0

func (m *Manager) ListWorkflowIDs(ctx context.Context, opts ListOptions) ([]string, error)

ListWorkflowIDs returns the IDs of persisted workflows, ordered by ID, using the backend's ListableStorage support. It returns an error wrapping errors.ErrUnsupported if the storage backend does not implement ListableStorage.

func (*Manager) ListenerCount added in v0.8.0

func (m *Manager) ListenerCount(eventType EventType) int

ListenerCount returns the number of listeners registered for eventType.

func (*Manager) LoadWorkflow

func (m *Manager) LoadWorkflow(ctx context.Context, id string, definition *Definition) (*Workflow, error)

LoadWorkflow loads a workflow instance from storage. When registry caching is enabled (the default) a cached instance is returned if present; otherwise the instance is loaded fresh from storage and cached.

func (*Manager) RemoveListener added in v0.8.0

func (m *Manager) RemoveListener(handle *ListenerHandle)

RemoveListener removes a listener using its handle This is the recommended way to remove listeners as it's reliable and efficient

func (*Manager) SaveWorkflow

func (m *Manager) SaveWorkflow(ctx context.Context, id string, wf *Workflow) error

SaveWorkflow saves a workflow instance state to storage.

When the backend is a VersionedStorage, the save is guarded by the workflow's current version: if another writer saved first, it returns ErrConflict and the workflow's version is left unchanged so the caller can reload and retry.

type ManagerOption added in v0.8.0

type ManagerOption func(*Manager)

ManagerOption configures a Manager.

func WithDefinitionMigration added in v0.8.0

func WithDefinitionMigration(fn DefinitionMigrationFunc) ManagerOption

WithDefinitionMigration installs a handler consulted when a persisted instance's definition fingerprint differs from the definition supplied to load it. Without it, such a load fails with ErrDefinitionMismatch.

func WithoutRegistryCache added in v0.8.0

func WithoutRegistryCache() ManagerOption

WithoutRegistryCache makes the Manager load every instance fresh from storage instead of serving a cached copy from the registry. Use it in multi-replica deployments, where a process-local cache can serve state that another replica has already advanced; optimistic concurrency (ErrConflict) then protects saves.

type Marking

type Marking interface {

	// Places returns the places that currently hold at least one token.
	Places() []Place
	// SetPlaces resets the marking to a single uncolored token at each place.
	SetPlaces(places []Place)
	// HasPlace reports whether place holds at least one token.
	HasPlace(place Place) bool
	// AddPlace ensures place holds at least one token (adds an uncolored token
	// if it is currently empty).
	AddPlace(place Place) error
	// RemovePlace removes all tokens from place.
	RemovePlace(place Place) error

	// TokensAt returns a copy of the tokens currently in place.
	TokensAt(place Place) []Token
	// TokenCount returns the number of tokens in place.
	TokenCount(place Place) int
	// AllTokens returns a copy of every place's tokens.
	AllTokens() map[Place][]Token
	// AddToken adds a token to place.
	AddToken(place Place, token Token)
	// RemoveToken removes the token with the given ID from place, returning
	// ErrTokenNotFound if it is not there.
	RemoveToken(place Place, id TokenID) error
	// HasToken reports whether a token with the given ID is in place.
	HasToken(place Place, id TokenID) bool
}

Marking represents the current state of a workflow as a Colored Petri Net marking: a mapping from each place to the multiset of tokens it holds.

The engine treats a boolean/elementary net as the trivial case of this model — a place is "present" when it holds at least one token, and a plain workflow simply uses uncolored tokens (no ID, no data). You only ever touch the token methods when you actually need data-carrying tokens; everything else is served by the presence view (Places/HasPlace/AddPlace/RemovePlace).

func NewMarking

func NewMarking(places []Place) Marking

NewMarking creates a marking with a single uncolored token at each of the given places.

func UnmarshalMarkingJSON

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

UnmarshalMarkingJSON unmarshals JSON data into a Marking. It accepts both the compact place-array form and the token-object form.

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 marking and its context data for the given ID.
	LoadState(ctx context.Context, id string) (marking Marking, context map[string]any, err error)

	// SaveState saves the workflow's marking and its context data for the given ID.
	// The full marking is persisted, so data-carrying (colored) tokens round-trip;
	// simple boolean workflows serialize to the compact place-array form.
	//
	// Implementations must persist the ENTIRE context map, so every key set via
	// SetContext survives a save/load round-trip (JSON-encoded values may come
	// back with adjusted types, e.g. numbers as float64). Silently persisting
	// only a subset of keys is a contract violation; the storagetest conformance
	// suite checks this.
	SaveState(ctx context.Context, id string, marking Marking, context map[string]any) error

	// DeleteState removes the workflow state for the given ID.
	DeleteState(ctx context.Context, 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).

Every method takes a context.Context so callers can apply cancellation and deadlines; implementations are expected to honor it (e.g. by using the database/sql *Context methods).

type Token added in v0.8.0

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

Token is a data-carrying marker that occupies a place in a Colored Petri Net.

Tokens are value types: copy them freely. A token's data is only reachable through methods that return copies, and the mutating helpers (With, WithData) return a new token rather than changing the receiver, so a token in a marking cannot be altered by aliasing. Token identity is the ID, not the data — two tokens with identical data are still distinct tokens.

Note: because a Token holds a map, Tokens are not comparable with ==; use Equal.

func NewToken added in v0.8.0

func NewToken(data TokenData) Token

NewToken creates a token carrying a copy of data and a freshly generated, unique ID.

func NewTokenWithID added in v0.8.0

func NewTokenWithID(id TokenID, data TokenData) Token

NewTokenWithID creates a token with an explicit ID. It is useful in tests and when reconstructing tokens loaded from storage.

func (Token) Data added in v0.8.0

func (t Token) Data() TokenData

Data returns a copy of the token's data. Mutating the result does not affect the token.

func (Token) EnteredAt added in v0.8.0

func (t Token) EnteredAt() (time.Time, bool)

EnteredAt returns when the token was produced into its current place, and whether that time is known. It is the zero Time (ok=false) for tokens in workflows without timed transitions, and for state persisted before timer support existed.

func (Token) Equal added in v0.8.0

func (t Token) Equal(other Token) bool

Equal reports whether two tokens have the same ID (token identity).

func (Token) Get added in v0.8.0

func (t Token) Get(key string) (any, bool)

Get returns the value stored under key and whether it was present.

func (Token) ID added in v0.8.0

func (t Token) ID() TokenID

ID returns the token's unique identifier.

func (Token) MarshalJSON added in v0.8.0

func (t Token) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (Token) String added in v0.8.0

func (t Token) String() string

String implements fmt.Stringer.

func (*Token) UnmarshalJSON added in v0.8.0

func (t *Token) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler.

func (Token) Validate added in v0.8.0

func (t Token) Validate() error

Validate reports whether the token is well-formed.

func (Token) With added in v0.8.0

func (t Token) With(key string, value any) Token

With returns a copy of the token with key set to value. The receiver is left unchanged.

func (Token) WithData added in v0.8.0

func (t Token) WithData(data TokenData) Token

WithData returns a copy of the token whose data is replaced by a copy of the given data, keeping the same ID. The receiver is left unchanged.

type TokenAggregate added in v0.8.0

type TokenAggregate struct {
	Count int
	Sum   float64
	Min   float64
	Max   float64
	Avg   float64
}

TokenAggregate is the result of AggregateTokens over a numeric token field. Min, Max, and Avg are meaningful only when Count > 0.

type TokenData added in v0.8.0

type TokenData map[string]any

TokenData is the data a token carries — its "color" in Colored Petri Net terms. It is an arbitrary set of key/value attributes (for example {"order_id": "001", "amount": 100}).

type TokenID added in v0.8.0

type TokenID string

TokenID uniquely identifies a token within a workflow instance.

type TokenPredicate added in v0.8.0

type TokenPredicate func(Token) bool

TokenPredicate reports whether a token should be selected. A nil predicate matches every token.

type TransactionalDueStorage added in v0.8.0

type TransactionalDueStorage interface {
	DueStorage
	TransactionalStorage

	// SaveVersionedStateInTxWithDue behaves like SaveVersionedStateInTx but also
	// records the instance's next-due time (nil clears it), so the state change,
	// the due-index update, and every side effect commit or roll back together.
	SaveVersionedStateInTxWithDue(ctx context.Context, id string, marking Marking, context map[string]any, expectedVersion int64, due *time.Time, effects ...TxSideEffect) (newVersion int64, err error)
}

TransactionalDueStorage composes a due-aware versioned save with atomic side effects — the transactional-path counterpart of DueStorage. SaveVersionedStateWithDue. A backend that is both a TransactionalStorage and a DueStorage MUST implement it so Manager.Execute keeps the due index current even when it commits state together with a history/outbox effect. Manager.Execute requires it: calling Execute with a WithTxSideEffect option against a timed definition on a backend that is a DueStorage but not a TransactionalDueStorage is rejected (errors.ErrUnsupported), because committing state and effect without updating the due column in the same transaction would silently corrupt the index.

type TransactionalStorage added in v0.8.0

type TransactionalStorage interface {
	VersionedStorage

	// SaveVersionedStateInTx behaves like SaveVersionedState but runs the save
	// and every side effect inside a single transaction, committing only if all
	// succeed. Effects run in order after the state write; each receives the
	// backend-specific transaction handle.
	SaveVersionedStateInTx(ctx context.Context, id string, marking Marking, context map[string]any, expectedVersion int64, effects ...TxSideEffect) (newVersion int64, err error)
}

TransactionalStorage is an optional interface a VersionedStorage backend may implement to compose a versioned save with additional writes in one atomic transaction. It is what makes "fire a transition + append its history record" crash-consistent: a process dying between the two can never leave the state and the audit log disagreeing. Manager.Execute uses it when side effects are registered via WithTxSideEffect.

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) (any, 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 any)

SetMetadata sets metadata for the transition

func (*Transition) SetTimeoutAfter added in v0.8.0

func (t *Transition) SetTimeoutAfter(d time.Duration)

SetTimeoutAfter marks the transition as time-driven: it becomes due once its input tokens have waited for d (measured from when they entered their place). The duration is stored on the transition itself as the single source of truth (diagrams and tooling read it back via TimeoutAfter). A non-positive d clears the timeout.

The engine never fires a due transition by itself — a host drives the clock (see Workflow.Due, Workflow.NextDue, and Manager.FireDue).

Like the rest of a Definition, a transition's timeout is expected to be configured before any Workflow is created from the definition; mutating it afterward is not safe against concurrent Workflow.Due/NextDue reads on workflows that share the definition.

func (*Transition) TimeoutAfter added in v0.8.0

func (t *Transition) TimeoutAfter() (time.Duration, bool)

TimeoutAfter returns the transition's timeout duration and whether one is set (a positive timeout). The timeout field is authoritative; there is no metadata mirror.

func (*Transition) To

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

To returns the target places of the transition

type TxSideEffect added in v0.8.0

type TxSideEffect func(ctx context.Context, tx any) error

TxSideEffect is a write executed atomically with a versioned state save — typically an audit/history record or an outbox row. The tx argument is backend-specific: the SQL backends pass a *sql.Tx. An effect that returns an error aborts the whole transaction, so the state change and the effect either both commit or both roll back.

type VersionedStorage added in v0.8.0

type VersionedStorage interface {
	Storage

	// LoadVersionedState loads the workflow's marking, context data, and current
	// version. A brand-new (never saved) workflow has version 0.
	LoadVersionedState(ctx context.Context, id string) (marking Marking, context map[string]any, version int64, err error)

	// SaveVersionedState saves the workflow only if the stored version equals
	// expectedVersion, returning the new (incremented) version on success. Pass
	// expectedVersion 0 to create a new workflow. A mismatch — because another
	// writer saved first, or the row already exists — returns ErrConflict.
	SaveVersionedState(ctx context.Context, id string, marking Marking, context map[string]any, expectedVersion int64) (newVersion int64, err error)
}

VersionedStorage is an optional interface a Storage backend may implement to support optimistic concurrency control. When the Manager is backed by a VersionedStorage it uses these methods so that two writers racing to save the same workflow cannot silently clobber each other: the second save fails with ErrConflict instead.

Each workflow row carries a monotonically increasing version. A caller loads the current version with LoadVersionedState, and passes it back to SaveVersionedState; the save only succeeds if the stored version still matches.

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, opts ...WorkflowOption) (*Workflow, error)

NewWorkflow creates a workflow instance starting at initialPlace.

Every workflow's marking is a Colored Petri Net marking; a plain workflow just uses uncolored tokens (boolean presence). Reach for the token methods (CreateToken, GetTokens, ...) only when you need data-carrying tokens.

func NewWorkflowFromMarking added in v0.8.0

func NewWorkflowFromMarking(name string, definition *Definition, initial Marking, opts ...WorkflowOption) (*Workflow, error)

NewWorkflowFromMarking creates a workflow instance whose starting state is the given marking. Use it when the initial state has multiple places or data-carrying (colored) tokens; NewWorkflow is the single-place shorthand.

The marking is adopted (owned by the workflow), and every place it occupies must be defined in the workflow. When the definition has timed transitions, tokens without an entry time are stamped at construction (so a fresh marking starts its timers); tokens that already carry an entry time keep it, so a persisted marking's running timers are restored rather than reset.

func (*Workflow) AddEventListener

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

AddEventListener adds an event listener for a specific event type It returns a handle that can be used to remove the listener later

func (*Workflow) AddGuardEventListener

func (w *Workflow) AddGuardEventListener(listener GuardEventListener) *ListenerHandle

AddGuardEventListener adds a guard event listener It returns a handle that can be used to remove the listener later

func (*Workflow) AggregateTokens added in v0.8.0

func (w *Workflow) AggregateTokens(pred TokenPredicate, field string) TokenAggregate

AggregateTokens computes count/sum/min/max/avg over the given numeric field of the colored tokens matching pred. Tokens lacking the field, or whose value is not numeric, are ignored. With no matching numeric values the zero aggregate is returned.

func (*Workflow) AllContext added in v0.8.0

func (w *Workflow) AllContext() map[string]any

AllContext returns a copy of all context values

func (*Workflow) AllTokens added in v0.8.0

func (w *Workflow) AllTokens() map[Place][]Token

AllTokens returns a copy of every place's tokens.

func (*Workflow) Apply

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

Apply applies a transition to the workflow

func (*Workflow) ApplyTransition added in v0.8.0

func (w *Workflow) ApplyTransition(transitionName string) error

ApplyTransition applies a specific transition by name

func (*Workflow) ApplyTransitionForToken added in v0.8.0

func (w *Workflow) ApplyTransitionForToken(ctx context.Context, transitionName string, tokenID TokenID) error

ApplyTransitionForToken fires transitionName consuming exactly the token with tokenID from the transition's input place, producing it (data preserved) at the transition's output place(s). It is the Colored Petri Net way to advance one token out of a place that holds several — for example, processing a single order from a batch.

The transition must have a single input place, and tokenID must currently be in it. Guards are validated exactly as for ApplyTransition.

Concurrency: token consumption is atomic. The token's presence is re-checked under the write lock immediately before it is removed, so callers racing to advance the same token cannot double-consume it — the loser gets ErrTokenNotFound and no token is lost or duplicated. Guards, like the whole-marking Apply methods, are evaluated on a snapshot taken before the final lock (event listeners must run unlocked because they may re-enter the workflow); a guard that depends on marking state beyond the token's own presence may therefore observe a slightly stale view under concurrency.

func (*Workflow) ApplyTransitionWithContext added in v0.8.0

func (w *Workflow) ApplyTransitionWithContext(ctx context.Context, transitionName string) error

ApplyTransitionWithContext applies a specific transition by name with a context

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) CanTransition added in v0.8.0

func (w *Workflow) CanTransition(transitionName string) error

CanTransition checks if a specific transition by name is possible

func (*Workflow) CanTransitionWithContext added in v0.8.0

func (w *Workflow) CanTransitionWithContext(ctx context.Context, transitionName string) error

CanTransitionWithContext checks if a specific transition by name is possible with a context

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) ClearPlace added in v0.8.0

func (w *Workflow) ClearPlace(place Place)

ClearPlace removes every token from place — both colored tokens and the uncolored presence token a boolean workflow starts with. It is a no-op if the place is already empty. This is useful when seeding a Colored Petri Net place with an exact set of tokens (so the initial presence token does not linger).

func (*Workflow) Context

func (w *Workflow) Context(key string) (any, bool)

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

func (*Workflow) CountTokens added in v0.8.0

func (w *Workflow) CountTokens(pred TokenPredicate) int

CountTokens returns the total number of colored tokens across all places that match pred (a nil pred counts every colored token).

func (*Workflow) CreateToken added in v0.8.0

func (w *Workflow) CreateToken(place Place, data TokenData) (Token, error)

CreateToken creates a token carrying data at place and returns it. It fails with ErrInvalidPlace if place is not part of the definition.

func (*Workflow) CreateTokens added in v0.8.0

func (w *Workflow) CreateTokens(place Place, datas []TokenData) ([]Token, error)

CreateTokens creates one token per entry in datas at place, returning them in order. It is a batch convenience over CreateToken.

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) Due added in v0.8.0

func (w *Workflow) Due(now time.Time) []Transition

Due returns the transitions whose timeout has elapsed as of now — those a host should fire to honor a deadline (e.g. "escalate if not approved in 3 days"). A transition is included only if it is timed (SetTimeoutAfter) and currently enabled. Guards are not evaluated here; firing still honors them, so a due transition with a blocking guard will be skipped by Manager.FireDue.

now is always explicit so the result is a pure function of the marking and the given time — deterministic and testable without a real clock.

func (*Workflow) EnabledTransitions

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

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

func (*Workflow) FindTokens added in v0.8.0

func (w *Workflow) FindTokens(pred TokenPredicate) map[Place][]Token

FindTokens returns, grouped by place, the colored tokens across the whole marking that match pred (a nil pred matches every colored token). Places with no match are omitted.

func (*Workflow) GetTokens added in v0.8.0

func (w *Workflow) GetTokens(place Place) []Token

GetTokens returns a copy of the tokens currently at place.

func (*Workflow) InitialPlace

func (w *Workflow) InitialPlace() Place

InitialPlace returns the workflow's first initial place. When the workflow started from a marking with multiple initial places it returns the first (sorted); use InitialPlaces for the full set.

func (*Workflow) InitialPlaces added in v0.8.0

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

InitialPlaces returns a copy of the places the workflow's initial marking occupied.

func (*Workflow) ListenerCount added in v0.8.0

func (w *Workflow) ListenerCount(eventType EventType) int

ListenerCount returns the number of listeners registered on this instance for eventType (definition- and manager-level listeners are not counted).

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) NextDue added in v0.8.0

func (w *Workflow) NextDue() (time.Time, bool)

NextDue returns the earliest time at which some timed transition becomes due, and false if no timed transition is currently enabled. The time may be in the past if a deadline has already elapsed. Hosts use it to schedule the next evaluation instead of polling.

func (*Workflow) RemoveListener added in v0.8.0

func (w *Workflow) RemoveListener(handle *ListenerHandle)

RemoveListener removes a listener using its handle This is the recommended way to remove listeners as it's reliable and efficient

func (*Workflow) RemoveToken added in v0.8.0

func (w *Workflow) RemoveToken(place Place, id TokenID) error

RemoveToken removes the token with the given ID from place, returning ErrTokenNotFound if it is not there.

func (*Workflow) SelectTokens added in v0.8.0

func (w *Workflow) SelectTokens(place Place, pred TokenPredicate) []Token

SelectTokens returns the tokens at place that match pred (a nil pred returns all tokens). It is a read-only helper for choosing which token(s) to advance with ApplyTransitionForToken.

func (*Workflow) SetContext

func (w *Workflow) SetContext(key string, value any)

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

func (*Workflow) TokenCount added in v0.8.0

func (w *Workflow) TokenCount(place Place) int

TokenCount returns the number of tokens at place.

func (*Workflow) TransformTokens added in v0.8.0

func (w *Workflow) TransformTokens(place Place, pred TokenPredicate, transform func(Token) TokenData) int

TransformTokens replaces the data of each colored token at place that matches pred (a nil pred matches all) with transform(token), keeping each token's identity. It returns the number of tokens transformed.

func (*Workflow) Version added in v0.8.0

func (w *Workflow) Version() int64

Version returns the workflow's current optimistic-concurrency version. It is 0 until the workflow has been persisted to a VersionedStorage backend.

type WorkflowOption added in v0.8.0

type WorkflowOption func(*Workflow)

WorkflowOption configures a Workflow at construction time.

func WithClock added in v0.8.0

func WithClock(now func() time.Time) WorkflowOption

WithClock sets the clock used to stamp tokens as they enter a place. It only matters for definitions with timed transitions; pass a fixed clock in tests to make token entry times — and therefore the Due API — deterministic. A nil clock is ignored.

Directories

Path Synopsis
examples
cpn_batch_processing command
Command cpn_batch_processing demonstrates Colored Petri Net (CPN) features of the workflow engine: data-carrying tokens, whole-batch and per-token firing, and token queries / aggregation / transformation.
Command cpn_batch_processing demonstrates Colored Petri Net (CPN) features of the workflow engine: data-carrying tokens, whole-batch and per-token firing, and token queries / aggregation / transformation.
cpn_routing command
Command cpn_routing demonstrates guard-based token routing loaded entirely from YAML.
Command cpn_routing demonstrates guard-based token routing loaded entirely from YAML.
simple_flow command
Package storagetest provides a reusable conformance suite for workflow.Storage implementations.
Package storagetest provides a reusable conformance suite for workflow.Storage implementations.

Jump to

Keyboard shortcuts

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