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 ¶
- Variables
- type BaseEvent
- type Constraint
- type Definition
- func (d *Definition) AddEventListener(eventType EventType, listener EventListener) *ListenerHandle
- func (d *Definition) AddGuardEventListener(listener GuardEventListener) *ListenerHandle
- func (d *Definition) AllPlaces() []Place
- func (d *Definition) AllTransitions() []Transition
- func (d *Definition) Fingerprint() string
- func (d *Definition) ListenerCount(eventType EventType) int
- func (d *Definition) Place(place Place) bool
- func (d *Definition) RemoveListener(handle *ListenerHandle)
- func (d *Definition) Transition(name string) *Transition
- type DefinitionMigrationFunc
- type DueStorage
- type Event
- type EventListener
- type EventType
- type ExecuteOption
- type ExpressionConstraint
- type GuardEvent
- type GuardEventListener
- type GuardListener
- type ListOptions
- type ListableStorage
- type Listener
- type ListenerHandle
- type Manager
- func (m *Manager) AddEventListener(eventType EventType, listener EventListener) *ListenerHandle
- func (m *Manager) AddGuardEventListener(listener GuardEventListener) *ListenerHandle
- func (m *Manager) CreateWorkflow(ctx context.Context, id string, definition *Definition, initialPlace Place) (*Workflow, error)
- func (m *Manager) DeleteWorkflow(ctx context.Context, id string) error
- func (m *Manager) EvictWorkflow(id string)
- func (m *Manager) Execute(ctx context.Context, id string, definition *Definition, ...) error
- func (m *Manager) FireDue(ctx context.Context, id string, definition *Definition, now time.Time, ...) ([]string, error)
- func (m *Manager) GetWorkflow(ctx context.Context, id string, definition *Definition) (*Workflow, error)
- func (m *Manager) ListDue(ctx context.Context, before time.Time, limit int) ([]string, error)
- func (m *Manager) ListWorkflowIDs(ctx context.Context, opts ListOptions) ([]string, error)
- func (m *Manager) ListenerCount(eventType EventType) int
- func (m *Manager) LoadWorkflow(ctx context.Context, id string, definition *Definition) (*Workflow, error)
- func (m *Manager) RemoveListener(handle *ListenerHandle)
- func (m *Manager) SaveWorkflow(ctx context.Context, id string, wf *Workflow) error
- type ManagerOption
- type Marking
- type Place
- type Registry
- type Storage
- type Token
- func (t Token) Data() TokenData
- func (t Token) EnteredAt() (time.Time, bool)
- func (t Token) Equal(other Token) bool
- func (t Token) Get(key string) (any, bool)
- func (t Token) ID() TokenID
- func (t Token) MarshalJSON() ([]byte, error)
- func (t Token) String() string
- func (t *Token) UnmarshalJSON(b []byte) error
- func (t Token) Validate() error
- func (t Token) With(key string, value any) Token
- func (t Token) WithData(data TokenData) Token
- type TokenAggregate
- type TokenData
- type TokenID
- type TokenPredicate
- type TransactionalDueStorage
- type TransactionalStorage
- type Transition
- func (t *Transition) AddConstraint(constraint Constraint)
- func (t *Transition) From() []Place
- func (t *Transition) Metadata(key string) (any, bool)
- func (t *Transition) Name() string
- func (t *Transition) SetMetadata(key string, value any)
- func (t *Transition) SetTimeoutAfter(d time.Duration)
- func (t *Transition) TimeoutAfter() (time.Duration, bool)
- func (t *Transition) To() []Place
- type TxSideEffect
- type VersionedStorage
- type Workflow
- func (w *Workflow) AddEventListener(eventType EventType, listener EventListener) *ListenerHandle
- func (w *Workflow) AddGuardEventListener(listener GuardEventListener) *ListenerHandle
- func (w *Workflow) AggregateTokens(pred TokenPredicate, field string) TokenAggregate
- func (w *Workflow) AllContext() map[string]any
- func (w *Workflow) AllTokens() map[Place][]Token
- func (w *Workflow) Apply(targetPlaces []Place) error
- func (w *Workflow) ApplyTransition(transitionName string) error
- func (w *Workflow) ApplyTransitionForToken(ctx context.Context, transitionName string, tokenID TokenID) error
- func (w *Workflow) ApplyTransitionWithContext(ctx context.Context, transitionName string) error
- func (w *Workflow) ApplyWithContext(ctx context.Context, targetPlaces []Place) error
- func (w *Workflow) Can(to []Place) error
- func (w *Workflow) CanTransition(transitionName string) error
- func (w *Workflow) CanTransitionWithContext(ctx context.Context, transitionName string) error
- func (w *Workflow) CanWithContext(ctx context.Context, to []Place) error
- func (w *Workflow) ClearPlace(place Place)
- func (w *Workflow) Context(key string) (any, bool)
- func (w *Workflow) CountTokens(pred TokenPredicate) int
- func (w *Workflow) CreateToken(place Place, data TokenData) (Token, error)
- func (w *Workflow) CreateTokens(place Place, datas []TokenData) ([]Token, error)
- func (w *Workflow) CurrentPlaces() []Place
- func (w *Workflow) Definition() *Definition
- func (w *Workflow) Diagram() string
- func (w *Workflow) Due(now time.Time) []Transition
- func (w *Workflow) EnabledTransitions() ([]Transition, error)
- func (w *Workflow) FindTokens(pred TokenPredicate) map[Place][]Token
- func (w *Workflow) GetTokens(place Place) []Token
- func (w *Workflow) InitialPlace() Place
- func (w *Workflow) InitialPlaces() []Place
- func (w *Workflow) ListenerCount(eventType EventType) int
- func (w *Workflow) Marking() Marking
- func (w *Workflow) Name() string
- func (w *Workflow) NextDue() (time.Time, bool)
- func (w *Workflow) RemoveListener(handle *ListenerHandle)
- func (w *Workflow) RemoveToken(place Place, id TokenID) error
- func (w *Workflow) SelectTokens(place Place, pred TokenPredicate) []Token
- func (w *Workflow) SetContext(key string, value any)
- func (w *Workflow) SetManager(m *Manager)
- func (w *Workflow) SetMarking(marking Marking) error
- func (w *Workflow) TokenCount(place Place) int
- func (w *Workflow) TransformTokens(place Place, pred TokenPredicate, transform func(Token) TokenData) int
- func (w *Workflow) Version() int64
- type WorkflowOption
Constants ¶
This section is empty.
Variables ¶
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) Transition ¶
func (e *BaseEvent) Transition() *Transition
Transition returns the transition associated with the event
type Constraint ¶
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 ¶
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
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 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 ¶
DeleteWorkflow removes a workflow instance and its state
func (*Manager) EvictWorkflow ¶ added in v0.8.0
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
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
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
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 ¶
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 ¶
NewMarking creates a marking with a single uncolored token at each of the given places.
func UnmarshalMarkingJSON ¶
UnmarshalMarkingJSON unmarshals JSON data into a Marking. It accepts both the compact place-array form and the token-object form.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry manages multiple workflows
func (*Registry) AddWorkflow ¶
AddWorkflow adds a workflow to the registry
func (*Registry) HasWorkflow ¶
HasWorkflow checks if a workflow exists
func (*Registry) ListWorkflows ¶
ListWorkflows returns a list of all workflow names
func (*Registry) RemoveWorkflow ¶
RemoveWorkflow removes a workflow from the registry
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
NewToken creates a token carrying a copy of data and a freshly generated, unique ID.
func NewTokenWithID ¶ added in v0.8.0
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
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
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
Equal reports whether two tokens have the same ID (token identity).
func (Token) Get ¶ added in v0.8.0
Get returns the value stored under key and whether it was present.
func (Token) MarshalJSON ¶ added in v0.8.0
MarshalJSON implements json.Marshaler.
func (*Token) UnmarshalJSON ¶ added in v0.8.0
UnmarshalJSON implements json.Unmarshaler.
type TokenAggregate ¶ added in v0.8.0
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
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
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) 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
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
AllContext returns a copy of all context values
func (*Workflow) ApplyTransition ¶ added in v0.8.0
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
ApplyTransitionWithContext applies a specific transition by name with a context
func (*Workflow) ApplyWithContext ¶
ApplyWithContext applies a transition to the workflow with a context
func (*Workflow) CanTransition ¶ added in v0.8.0
CanTransition checks if a specific transition by name is possible
func (*Workflow) CanTransitionWithContext ¶ added in v0.8.0
CanTransitionWithContext checks if a specific transition by name is possible with a context
func (*Workflow) CanWithContext ¶
CanWithContext checks if transition to target places is possible with a context
func (*Workflow) ClearPlace ¶ added in v0.8.0
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) 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
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
CreateTokens creates one token per entry in datas at place, returning them in order. It is a batch convenience over CreateToken.
func (*Workflow) CurrentPlaces ¶
CurrentPlaces returns the current places of the workflow
func (*Workflow) Definition ¶
func (w *Workflow) Definition() *Definition
Definition returns the workflow definition
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
GetTokens returns a copy of the tokens currently at place.
func (*Workflow) InitialPlace ¶
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
InitialPlaces returns a copy of the places the workflow's initial marking occupied.
func (*Workflow) ListenerCount ¶ added in v0.8.0
ListenerCount returns the number of listeners registered on this instance for eventType (definition- and manager-level listeners are not counted).
func (*Workflow) NextDue ¶ added in v0.8.0
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
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 ¶
SetContext sets a value in the workflow context
func (*Workflow) SetManager ¶
SetManager sets the manager pointer for this workflow
func (*Workflow) SetMarking ¶
SetMarking sets the workflow marking
func (*Workflow) TokenCount ¶ added in v0.8.0
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.
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.
Source Files
¶
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. |
|
document_approval
command
|
|
|
order_processing
command
|
|
|
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. |