Documentation
¶
Overview ¶
Package automa provides primitives for composing and executing automated workflows using the Saga pattern.
Core concepts ¶
A Step is the smallest unit of work. Every step has a unique ID and three lifecycle phases:
- Prepare — sets up context and state before execution begins.
- Execute — performs the actual work and returns a Report.
- Rollback — undoes the work when a subsequent step fails.
A Workflow is itself a Step that owns an ordered list of child Steps. The workflow drives the execution loop, applying the configured TypeMode (StopOnError, ContinueOnError, RollbackOnError) and collecting per-step Report values into a final workflow report.
State is carried through a workflow via NamespacedStateBag, which isolates each step's private data (Local) from data that is intentionally shared across all steps (Global) while also supporting arbitrary named partitions (WithNamespace).
Building workflows ¶
Use NewWorkflowBuilder and NewStepBuilder to construct workflows and steps declaratively:
wf, err := automa.NewWorkflowBuilder().
WithId("deploy").
WithExecutionMode(automa.RollbackOnError).
Steps(
automa.NewStepBuilder().
WithId("provision").
WithExecute(func(ctx context.Context, stp automa.Step) *automa.Report {
// ... provision resources ...
return automa.SuccessReport(stp)
}).
WithRollback(func(ctx context.Context, stp automa.Step) *automa.Report {
// ... tear down resources ...
return automa.SuccessReport(stp)
}),
).
Build()
if err != nil {
log.Fatal(err)
}
report := wf.Execute(ctx)
Registry ¶
A Registry stores named Builder instances so that workflows can look up steps by ID without hard-coding constructor calls.
Index ¶
- Variables
- func BoolFromState(state StateBag, key Key) (bool, bool)
- func BoolMapFromState(state StateBag, key Key) map[string]bool
- func BoolSliceFromState(state StateBag, key Key) []bool
- func ContextWithState(ctx context.Context, stateBag StateBag) context.Context
- func FloatFromState(state StateBag, key Key) (float64, bool)
- func FloatMapFromState(state StateBag, key Key) map[string]float64
- func FloatSliceFromState(state StateBag, key Key) []float64
- func FromState[T any](state StateBag, key Key, zero T) (T, bool)
- func IntFromState(state StateBag, key Key) (int, bool)
- func IntMapFromState(state StateBag, key Key) map[string]int
- func IntSliceFromState(state StateBag, key Key) []int
- func IsEqualMap(a, b StringMap) bool
- func IsNil[T any](v T) bool
- func IsWorkflow(stp Step) bool
- func MapFromState[K comparable, V any](state StateBag, key Key) map[K]V
- func NormalizeValue(v interface{}) (interface{}, error)
- func SliceFromState[T any](state StateBag, key Key) []T
- func StateBagToStringMap(sb StateBag) map[string]interface{}
- func StringFromState(state StateBag, key Key) (string, bool)
- func StringSliceFromState(state StateBag, key Key) []string
- func ToBool(v interface{}) (bool, bool)
- func ToFloat64(v interface{}) (float64, bool)
- func ToInt64(v interface{}) (int64, bool)
- func ToStringSlice(v interface{}) ([]string, bool)
- type Builder
- type Cloner
- type EffectiveFunc
- type EffectiveStrategy
- type EffectiveValue
- type ExecuteFunc
- type Key
- type NamespacedStateBag
- type OnCompletionFunc
- type OnFailureFunc
- type PrepareFunc
- type Registry
- type Report
- func FailureReport(s Step, opts ...ReportOption) *Report
- func NewReport(id string, opts ...ReportOption) *Report
- func RunWorkflow(ctx context.Context, wb *WorkflowBuilder) *Report
- func SkippedReport(s Step, opts ...ReportOption) *Report
- func StepFailureReport(id string, opts ...ReportOption) *Report
- func StepSkippedReport(id string, opts ...ReportOption) *Report
- func StepSuccessReport(id string, opts ...ReportOption) *Report
- func SuccessReport(s Step, opts ...ReportOption) *Report
- func (r *Report) Clone() *Report
- func (r *Report) Duration() time.Duration
- func (r *Report) HasError() bool
- func (r *Report) IsFailed() bool
- func (r *Report) IsSkipped() bool
- func (r *Report) IsSuccess() bool
- func (r *Report) MarshalJSON() ([]byte, error)
- func (r *Report) MarshalYAML() (interface{}, error)
- type ReportOption
- func WithActionType(actionType TypeAction) ReportOption
- func WithDetail(detail string) ReportOption
- func WithEndTime(endTime time.Time) ReportOption
- func WithError(err error) ReportOption
- func WithExecutionMode(mode TypeMode) ReportOption
- func WithIsWorkflow(isWorkflow bool) ReportOption
- func WithMetadata(metadata StringMap) ReportOption
- func WithReport(report *Report) ReportOption
- func WithRollbackMode(mode TypeMode) ReportOption
- func WithRollbackReport(rollback *Report) ReportOption
- func WithStartTime(startTime time.Time) ReportOption
- func WithStatus(status TypeStatus) ReportOption
- func WithStepReports(reports ...*Report) ReportOption
- func WithWorkflow(w *workflow) ReportOption
- type RollbackFunc
- type RuntimeValue
- func (v *RuntimeValue[T]) ClearCache()
- func (v *RuntimeValue[T]) Clone() (*RuntimeValue[T], error)
- func (v *RuntimeValue[T]) Default() Value[T]
- func (v *RuntimeValue[T]) Effective() (*EffectiveValue[T], error)
- func (v *RuntimeValue[T]) EffectiveWithContext(ctx context.Context) (*EffectiveValue[T], error)
- func (v *RuntimeValue[T]) SetEffectiveFunc(f EffectiveFunc[T])
- func (v *RuntimeValue[T]) SetUserInput(user Value[T])
- func (v *RuntimeValue[T]) UserInput() Value[T]
- func (v *RuntimeValue[T]) WithContext(ctx context.Context) *RuntimeValue[T]
- type StateBag
- type Step
- type StepBuilder
- func (s *StepBuilder) Build() (Step, error)
- func (s *StepBuilder) BuildAndCopy() (Step, error)
- func (s *StepBuilder) Id() string
- func (s *StepBuilder) Validate() error
- func (s *StepBuilder) WithAsyncCallbacks(enable bool) *StepBuilder
- func (s *StepBuilder) WithExecute(f ExecuteFunc) *StepBuilder
- func (s *StepBuilder) WithId(id string) *StepBuilder
- func (s *StepBuilder) WithLogger(logger *slog.Logger) *StepBuilder
- func (s *StepBuilder) WithOnCompletion(f OnCompletionFunc) *StepBuilder
- func (s *StepBuilder) WithOnFailure(f OnFailureFunc) *StepBuilder
- func (s *StepBuilder) WithPrepare(f PrepareFunc) *StepBuilder
- func (s *StepBuilder) WithRollback(f RollbackFunc) *StepBuilder
- func (s *StepBuilder) WithState(state NamespacedStateBag) *StepBuilder
- type StringMap
- func (s StringMap) Clone() (StringMap, error)
- func (s StringMap) Delete(key string)
- func (s StringMap) Get(key string) (string, bool)
- func (s StringMap) IsEqual(other StringMap) bool
- func (s StringMap) Items() map[string]string
- func (s StringMap) Keys() []string
- func (s StringMap) Merge(other StringMap)
- func (s StringMap) Set(key, value string)
- func (s StringMap) Values() []string
- type SyncNamespacedStateBag
- func (n *SyncNamespacedStateBag) Clone() (NamespacedStateBag, error)
- func (n *SyncNamespacedStateBag) Global() StateBag
- func (n *SyncNamespacedStateBag) Local() StateBag
- func (n *SyncNamespacedStateBag) MarshalJSON() ([]byte, error)
- func (n *SyncNamespacedStateBag) MarshalYAML() (interface{}, error)
- func (n *SyncNamespacedStateBag) Merge(other NamespacedStateBag) (NamespacedStateBag, error)
- func (n *SyncNamespacedStateBag) UnmarshalJSON(data []byte) error
- func (n *SyncNamespacedStateBag) UnmarshalYAML(node *yaml.Node) error
- func (n *SyncNamespacedStateBag) WithNamespace(name string) StateBag
- type SyncStateBag
- func (s *SyncStateBag) Bool(key Key) (bool, bool)
- func (s *SyncStateBag) Clear() StateBag
- func (s *SyncStateBag) Clone() (StateBag, error)
- func (s *SyncStateBag) Delete(key Key) StateBag
- func (s *SyncStateBag) Float(key Key) (float64, bool)
- func (s *SyncStateBag) Float32(key Key) (float32, bool)
- func (s *SyncStateBag) Float64(key Key) (float64, bool)
- func (s *SyncStateBag) Get(key Key) (interface{}, bool)
- func (s *SyncStateBag) Int(key Key) (int, bool)
- func (s *SyncStateBag) Int8(key Key) (int8, bool)
- func (s *SyncStateBag) Int16(key Key) (int16, bool)
- func (s *SyncStateBag) Int32(key Key) (int32, bool)
- func (s *SyncStateBag) Int64(key Key) (int64, bool)
- func (s *SyncStateBag) Items() map[Key]interface{}
- func (s *SyncStateBag) Keys() []Key
- func (s *SyncStateBag) MarshalJSON() ([]byte, error)
- func (s *SyncStateBag) MarshalYAML() (interface{}, error)
- func (s *SyncStateBag) Merge(other StateBag) (StateBag, error)
- func (s *SyncStateBag) Set(key Key, value interface{}) StateBag
- func (s *SyncStateBag) Size() int
- func (s *SyncStateBag) String(key Key) (string, bool)
- func (s *SyncStateBag) UnmarshalJSON(data []byte) error
- func (s *SyncStateBag) UnmarshalYAML(node *yaml.Node) error
- type TypeAction
- type TypeMode
- type TypeStatus
- type Value
- type ValueOption
- type Workflow
- type WorkflowBuilder
- func (wb *WorkflowBuilder) Build() (Step, error)
- func (wb *WorkflowBuilder) Id() string
- func (wb *WorkflowBuilder) NamedSteps(stepIds ...string) *WorkflowBuilder
- func (wb *WorkflowBuilder) Steps(steps ...Builder) *WorkflowBuilder
- func (wb *WorkflowBuilder) Validate() error
- func (wb *WorkflowBuilder) WithAsyncCallbacks(enable bool) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithExecutionMode(mode TypeMode) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithId(id string) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithLogger(logger *slog.Logger) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithOnCompletion(f OnCompletionFunc) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithOnFailure(f OnFailureFunc) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithPrepare(prepareFunc PrepareFunc) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithRegistry(sr Registry) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithRollback(rollback RollbackFunc) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithRollbackMode(mode TypeMode) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithState(state NamespacedStateBag) *WorkflowBuilder
- func (wb *WorkflowBuilder) WithStatePreservation(enable bool) *WorkflowBuilder
Constants ¶
This section is empty.
Variables ¶
var ( ErrNamespace = errorx.NewNamespace("automa") StepIdProperty = errorx.RegisterProperty("step_id") IllegalArgument = ErrNamespace.NewType("illegal_argument") StepNotFound = IllegalArgument.NewSubtype("step_not_found", errorx.NotFound()) StepAlreadyExists = IllegalArgument.NewSubtype("step_already_exists", errorx.Duplicate()) StepExecutionError = ErrNamespace.NewType("step_execution_error") )
Functions ¶
func BoolFromState ¶ added in v0.5.0
BoolFromState is a convenience wrapper around FromState[bool]. It returns the bool value stored under key in state, and whether the lookup and coercion succeeded. Returns (false, false) when the key is absent or the value cannot be coerced to bool.
func BoolMapFromState ¶ added in v0.5.0
BoolMapFromState is a convenience wrapper around MapFromState[string,bool]. Returns an empty map[string]bool when the key is absent or the value is not a map[string]bool.
func BoolSliceFromState ¶ added in v0.5.0
BoolSliceFromState is a convenience wrapper around SliceFromState[bool]. Returns an empty []bool when the key is absent or the value is not []bool.
func ContextWithState ¶ added in v0.5.0
ContextWithState returns a new context derived from ctx with the given StateBag stored under KeyState. The bag can be retrieved later via StateFromContext.
func FloatFromState ¶ added in v0.5.0
FloatFromState is a convenience wrapper around FromState[float64]. It returns the float64 value stored under key in state, and whether the lookup and coercion succeeded. Returns (0.0, false) when the key is absent or the value cannot be coerced to float64.
func FloatMapFromState ¶ added in v0.5.0
FloatMapFromState is a convenience wrapper around MapFromState[string,float64]. Returns an empty map[string]float64 when the key is absent or the value is not a map[string]float64.
func FloatSliceFromState ¶ added in v0.5.0
FloatSliceFromState is a convenience wrapper around SliceFromState[float64]. Returns an empty []float64 when the key is absent or the value is not []float64.
func FromState ¶ added in v0.5.0
FromState retrieves a value of type T from state under key, reporting whether the lookup and coercion succeeded.
Lookup and coercion pipeline (executed in order until one succeeds):
- Key absence: returns (zero, false) immediately.
- Exact type match: if the raw stored value is already a T, it is returned without any conversion.
- Normalization: the raw value is passed through normalizeFromState to unwrap pointers, decode yaml.Node, convert json.Number, etc.
- Exact type match on normalized value.
- Type coercion: for primitive target types (string, bool, int*, uint*, float*), the appropriate conversion helper is applied.
Return values:
- (value, true) — key exists and value was successfully coerced to T.
- (zero, false) — key absent, normalization failed, or coercion to T is not possible.
The two-value return eliminates the ambiguity of a single-value form: without the bool, a caller cannot distinguish "key missing" from "stored value is the zero value of T". Use state.Get directly when you need the raw, uncoerced value.
After a JSON round-trip, numbers are stored as float64 by encoding/json. FromState handles this transparently: requesting Int for a key whose value is float64(5) will return (5, true).
The StateBag convenience methods (String, Int, Bool, …) delegate to FromState with the appropriate zero value.
func IntFromState ¶ added in v0.5.0
IntFromState is a convenience wrapper around FromState[int]. It returns the int value stored under key in state, and whether the lookup and coercion succeeded. Returns (0, false) when the key is absent or the value cannot be coerced to int.
func IntMapFromState ¶ added in v0.5.0
IntMapFromState is a convenience wrapper around MapFromState[string,int]. Returns an empty map[string]int when the key is absent or the value is not a map[string]int.
func IntSliceFromState ¶ added in v0.5.0
IntSliceFromState is a convenience wrapper around SliceFromState[int]. Returns an empty []int when the key is absent or the value is not []int.
func IsEqualMap ¶ added in v0.10.0
IsEqualMap compares two automa.StateBag values using Items()
func IsNil ¶ added in v0.8.3
IsNil reports whether the provided generic value is nil for kinds that can be nil.
It returns true for values that are nil, and also for untyped nil values (for example `var x interface{} = nil`) which are represented internally as an invalid `reflect.Value`. For kinds that cannot be nil this function returns false and will not panic.
func IsWorkflow ¶ added in v0.5.0
func MapFromState ¶ added in v0.5.0
func MapFromState[K comparable, V any](state StateBag, key Key) map[K]V
MapFromState retrieves a map[K]V value stored under key. It performs an exact type assertion only — no key or value coercion is attempted. Returns an empty map[K]V when the key is absent or the stored value is not a map[K]V.
func NormalizeValue ¶ added in v0.10.0
func NormalizeValue(v interface{}) (interface{}, error)
NormalizeValue is the exported form of normalizeFromState. It canonicalizes common shapes produced by JSON/YAML decoding into stable Go types:
- Dereferences pointer chains.
- Decodes *yaml.Node / yaml.Node into native Go values.
- Converts json.Number to int64, uint64, float64, or string.
- Converts map[interface{}]interface{} (YAML) to map[string]interface{}.
- Recursively normalizes slices and maps.
This is useful before calling FromState or performing manual type assertions on values decoded from JSON or YAML.
Returns (nil, error) only on a yaml.Node decode error or an unstringable map key. All other inputs either return a normalized value or (nil, nil) for nil inputs.
func SliceFromState ¶ added in v0.5.0
SliceFromState retrieves a []T value stored under key. It performs an exact type assertion only — no element-by-element coercion is attempted. Returns an empty []T when the key is absent or the stored value is not a []T.
func StateBagToStringMap ¶ added in v0.10.0
StateBagToStringMap returns a shallow snapshot of the bag's contents as a map[string]interface{} with string keys, suitable for JSON or YAML marshaling. Keys are converted from Key to string. Returns an empty map when sb is nil.
func StringFromState ¶ added in v0.5.0
StringFromState is a convenience wrapper around FromState[string]. It returns the string value stored under key in state, and whether the lookup and coercion succeeded. Returns ("", false) when the key is absent or the value cannot be coerced to string.
func StringSliceFromState ¶ added in v0.5.0
StringSliceFromState is a convenience wrapper around SliceFromState[string]. Returns an empty []string when the key is absent or the value is not []string.
func ToBool ¶ added in v0.10.0
ToBool is the exported form of toBool. It coerces v to bool using boolean strings ("true"/"false") and numeric-to-bool conversions (non-zero → true). Returns (false, false) for unsupported types or unparseable inputs.
func ToFloat64 ¶ added in v0.10.0
ToFloat64 is the exported form of toFloat64. It attempts to coerce v to float64. All numeric types and numeric strings are supported. Returns (0, false) for unsupported types or unparseable strings.
func ToInt64 ¶ added in v0.10.0
ToInt64 is the exported form of toInt64. It attempts to coerce v to int64 with range and overflow checking. Float values are truncated toward zero. Returns (0, false) for unsupported types, unparseable strings, or out-of-range values.
See toInt64 for the full set of supported input types.
func ToStringSlice ¶ added in v0.10.0
ToStringSlice converts common slice shapes into a []string.
Supported shapes:
- []string: returned as-is.
- []interface{}: each element is formatted with fmt.Sprint.
- Any other slice (detected via reflection): each element is formatted with fmt.Sprint.
Returns (nil, false) for nil input or non-slice types.
Types ¶
type Builder ¶ added in v0.4.0
type Builder interface {
// Id returns the unique identifier of the step this Builder produces.
// The ID is used as the registry key and must be non-empty.
Id() string
// Validate checks the builder's configuration for consistency and
// completeness. It is called by Build and also by the workflow builder
// during assembly. Returns a descriptive error when the configuration is
// invalid (e.g. missing ID or execute function).
Validate() error
// Build constructs and returns the configured Step. Build may be called
// more than once; each call should return a new, independent Step instance.
// Returns an error if Validate fails or construction otherwise fails.
Build() (Step, error)
}
Builder constructs a Step from a declarative specification.
Builders are stored in a Registry so that workflows can look up step factories by ID. Use NewStepBuilder or NewWorkflowBuilder to obtain a Builder; implement this interface directly only when building a custom step factory.
type Cloner ¶ added in v0.8.0
Cloner marks values that can return a cloned copy of themselves. Clone returns a typed value of type T and an error; the caller decides what to store.
type EffectiveFunc ¶ added in v0.8.0
type EffectiveFunc[T any] func(ctx context.Context, defaultVal Value[T], userInput Value[T]) (*EffectiveValue[T], bool, error)
EffectiveFunc is a function type used to compute the effective Value[T] based on the provided defaultVal and userInput.
The function should return the computed *EffectiveValue[T], a boolean indicating whether the result should be cached for future calls, and an error if the computation fails.
Semantics:
- If error != nil the call fails.
- If returned *EffectiveValue[T] is nil, the call fails (treated as an implementation error).
- If shouldCache == true the first successful non-nil result will be cached in the RuntimeValue instance and returned to subsequent callers.
- If shouldCache == false the computed result will be returned to the caller but not cached; subsequent calls will re-evaluate.
The function is expected to be side-effect-free if caching is enabled. It accepts a context for cancellation and timeout control.
type EffectiveStrategy ¶ added in v0.8.2
type EffectiveStrategy uint8
EffectiveStrategy describes how an effective value was determined.
It is used to indicate whether a value came from the system default, from explicit user input, or from custom resolution logic.
const ( // StrategyDefault indicates that the effective value was determined by // the system's default value. StrategyDefault EffectiveStrategy = 0 // StrategyUserInput indicates that the effective value was provided // explicitly by user input. StrategyUserInput EffectiveStrategy = 1 // StrategyCustom indicates that the effective value was determined by // custom logic StrategyCustom EffectiveStrategy = 2 // StrategyCurrent indicates that the effective value was determined by // current state StrategyCurrent EffectiveStrategy = 3 // StrategyConfig indicates that the effective value was determined by // configuration StrategyConfig EffectiveStrategy = 4 )
func (EffectiveStrategy) MarshalJSON ¶ added in v0.8.2
func (es EffectiveStrategy) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler for EffectiveStrategy.
It encodes the strategy as a quoted string using the same values as String().
func (EffectiveStrategy) MarshalYAML ¶ added in v0.8.3
func (es EffectiveStrategy) MarshalYAML() (interface{}, error)
MarshalYAML encodes the EffectiveStrategy as its string representation.
func (EffectiveStrategy) String ¶ added in v0.8.2
func (es EffectiveStrategy) String() string
String returns the textual representation of the EffectiveStrategy.
Known values are "default", "userInput" and "custom". Unknown values return "unknown".
func (*EffectiveStrategy) UnmarshalJSON ¶ added in v0.8.2
func (es *EffectiveStrategy) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler for EffectiveStrategy.
It accepts the quoted string representations "default", "userInput" and "custom". Unknown values are mapped to StrategyDefault for backward compatibility.
func (*EffectiveStrategy) UnmarshalYAML ¶ added in v0.8.3
func (es *EffectiveStrategy) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML decodes the EffectiveStrategy from a YAML node containing the string. Unknown values are mapped to StrategyDefault for compatibility.
type EffectiveValue ¶ added in v0.8.2
type EffectiveValue[T any] struct { // contains filtered or unexported fields }
EffectiveValue wraps a Value[T] together with information about how that value was chosen (the EffectiveStrategy).
The wrapper provides cloning and accessors to retrieve the underlying value and the associated strategy.
func NewEffective ¶ added in v0.8.3
func NewEffective[T any](v T, strategy EffectiveStrategy) (*EffectiveValue[T], error)
NewEffective constructs a new EffectiveValue that pairs the provided Value with the given EffectiveStrategy. An error is returned if the provided Value is nil.
func NewEffectiveValue ¶ added in v0.8.2
func NewEffectiveValue[T any](v Value[T], strategy EffectiveStrategy) (*EffectiveValue[T], error)
NewEffectiveValue constructs a new EffectiveValue that pairs the provided Value with the given EffectiveStrategy. An error is returned if the provided Value is nil.
func (EffectiveValue[T]) Clone ¶ added in v0.8.2
func (ev EffectiveValue[T]) Clone() (*EffectiveValue[T], error)
Clone produces a deep clone of the EffectiveValue by cloning the underlying Value using its Clone method.
If the underlying Value's Clone fails, the error is returned.
func (EffectiveValue[T]) Get ¶ added in v0.8.2
func (ev EffectiveValue[T]) Get() Value[T]
Get returns the underlying Value[T] wrapped by this EffectiveValue.
The returned Value should be treated according to its own concurrency and cloning semantics.
func (EffectiveValue[T]) Strategy ¶ added in v0.8.2
func (ev EffectiveValue[T]) Strategy() EffectiveStrategy
Strategy returns the EffectiveStrategy indicating how the underlying value was selected.
type ExecuteFunc ¶ added in v0.5.0
ExecuteFunc is the signature for a step's primary work function. It receives the execution context and the owning Step (for access to its ID, state, and logger), and must return a non-nil Report. Returning nil causes [defaultStep.Execute] to synthesise a Failure report automatically.
type Key ¶ added in v0.5.0
type Key string
Key is a typed string used as a map key inside a StateBag. Using a named type instead of a plain string prevents accidental key collisions between packages and enables the compiler to catch misuse at build time.
const ( // KeyState is the context key under which a StateBag is stored and // retrieved via ContextWithState / StateFromContext. KeyState Key = "automa_state_bag" // KeyStep is the context key under which the currently-executing Step is // stored for downstream access. KeyStep Key = "automa_step" // KeyId is the context key under which the identifier of the current // workflow or step is stored. KeyId Key = "automa_id" // KeyIsWorkflow is the context key whose presence (and boolean value) // indicates that the current execution context belongs to a Workflow rather // than an individual Step. KeyIsWorkflow Key = "automa_is_workflow" // KeyStartTime is the context key under which the wall-clock start time of // an execution is stored. KeyStartTime Key = "automa_start_time" // KeyEndTime is the context key under which the wall-clock end time of an // execution is stored. KeyEndTime Key = "automa_end_time" // KeyReport is the context key under which the Report produced by the most // recently completed step or workflow is stored. KeyReport Key = "automa_report" )
type NamespacedStateBag ¶ added in v0.9.0
type NamespacedStateBag interface {
// Local returns the StateBag for the local namespace.
//
// This bag is private to the step that owns this NamespacedStateBag.
// No other step in the workflow can read or write it.
Local() StateBag
// Global returns the StateBag for the global namespace.
//
// This bag is shared across all steps in the workflow. Writes by one step
// are immediately visible to subsequent steps that call Global().
Global() StateBag
// WithNamespace returns the StateBag for the named custom namespace,
// creating it on first access. The returned bag is stable: repeated calls
// with the same name return the same underlying bag.
//
// Custom namespaces are isolated from Local() and Global() and from each
// other. They are useful when a single step implementation is instantiated
// multiple times in one workflow.
WithNamespace(name string) StateBag
// Clone returns a fully independent deep copy of this NamespacedStateBag,
// including the local bag, the global bag, and all custom namespace bags.
// Mutations to the clone do not affect the original, and vice versa.
Clone() (NamespacedStateBag, error)
// Merge copies every namespace from other into this bag, overwriting
// conflicting keys. Each namespace kind is merged independently:
// - Local bags are merged (other's keys overwrite matching keys here).
// - Global bags are merged.
// - Custom namespaces are merged by name: matching namespaces are merged,
// new namespaces in other are deep-cloned and added.
//
// Returns the receiver and any error. Passing nil is a no-op.
Merge(other NamespacedStateBag) (NamespacedStateBag, error)
}
NamespacedStateBag provides namespace-aware state management for workflow steps.
Why namespacing? ¶
When multiple steps in a workflow share a single flat StateBag, they can accidentally overwrite each other's data by using the same key. For example, two "setup-bind-mount" steps both writing "bind-mount-source" to the same bag will have the second step silently overwrite the first step's value.
NamespacedStateBag solves this by providing three distinct partitions:
- Local — private to one step. The workflow gives each step its own local bag, so writes are never visible to any other step.
- Global — shared across all steps in the workflow. Use this for configuration or counters that steps need to publish and consume.
- Custom — an arbitrary named partition (e.g. "database-primary"). Use this when a reusable step implementation runs multiple times in the same workflow and each instance needs a collision-free namespace.
Usage ¶
// Step-private write (not visible to other steps)
stp.State().Local().Set("bind-mount", bindMount)
// Read from shared global state set by a previous step
env, ok := stp.State().Global().String("env")
// Named partition for a reusable step
stp.State().WithNamespace("database-primary").Set("conn", conn)
// Read back in Rollback (same namespace, same key)
val, ok := stp.State().WithNamespace("database-primary").Get("conn")
Workflow integration ¶
Before each step's Prepare call, the workflow injects a NamespacedStateBag whose Global() pointer is shared across all steps and whose Local() is a fresh empty bag. Sub-workflows receive a deep clone of the parent's global state so that their mutations do not propagate back to the parent.
The primary implementation is SyncNamespacedStateBag.
type OnCompletionFunc ¶ added in v0.4.0
OnCompletionFunc is an optional callback invoked after a successful or skipped Execute. It receives the execution context, the owning Step, and the final Report. When StepBuilder.WithAsyncCallbacks is enabled the callback is called in a separate goroutine with a deep-cloned report.
type OnFailureFunc ¶ added in v0.5.0
OnFailureFunc is an optional callback invoked after a failed Execute. It receives the execution context, the owning Step, and the failure Report. When StepBuilder.WithAsyncCallbacks is enabled the callback is called in a separate goroutine with a deep-cloned report.
type PrepareFunc ¶ added in v0.5.0
PrepareFunc is the signature for a step's preparation function. It is called before Execute and may enrich ctx (e.g. inject a request-scoped logger or deadline), validate preconditions, or pre-populate the step's local state. The returned context is forwarded to Execute and Rollback. A non-nil error aborts the step and is treated as a failure by the workflow.
type Registry ¶ added in v0.3.0
type Registry interface {
// Add registers one or more Builders. Returns an error if any Builder's ID
// is already present; in that case no builders from the batch are added.
Add(steps ...Builder) error
// Remove deletes the Builder with the given ID. Returns true if the ID
// existed and was removed, false if it was not found.
Remove(id string) bool
// Has reports whether a Builder with the given ID is registered.
Has(id string) bool
// Of returns the Builder registered under id, or nil if not found.
Of(id string) Builder
}
Registry is a thread-safe store of named Builder instances.
It allows workflows to look up step factories by ID rather than by direct constructor reference, which is useful for dynamic or configuration-driven workflow assembly.
Use NewRegistry to obtain an instance.
func NewRegistry ¶ added in v0.3.0
func NewRegistry() Registry
NewRegistry returns a new, empty Registry backed by an in-memory map. The returned Registry is safe for concurrent use.
type Report ¶ added in v0.4.0
type Report struct {
// Id is the step or workflow ID that produced this report.
Id string
// IsWorkflow is true when this report was produced by a Workflow rather
// than a plain Step.
IsWorkflow bool
// Action identifies which lifecycle phase produced this report:
// ActionPrepare, ActionExecute, or ActionRollback.
Action TypeAction
// Status is the outcome of the phase: StatusSuccess, StatusFailed, or
// StatusSkipped.
Status TypeStatus
// StartTime is the wall-clock time at which the phase began.
StartTime time.Time
// EndTime is the wall-clock time at which the phase completed.
EndTime time.Time
// Detail is an optional human-readable message providing additional context
// about the outcome (e.g. why a step was skipped).
Detail string
// Error holds the error returned by the step if the phase failed. It is
// nil for successful and skipped reports. During JSON/YAML marshaling this
// field is serialized as the string produced by Error().
Error error
// Metadata is an optional map of string key-value pairs that a step can
// populate with structured diagnostic information (e.g. resource IDs,
// configuration values, counts). It is serialized as a JSON/YAML object.
Metadata StringMap
// StepReports contains the per-step reports for a workflow report, in
// execution order. It is nil for plain step reports.
StepReports []*Report
// Rollback holds the rollback sub-report tree when rollback was triggered.
// For a workflow report this contains the rollback outcomes for all steps
// that were rolled back. It is nil when no rollback occurred.
Rollback *Report
// ExecutionMode records the workflow's execution mode at the time the
// report was produced. This provides context when replaying or auditing
// a workflow run.
ExecutionMode TypeMode
// RollbackMode records the workflow's rollback mode at the time the
// report was produced.
RollbackMode TypeMode
}
Report describes the outcome of a single step or an entire workflow execution. It is the primary observability primitive in automa: every Execute, Rollback, and workflow run produces a Report that the caller can inspect, log, or persist.
Structure ¶
A step report contains a flat set of fields describing that step's outcome. A workflow report nests the per-step reports inside StepReports, and if rollback was triggered, the rollback outcomes are nested inside the Rollback field of the report for the step that failed.
workflow report ├── StepReports[0] — step "provision" (Execute) ├── StepReports[1] — step "configure" (Execute, failed) │ └── Rollback │ ├── StepReports[0] — step "configure" (Rollback) │ └── StepReports[1] — step "provision" (Rollback) └── …
Serialization ¶
Report implements json.Marshaler and yaml.Marshaler. The error field is serialized as a string (its Error() message) because the error interface is not directly marshallable. The field names follow camelCase JSON conventions.
Immutability ¶
Reports produced by SuccessReport, FailureReport, and SkippedReport are considered immutable after creation. Use Clone to obtain a deep copy when the report needs to be stored or passed to an asynchronous callback.
func FailureReport ¶ added in v0.5.0
func FailureReport(s Step, opts ...ReportOption) *Report
FailureReport creates a failure report for the given Step. It sets IsWorkflow to reflect whether s is a Workflow, Status=StatusFailed, and EndTime=time.Now(). Additional opts are applied after these defaults.
Attach an error with WithError to record the root cause:
return automa.FailureReport(stp, automa.WithError(err))
func NewReport ¶ added in v0.4.0
func NewReport(id string, opts ...ReportOption) *Report
NewReport creates a new Report with the given id and default field values:
- StartTime and EndTime are both set to time.Now().
- Status defaults to StatusSuccess.
- ExecutionMode defaults to StopOnError.
- RollbackMode defaults to ContinueOnError.
All provided opts are applied in order after the defaults, so options can override any default field. Prefer the typed constructors (SuccessReport, FailureReport, SkippedReport) over calling NewReport directly.
func RunWorkflow ¶ added in v0.5.0
func RunWorkflow(ctx context.Context, wb *WorkflowBuilder) *Report
RunWorkflow builds and runs the workflow from the given WorkflowBuilder. It returns a Report summarizing the execution result. If the workflow fails to build or prepare, it returns a failure Report with the corresponding error. Note if the prepare step fails, no rollback is performed or handleFailure isn't invoked as no steps have been executed yet. If preparation files, it returns error with ActionType set to ActionPrepare so that caller can distinguish it from execution errors.
func SkippedReport ¶ added in v0.5.0
func SkippedReport(s Step, opts ...ReportOption) *Report
SkippedReport creates a skipped report for the given Step. It sets IsWorkflow to reflect whether s is a Workflow, Status=StatusSkipped, and EndTime=time.Now(). Additional opts are applied after these defaults.
Use this when a step determines at runtime that its work is unnecessary:
return automa.SkippedReport(stp, automa.WithDetail("already provisioned"))
func StepFailureReport ¶ added in v0.4.0
func StepFailureReport(id string, opts ...ReportOption) *Report
StepFailureReport creates a failure report identified by a plain string id. It sets IsWorkflow=false, Status=StatusFailed, and EndTime=time.Now(). Additional opts are applied after these defaults.
func StepSkippedReport ¶ added in v0.4.0
func StepSkippedReport(id string, opts ...ReportOption) *Report
StepSkippedReport creates a skipped report identified by a plain string id. It sets IsWorkflow=false, Status=StatusSkipped, and EndTime=time.Now(). Additional opts are applied after these defaults.
func StepSuccessReport ¶ added in v0.4.0
func StepSuccessReport(id string, opts ...ReportOption) *Report
StepSuccessReport creates a success report identified by a plain string id rather than a Step. It sets IsWorkflow=false, Status=StatusSuccess, and EndTime=time.Now(). Additional opts are applied after these defaults.
Use this constructor in tests or in situations where a Step is not available but a success report needs to be produced for a named step.
func SuccessReport ¶ added in v0.5.0
func SuccessReport(s Step, opts ...ReportOption) *Report
SuccessReport creates a success report for the given Step. It sets IsWorkflow to reflect whether s is a Workflow, Status=StatusSuccess, and EndTime=time.Now(). Additional opts are applied after these defaults.
This is the primary constructor for step success outcomes inside an ExecuteFunc or RollbackFunc:
return automa.SuccessReport(stp) return automa.SuccessReport(stp, automa.WithMetadata(meta))
func (*Report) Clone ¶ added in v0.5.0
Clone returns a deep copy of the report. The copy is safe to modify without affecting the original. Specifically:
- All scalar fields (Id, Status, Action, …) are copied by value.
- Error is copied by reference (errors are immutable by convention).
- Metadata is deep-copied into a new StringMap.
- StepReports is deep-copied by recursively cloning each child.
- Rollback is deep-copied by recursively cloning the sub-report.
Clone on a nil receiver returns nil.
func (*Report) Duration ¶ added in v0.5.0
Duration returns the elapsed time between StartTime and EndTime. It returns zero for reports where both timestamps are equal (e.g. when only one of the two was set).
func (*Report) HasError ¶ added in v0.5.0
HasError reports whether the report carries a non-nil error. A report can have a failed status without an error (e.g. when the step set the status explicitly), so callers that need to distinguish these cases should check both IsFailed and HasError.
func (*Report) IsFailed ¶ added in v0.5.0
IsFailed reports whether the phase failed. The report is considered failed when either Status == StatusFailed OR a non-nil Error is present, whichever comes first. This means a step that sets a success status but also attaches an error is still treated as failed.
func (*Report) IsSkipped ¶ added in v0.5.0
IsSkipped reports whether the phase was skipped. A skipped report indicates that no work was performed (e.g. the step had no execute function, or the step's logic determined that the operation was already complete).
func (*Report) IsSuccess ¶ added in v0.5.0
IsSuccess reports whether the phase completed successfully with no error. Both conditions must hold: Status == StatusSuccess AND Error == nil. A report with Status == StatusSuccess but a non-nil Error is not considered successful.
func (*Report) MarshalJSON ¶ added in v0.4.0
MarshalJSON implements json.Marshaler. It serializes the Report into a JSON object using the marshalReport wire format. The Error field is serialized as its Error() string (or omitted when nil). The step ID is included when non-empty; rollback sub-reports typically omit the ID via the omitempty tag.
func (*Report) MarshalYAML ¶ added in v0.4.0
MarshalYAML implements yaml.Marshaler. It serializes the Report into a YAML mapping using the marshalReport wire format. The Error field is serialized as its Error() string (or omitted when nil). Field names follow the yaml struct tags on marshalReport.
type ReportOption ¶ added in v0.4.0
type ReportOption func(*Report)
ReportOption is a functional option applied to a Report by the NewReport, SuccessReport, FailureReport, and SkippedReport constructors. Options are applied in order after the report's default fields are set, so a later option can override an earlier one.
func WithActionType ¶ added in v0.4.0
func WithActionType(actionType TypeAction) ReportOption
WithActionType sets the report's Action field to the given TypeAction. This is used by the framework to record which lifecycle phase (Prepare, Execute, Rollback) produced the report.
func WithDetail ¶ added in v0.4.0
func WithDetail(detail string) ReportOption
WithDetail sets the report's Detail field to a human-readable message that provides additional context about the outcome. Useful for skipped reports where a brief explanation of why the step was skipped aids observability.
func WithEndTime ¶ added in v0.4.0
func WithEndTime(endTime time.Time) ReportOption
WithEndTime sets the report's EndTime field. If StartTime has not been set yet (is zero), it is also set to endTime so that Duration() never returns a negative value for reports that only record an end.
func WithError ¶ added in v0.4.0
func WithError(err error) ReportOption
WithError sets the report's Error field. The report's Status is not changed by this option; set the status explicitly with WithStatus or use the FailureReport constructor which sets both StatusFailed and an error in one call.
func WithExecutionMode ¶ added in v0.7.0
func WithExecutionMode(mode TypeMode) ReportOption
WithExecutionMode sets the report's ExecutionMode field. This records the workflow's execution policy at the time the report was produced and is used for audit and replay purposes.
func WithIsWorkflow ¶ added in v0.5.0
func WithIsWorkflow(isWorkflow bool) ReportOption
WithIsWorkflow sets the report's IsWorkflow field. When true, readers of the report know it was produced by a Workflow rather than a plain Step, which affects how StepReports is interpreted.
func WithMetadata ¶ added in v0.4.0
func WithMetadata(metadata StringMap) ReportOption
WithMetadata sets the report's Metadata field to the given StringMap, replacing any previously set metadata. Use this option to attach structured diagnostic information (resource IDs, counts, configuration values) to a report.
func WithReport ¶ added in v0.4.0
func WithReport(report *Report) ReportOption
WithReport merges selected fields from an existing report into the target report. Only non-zero/non-nil fields in the source are applied; existing non-zero fields in the target are overwritten. This is used by defaultStep to promote user-returned report details (Detail, Metadata, Error, etc.) into the final canonically-constructed report while ensuring the ActionType and timing fields are always set by the framework.
Fields merged (when non-zero/non-nil in source):
- Detail, Action, StartTime, EndTime, Status, Error
- Metadata (merged key-by-key into the target map)
- StepReports (appended)
- Rollback (replaced)
- ExecutionMode and RollbackMode (always copied)
func WithRollbackMode ¶ added in v0.4.0
func WithRollbackMode(mode TypeMode) ReportOption
WithRollbackMode sets the report's RollbackMode field. This records the workflow's rollback policy at the time the report was produced.
func WithRollbackReport ¶ added in v0.4.0
func WithRollbackReport(rollback *Report) ReportOption
WithRollbackReport sets the Rollback field of the report to the given sub-report. This is used by the workflow to attach the rollback outcome tree to the report of the step (or workflow) that triggered rollback.
func WithStartTime ¶ added in v0.4.0
func WithStartTime(startTime time.Time) ReportOption
WithStartTime sets the report's StartTime field. If EndTime has not been set yet (is zero), it is also set to startTime so that Duration() never returns a negative value for reports that only record a start.
func WithStatus ¶ added in v0.4.0
func WithStatus(status TypeStatus) ReportOption
WithStatus sets the report's Status field to the given TypeStatus. This option is applied after the constructor's default status, so it can override the default (e.g. to mark a report as Skipped inside a custom execute function).
func WithStepReports ¶ added in v0.4.0
func WithStepReports(reports ...*Report) ReportOption
WithStepReports appends one or more child reports to the report's StepReports slice. If the slice is nil it is allocated first. This option is used by the workflow to attach per-step execution reports to the workflow's final report.
func WithWorkflow ¶ added in v0.7.0
func WithWorkflow(w *workflow) ReportOption
WithWorkflow copies the IsWorkflow flag, ExecutionMode, and RollbackMode from a workflow into the report. This is called by the workflow's Execute and Rollback methods to stamp the workflow's configuration onto its final report. It is a no-op when w is nil.
type RollbackFunc ¶ added in v0.5.0
RollbackFunc is the signature for a step's rollback function. It is called in reverse step order when a later step fails and the workflow is configured with RollbackOnError. It receives the execution context and the owning Step, and must return a non-nil Report. Returning nil causes [defaultStep.Rollback] to synthesise a Failure report automatically. Rollback functions should be idempotent.
type RuntimeValue ¶ added in v0.8.0
type RuntimeValue[T any] struct { // contains filtered or unexported fields }
RuntimeValue is a generic container for a default value together with optional user input and an optional effective function.
Resolution semantics:
- If an effectiveFunc is provided, it is invoked during the first call to Effective() to compute the effective value. The function is expected to return a non-nil EffectiveValue and to be side-effect-free when it returns shouldCache=true.
- If effectiveFunc is not provided, the effective value is determined at construction time: userInput (if provided) otherwise defaultVal.
- Default() returns the configured defaultVal.
- UserInput() returns the configured userInput, if any.
Concurrency:
- RuntimeValue is safe for concurrent use by multiple goroutines.
- The type uses an internal sync.RWMutex for protecting its fields and golang.org/x/sync/singleflight to deduplicate concurrent evaluations of effectiveFunc when the cached effective value is not yet set.
- Provided Value implementations are assumed to be safe for concurrent use or immutable; if they are mutable, callers should take care to clone or otherwise protect them.
func NewRuntime ¶ added in v0.8.3
func NewRuntime[T any](defaultVal T, opts ...ValueOption[T]) (*RuntimeValue[T], error)
NewRuntime constructs a new RuntimeValue instance from a default value.
The defaultVal must be provided and cannot be nil. Optional ValueOptions may set userInput and effectiveFunc.
func NewRuntimeValue ¶ added in v0.8.0
func NewRuntimeValue[T any](defaultVal Value[T], opts ...ValueOption[T]) (*RuntimeValue[T], error)
NewRuntimeValue constructs a new RuntimeValue instance.
The defaultVal must be provided and cannot be nil. Optional ValueOptions may set userInput and effectiveFunc. If effectiveFunc is not provided, the effective value is set to userInput if present, otherwise to defaultVal.
func (*RuntimeValue[T]) ClearCache ¶ added in v0.8.0
func (v *RuntimeValue[T]) ClearCache()
ClearCache clears any cached effective value so subsequent Effective calls will re-evaluate according to the configured effectiveFunc (if any).
func (*RuntimeValue[T]) Clone ¶ added in v0.8.0
func (v *RuntimeValue[T]) Clone() (*RuntimeValue[T], error)
Clone produces a deep copy of the RuntimeValue.
The cloned RuntimeValue preserves the effectiveFunc reference and clones effective, defaultVal and userInput (if non-nil) using their Clone methods.
If the receiver is nil, Clone returns an error (errorx.IllegalState).
Clone snapshots the pointers under a read lock, then performs the potentially expensive Clone operations outside the lock to avoid blocking concurrent readers.
Note: the singleflight.Group is intentionally not copied (zero-value is fine). The clone will deduplicate its own concurrent Effective invocations.
func (*RuntimeValue[T]) Default ¶ added in v0.8.0
func (v *RuntimeValue[T]) Default() Value[T]
Default returns the configured default Value.
This method is nil-receiver safe: calling Default on a nil *RuntimeValue returns nil.
func (*RuntimeValue[T]) Effective ¶ added in v0.8.0
func (v *RuntimeValue[T]) Effective() (*EffectiveValue[T], error)
Effective returns the effective EffectiveValue for the runtime value. It uses the stored context for effectiveFunc invocation.
See EffectiveWithContext for full semantics and concurrency behavior.
func (*RuntimeValue[T]) EffectiveWithContext ¶ added in v0.8.1
func (v *RuntimeValue[T]) EffectiveWithContext(ctx context.Context) (*EffectiveValue[T], error)
EffectiveWithContext returns the effective EffectiveValue for the runtime value, using the provided context for effectiveFunc invocation. It does not modify the stored context of the RuntimeValue.
If an effectiveFunc is provided, it is invoked to compute the effective value on demand. If the effectiveFunc returns a value with shouldCache==true, the first successful non-nil result is cached for subsequent calls. If shouldCache==false the computed result is returned but not cached.
Concurrency: this method is safe for concurrent callers. It uses a read-mostly pattern: read locks for fast returns and a write lock only when the effective value must be cached after computing it via effectiveFunc. Concurrent evaluations of effectiveFunc are deduplicated using singleflight.
func (*RuntimeValue[T]) SetEffectiveFunc ¶ added in v0.8.0
func (v *RuntimeValue[T]) SetEffectiveFunc(f EffectiveFunc[T])
SetEffectiveFunc safely sets a new EffectiveFunc at runtime. It clears any cached effective value to ensure the new function will be used.
func (*RuntimeValue[T]) SetUserInput ¶ added in v0.8.0
func (v *RuntimeValue[T]) SetUserInput(user Value[T])
SetUserInput safely sets (or clears) the user input Value at runtime. If an effectiveFunc is not configured the effective value is updated immediately to reflect the new userInput; otherwise the cached effective is cleared so future calls will re-evaluate.
func (*RuntimeValue[T]) UserInput ¶ added in v0.8.0
func (v *RuntimeValue[T]) UserInput() Value[T]
UserInput returns the configured user input Value, if any.
This method is nil-receiver safe: calling UserInput on a nil *RuntimeValue returns nil. User input is nil if not provided via WithUserInput or SetUserInput, so callers should check for nil.
func (*RuntimeValue[T]) WithContext ¶ added in v0.8.1
func (v *RuntimeValue[T]) WithContext(ctx context.Context) *RuntimeValue[T]
WithContext sets the context to be used for effectiveFunc invocations. It returns the same RuntimeValue instance for chaining.
type StateBag ¶ added in v0.5.0
type StateBag interface {
// Get returns the raw value stored under key and whether it was present.
// No coercion is applied; the caller receives exactly what was passed to Set.
Get(key Key) (interface{}, bool)
// Set stores value under key, replacing any prior value, and returns the
// StateBag to allow method chaining: bag.Set("a", 1).Set("b", 2).
// Storing a nil value is permitted and is distinguishable from an absent
// key via Get.
Set(key Key, value interface{}) StateBag
// Delete removes the entry for key. It is a no-op when the key is absent.
// Returns the StateBag for chaining.
Delete(key Key) StateBag
// Clear removes all entries, leaving the bag empty but still usable.
// Returns the StateBag for chaining.
Clear() StateBag
// Keys returns a snapshot of all keys currently stored in the bag.
// Order is not guaranteed. The returned slice is a copy.
Keys() []Key
// Size returns the number of key-value pairs currently in the bag.
Size() int
// Items returns a shallow snapshot of all key-value pairs as a new
// map[Key]interface{}. Modifying the returned map does not affect the bag.
Items() map[Key]interface{}
// Merge copies every key-value pair from other into this bag, overwriting
// existing keys. Returns the receiver and any error. Passing nil is a no-op.
Merge(other StateBag) (StateBag, error)
// Clone returns a deep copy of the bag. Values that implement a Clone()
// method are deep-copied; all others are shallow-copied.
Clone() (StateBag, error)
// String returns the string value stored under key, coercing numeric and
// boolean types to their string representations. Returns ("", false) when
// the key is absent or coercion is not possible.
String(key Key) (string, bool)
// Bool returns the bool value stored under key, coercing numeric types
// (non-zero → true) and the strings "true"/"false". Returns (false, false)
// when the key is absent or coercion is not possible.
Bool(key Key) (bool, bool)
// Int returns the int value stored under key, truncating floats toward
// zero and parsing numeric strings. Returns (0, false) when absent or
// not coercible.
Int(key Key) (int, bool)
// Int8 is like Int but returns int8. Returns (0, false) when absent or
// not coercible.
Int8(key Key) (int8, bool)
// Int16 is like Int but returns int16. Returns (0, false) when absent or
// not coercible.
Int16(key Key) (int16, bool)
// Int32 is like Int but returns int32. Returns (0, false) when absent or
// not coercible.
Int32(key Key) (int32, bool)
// Int64 is like Int but returns int64. Returns (0, false) when absent or
// not coercible.
Int64(key Key) (int64, bool)
// Float returns the float64 value stored under key, converting all numeric
// types and parsing numeric strings. Returns (0.0, false) when absent or
// not coercible.
Float(key Key) (float64, bool)
// Float32 is like Float but returns float32. Returns (0.0, false) when
// absent or not coercible.
Float32(key Key) (float32, bool)
// Float64 is like Float. Returns (0.0, false) when absent or not coercible.
Float64(key Key) (float64, bool)
// Marshaler serializes the bag as a JSON object (key → value).
json.Marshaler
// Unmarshaler replaces the bag's contents by decoding a JSON object.
// After decoding, JSON numbers are stored as float64; use the typed
// accessors to coerce them back to integer or other types.
json.Unmarshaler
// Marshaler serializes the bag as a YAML mapping (key → value).
yaml.Marshaler
// Unmarshaler replaces the bag's contents by decoding a YAML mapping.
yaml.Unmarshaler
}
StateBag is a thread-safe key-value store used as the backing storage for each namespace partition inside a NamespacedStateBag.
It is intentionally low-level: it has no notion of local vs. global scope. Callers that need namespace isolation should work through NamespacedStateBag and its Local(), Global(), and WithNamespace() methods rather than operating on a StateBag directly.
The primary implementation is SyncStateBag.
Typed accessors ¶
The String, Bool, Int, … methods are coercing accessors: they attempt to convert the stored value to the target type even when the types do not match exactly (e.g. a float64 stored by a JSON round-trip will be returned by Int()). Each returns (value, true) on success and (zero, false) when the key is absent or coercion fails.
When you need to distinguish "key absent / coercion failed" from an intentionally stored zero value, use FromState[T] which surfaces the same semantics explicitly. For the raw, uncoerced value use Get.
Serialization ¶
StateBag embeds json.Marshaler, json.Unmarshaler, yaml.Marshaler, and yaml.Unmarshaler so that workflow state can be persisted and restored. After a JSON round-trip, numbers are decoded as float64 by encoding/json; the typed accessors handle this transparently via internal coercion.
func StateFromContext ¶ added in v0.5.0
StateFromContext retrieves the StateBag stored in ctx under KeyState. If ctx is nil or contains no StateBag, an empty *SyncStateBag is returned so callers never need to check for nil before using the result.
type Step ¶
type Step interface {
// Id returns the unique identifier for this step within its workflow.
// IDs are used in reports, logging, and the Registry.
Id() string
// Prepare is called by the workflow before Execute. It may enrich ctx
// (e.g. inject a logger or deadline), validate preconditions, or
// pre-populate the step's local state. The returned context is passed to
// Execute and Rollback. A non-nil error aborts this step.
Prepare(ctx context.Context) (context.Context, error)
// Execute performs the step's primary work and returns a [Report]
// describing the outcome. The report's [TypeStatus] determines whether
// the workflow continues, stops, or begins rolling back.
Execute(ctx context.Context) *Report
// Rollback undoes the work performed by Execute. It is called in reverse
// step order when the workflow is operating in [RollbackOnError] mode and
// a later step has failed. Rollback should be idempotent.
Rollback(ctx context.Context) *Report
// State returns the [NamespacedStateBag] attached to this step.
// Local() is private to this step; Global() is shared across all steps in
// the workflow; WithNamespace(name) provides an isolated named partition.
State() NamespacedStateBag
// WithState returns a shallow copy of the step with the provided
// [NamespacedStateBag] attached. The workflow calls this before Prepare to
// inject each step's own namespaced view of the workflow state.
WithState(s NamespacedStateBag) Step
}
Step is the fundamental unit of work in an automa workflow.
Lifecycle ¶
Each Step passes through up to three phases during a workflow execution:
Prepare(ctx) — called before Execute. Use it to enrich the context, validate preconditions, or pre-populate local state. Returning a non-nil error aborts execution of this step (and, depending on the workflow's TypeMode, possibly the whole workflow).
Execute(ctx) — performs the step's primary work. Returns a Report whose TypeStatus (Success, Failed, Skipped) drives the workflow's execution-mode logic.
Rollback(ctx) — called in reverse order when the workflow needs to undo previously executed steps. It is only invoked when the workflow's execution mode is RollbackOnError and a later step has failed. Rollback should be idempotent: it may be called more than once.
State ¶
State() returns the NamespacedStateBag associated with this step. The workflow provides each step with its own namespaced view so that local writes are isolated while global writes remain visible to subsequent steps. WithState returns a shallow copy of the step with the given bag attached, which is how the workflow injects per-step state before calling Prepare.
Implementations ¶
Use NewStepBuilder to construct a Step without implementing this interface directly. For sub-workflows, a Workflow also satisfies Step and can be nested inside another workflow's step list.
type StepBuilder ¶ added in v0.4.0
type StepBuilder struct {
// Step is the defaultStep being assembled. It is exported only so that
// WorkflowBuilder can access it when inheriting execution modes; callers
// should use the With* methods rather than modifying this field directly.
Step *defaultStep
}
StepBuilder is a fluent builder for constructing Step instances without implementing the Step interface directly.
Every method returns the same *StepBuilder so calls can be chained:
step := automa.NewStepBuilder().
WithId("provision-db").
WithPrepare(func(ctx context.Context, stp automa.Step) (context.Context, error) {
// validate config, inject logger …
return ctx, nil
}).
WithExecute(func(ctx context.Context, stp automa.Step) *automa.Report {
// provision the database …
return automa.SuccessReport(stp)
}).
WithRollback(func(ctx context.Context, stp automa.Step) *automa.Report {
// tear down the database …
return automa.SuccessReport(stp)
})
Call StepBuilder.Build to finalise and obtain the Step. Build resets the internal state so the builder can be reused to construct another step.
StepBuilder is not safe for concurrent use.
func NewStepBuilder ¶ added in v0.4.0
func NewStepBuilder() *StepBuilder
NewStepBuilder returns a new, empty StepBuilder ready for configuration via the With* methods.
func (*StepBuilder) Build ¶ added in v0.4.0
func (s *StepBuilder) Build() (Step, error)
Build finalises the step under construction and returns it as a Step.
After Build returns, the builder is reset to a fresh empty [defaultStep] so it can be used to construct another step. The returned Step and the next construction cycle are fully independent.
Returns an error if [Validate] fails.
func (*StepBuilder) BuildAndCopy ¶ added in v0.5.0
func (s *StepBuilder) BuildAndCopy() (Step, error)
BuildAndCopy finalises the step under construction, returns it as a Step, and resets the builder with a copy of every field except id (which is cleared to force the caller to assign a new unique ID before the next Build).
This is useful when creating several steps that share the same logger, prepare function, execute logic, or callbacks but need distinct IDs:
base := automa.NewStepBuilder().
WithExecute(sharedExecuteFunc).
WithRollback(sharedRollbackFunc)
step1, _ := base.WithId("step-1").BuildAndCopy()
step2, _ := base.WithId("step-2").BuildAndCopy()
If the step has a NamespacedStateBag it is deep-cloned via NamespacedStateBag.Clone so the new construction cycle starts with an independent copy of the state. Returns an error if [Validate] fails or if state cloning fails.
func (*StepBuilder) Id ¶ added in v0.4.0
func (s *StepBuilder) Id() string
Id returns the ID that has been set on the step under construction. This satisfies the Builder interface and is also used by WorkflowBuilder to key the step in its internal builder map.
func (*StepBuilder) Validate ¶ added in v0.4.0
func (s *StepBuilder) Validate() error
Validate checks that the step under construction has all required fields:
- id must be non-empty.
- execute must be non-nil.
It is called automatically by [Build] and [BuildAndCopy], and also by WorkflowBuilder.Validate when assembling a workflow.
func (*StepBuilder) WithAsyncCallbacks ¶ added in v0.5.0
func (s *StepBuilder) WithAsyncCallbacks(enable bool) *StepBuilder
WithAsyncCallbacks controls whether the onCompletion and onFailure hooks are invoked synchronously (false, the default) or in a new goroutine (true). When async is enabled the callback receives a deep clone of the Report so that mutations in the goroutine cannot race with the workflow.
func (*StepBuilder) WithExecute ¶ added in v0.5.0
func (s *StepBuilder) WithExecute(f ExecuteFunc) *StepBuilder
WithExecute sets the ExecuteFunc that performs the step's primary work. This field is required; [Validate] returns an error if it is nil.
func (*StepBuilder) WithId ¶ added in v0.5.0
func (s *StepBuilder) WithId(id string) *StepBuilder
WithId sets the unique identifier for the step being built and returns the builder for chaining. The ID must be non-empty; [Validate] will return an error if it is not set before [Build] is called.
func (*StepBuilder) WithLogger ¶ added in v0.5.0
func (s *StepBuilder) WithLogger(logger *slog.Logger) *StepBuilder
WithLogger attaches a structured slog.Logger to the step. The logger is stored on the step for internal use; it is not currently surfaced through the Step interface, so step callbacks should close over their own logger if they need one. A nil logger leaves the step without a logger.
func (*StepBuilder) WithOnCompletion ¶ added in v0.5.0
func (s *StepBuilder) WithOnCompletion(f OnCompletionFunc) *StepBuilder
WithOnCompletion sets a callback that is invoked after a successful or skipped Execute. See OnCompletionFunc for the calling conventions and the async-callback note.
func (*StepBuilder) WithOnFailure ¶ added in v0.5.0
func (s *StepBuilder) WithOnFailure(f OnFailureFunc) *StepBuilder
WithOnFailure sets a callback that is invoked after a failed Execute. See OnFailureFunc for the calling conventions and the async-callback note.
func (*StepBuilder) WithPrepare ¶ added in v0.5.0
func (s *StepBuilder) WithPrepare(f PrepareFunc) *StepBuilder
WithPrepare sets the PrepareFunc that will be called before Execute. Use it to enrich the context, validate preconditions, or pre-populate local state. Omitting this call means no preparation is performed.
func (*StepBuilder) WithRollback ¶ added in v0.5.0
func (s *StepBuilder) WithRollback(f RollbackFunc) *StepBuilder
WithRollback sets the RollbackFunc that undoes the step's work when a later step fails and the workflow uses RollbackOnError. Omitting this call means rollback is a no-op (returns Skipped).
func (*StepBuilder) WithState ¶ added in v0.5.0
func (s *StepBuilder) WithState(state NamespacedStateBag) *StepBuilder
WithState pre-populates the NamespacedStateBag for the step under construction. This is rarely needed directly; the workflow injects state automatically via Step.WithState before calling Prepare. Use this method only when running a step outside a workflow or in tests.
type StringMap ¶ added in v0.10.0
func NewStringMap ¶ added in v0.10.0
func NewStringMap() StringMap
func StringMapFromState ¶ added in v0.5.0
StringMapFromState is a convenience wrapper around MapFromState[string,string]. Returns an empty StringMap when the key is absent or the value is not a StringMap.
func ToStringMap ¶ added in v0.10.0
ToStringMap converts common map shapes into a StringMap (map[string]string).
Supported shapes:
- StringMap: returned as-is.
- map[string]interface{}: each value is formatted with fmt.Sprint.
- Any other map (detected via reflection): keys and values are formatted with fmt.Sprint.
Returns (nil, false) for nil input or non-map types.
type SyncNamespacedStateBag ¶ added in v0.9.0
type SyncNamespacedStateBag struct {
// contains filtered or unexported fields
}
SyncNamespacedStateBag is a thread-safe implementation of NamespacedStateBag that partitions state into three kinds of namespace:
- Local — private to a single step; each step receives its own local bag so writes in one step cannot accidentally overwrite another step's data.
- Global — shared across all steps in a workflow; mutations are visible to every subsequent step that reads from the same global bag.
- Custom — an arbitrary set of named bags (e.g. "database-1", "cache"), useful when a reusable step implementation needs a stable, collision-free namespace regardless of how many instances are running.
Concurrency model:
- n.mu (sync.RWMutex) protects the three pointer fields (local, global, custom map) and the custom map itself.
- Read operations on the pointers (Local, Global) use a fast-path RLock and upgrade to a write lock only when lazy initialization is needed.
- Write operations (WithNamespace, Merge, UnmarshalJSON, UnmarshalYAML, MarshalJSON, MarshalYAML) acquire the write lock for the duration.
- The individual StateBag instances (local, global, custom entries) are themselves thread-safe SyncStateBag values; their own internal lock serialises Set/Get/Delete calls.
Zero value: a zero-value SyncNamespacedStateBag is safe to use without explicit construction. All fields are lazily initialized on first access via initLocked, so the following is valid:
var ns automa.SyncNamespacedStateBag
ns.Local().Set("key", "value")
func NewNamespacedStateBag ¶ added in v0.9.0
func NewNamespacedStateBag(local, global StateBag) *SyncNamespacedStateBag
NewNamespacedStateBag constructs a SyncNamespacedStateBag with explicit initial local and global bags. Either argument may be nil, in which case an empty *SyncStateBag is substituted. The custom namespace map is always initialized to an empty map so that WithNamespace can write without a nil-map panic.
Example:
// Share a common global bag between steps while giving each step its own local bag.
global := automa.NewStateBag()
global.Set("env", "production")
ns := automa.NewNamespacedStateBag(nil, global)
func (*SyncNamespacedStateBag) Clone ¶ added in v0.9.0
func (n *SyncNamespacedStateBag) Clone() (NamespacedStateBag, error)
Clone returns a fully independent deep copy of the SyncNamespacedStateBag, including all three namespace kinds.
Clone strategy:
- A write lock is acquired to snapshot the field pointers and the custom map (so the set of namespace names cannot change mid-clone).
- The lock is released before cloning individual bags to avoid holding n's write lock while the inner SyncStateBag Clone() calls acquire their own locks, which would otherwise risk lock ordering issues.
- Local, global, and each custom bag are deep-copied in sequence via their own Clone() methods.
The returned NamespacedStateBag shares no memory with the original: mutations to the clone do not affect the original, and vice versa.
Returns (nil, error) when the receiver is nil or when any inner Clone fails.
func (*SyncNamespacedStateBag) Global ¶ added in v0.9.0
func (n *SyncNamespacedStateBag) Global() StateBag
Global returns the StateBag for the global namespace.
The global bag is shared across all steps in a workflow. A step can publish data (e.g. configuration, counters) by writing to Global(), and any subsequent step can read it. Because the bag is shared, concurrent writes from different goroutines are serialised by SyncStateBag's own mutex.
Thread-safety: same fast-path/slow-path pattern as Local().
func (*SyncNamespacedStateBag) Local ¶ added in v0.9.0
func (n *SyncNamespacedStateBag) Local() StateBag
Local returns the StateBag for the local namespace.
The local bag is private to the step that owns this SyncNamespacedStateBag. Writes to it are never visible to other steps, even when they share the same global bag.
Thread-safety: a fast-path read lock checks whether the field is already initialized. Only if it is nil (zero-value receiver) is the lock upgraded to a write lock for initialization. After initialization the same bag pointer is always returned.
func (*SyncNamespacedStateBag) MarshalJSON ¶ added in v0.10.0
func (n *SyncNamespacedStateBag) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler. It serializes all three namespace kinds into the namespacedSnapshot wire format:
{
"local": { "key": value, … },
"global": { "key": value, … },
"custom": { "ns-name": { "key": value, … }, … }
}
A nil receiver marshals as JSON null.
Thread-safety: a write lock is acquired briefly to initialize nil fields and snapshot the field pointers. Individual bag marshaling then proceeds outside the lock to avoid holding n's write lock while inner SyncStateBag read locks are taken.
func (*SyncNamespacedStateBag) MarshalYAML ¶ added in v0.10.0
func (n *SyncNamespacedStateBag) MarshalYAML() (interface{}, error)
MarshalYAML implements yaml.Marshaler. It returns a namespacedSnapshot struct that the YAML encoder serializes into the same three-section layout as MarshalJSON:
local:
key: value
global:
key: value
custom:
ns-name:
key: value
A nil receiver returns (nil, nil), which the encoder renders as YAML null.
Thread-safety: same snapshot-then-release pattern as MarshalJSON.
func (*SyncNamespacedStateBag) Merge ¶ added in v0.9.0
func (n *SyncNamespacedStateBag) Merge(other NamespacedStateBag) (NamespacedStateBag, error)
Merge merges every namespace from other into n, overwriting conflicting keys, and returns n.
Merge semantics per namespace:
- Local: other's local keys are merged into n's local bag (other wins on key conflicts).
- Global: other's global keys are merged into n's global bag.
- Custom: for each custom namespace in other —
- If the same namespace already exists in n, the two bags are merged (other wins on key conflicts).
- If the namespace is new to n, other's bag is deep-cloned and added so that n and other do not share the same StateBag pointer.
Type constraint: other must be a *SyncNamespacedStateBag. If a different implementation is passed, an error is returned immediately so that callers are not surprised by silently dropped custom namespaces.
Deadlock prevention: other's field snapshots are read before n's write lock is acquired, following the same snapshot-before-lock pattern used by SyncStateBag.Merge.
Returns (n, nil) on success. Returns (n, nil) unchanged when other is nil. Returns (nil, error) on type mismatch or inner merge/clone failure.
func (*SyncNamespacedStateBag) UnmarshalJSON ¶ added in v0.10.0
func (n *SyncNamespacedStateBag) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler. It decodes the namespacedSnapshot wire format into the bag, completely replacing all existing namespace contents.
Type notes (standard encoding/json behaviour applies to values):
- JSON numbers become float64.
- JSON objects become map[string]interface{}.
- JSON arrays become []interface{}.
Use the typed accessors (Int, String, …) or FromState after unmarshaling to coerce float64 numbers back to integer types.
Returns an error if the receiver is nil or if the JSON is malformed.
func (*SyncNamespacedStateBag) UnmarshalYAML ¶ added in v0.10.0
func (n *SyncNamespacedStateBag) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML implements yaml.Unmarshaler. It decodes a YAML mapping node in the namespacedSnapshot format into the bag, completely replacing all existing namespace contents.
Type notes (gopkg.in/yaml.v3 behaviour applies to values):
- YAML integers decode as int.
- YAML floats decode as float64.
- YAML booleans decode as bool.
Returns an error if the receiver is nil or if the YAML node cannot be decoded into the expected structure.
func (*SyncNamespacedStateBag) WithNamespace ¶ added in v0.9.0
func (n *SyncNamespacedStateBag) WithNamespace(name string) StateBag
WithNamespace returns the StateBag for the named custom namespace, creating it on demand if it does not already exist.
Custom namespaces are useful for step implementations that are instantiated multiple times in the same workflow (e.g. "setup-bind-mount-for-app1" and "setup-bind-mount-for-app2"). By writing to WithNamespace(name) each instance gets an isolated bag without the caller needing to worry about key collisions with other instances.
The returned bag is stable: repeated calls with the same name always return the same underlying StateBag pointer.
Thread-safety: the write lock is always acquired to guarantee atomic check-then-create semantics.
type SyncStateBag ¶ added in v0.5.0
type SyncStateBag struct {
// contains filtered or unexported fields
}
SyncStateBag is a thread-safe implementation of StateBag backed by a plain map[Key]interface{} protected by a sync.RWMutex.
Concurrency model:
- Read operations (Get, Items, Keys, Size, String, Int, …) acquire a shared read lock, so multiple goroutines may read concurrently.
- Write operations (Set, Delete, Clear, Merge, UnmarshalJSON, UnmarshalYAML) acquire an exclusive write lock.
- Snapshot methods (Items, MarshalJSON, MarshalYAML) hold the read lock for the full traversal, producing a consistent point-in-time view.
Zero value: a zero-value SyncStateBag is ready to use; the internal map is allocated lazily on the first write.
func (*SyncStateBag) Bool ¶ added in v0.6.0
func (s *SyncStateBag) Bool(key Key) (bool, bool)
Bool retrieves a bool value from the bag for key using FromState[bool].
Coercion rules (applied after an exact bool match fails):
- String "true"/"false" (case-insensitive, as per strconv.ParseBool).
- Numeric types: non-zero → true, zero → false.
Returns (false, false) when the key is absent or the value cannot be coerced to bool.
func (*SyncStateBag) Clear ¶ added in v0.5.0
func (s *SyncStateBag) Clear() StateBag
Clear removes all entries from the bag, leaving it empty but still usable. Returns s for method chaining.
func (*SyncStateBag) Clone ¶ added in v0.5.0
func (s *SyncStateBag) Clone() (StateBag, error)
Clone returns a deep copy of the SyncStateBag as a StateBag.
Deep-copy strategy for each stored value:
- If the value is nil, nil is stored in the clone.
- If the value's type exposes a zero-argument Clone() (T, error) method, that method is called and the returned copy is stored. Any error is wrapped and returned, leaving the partially-built clone unreferenced.
- If the value's type exposes a zero-argument Clone() T method (single return), that method is called and the result is stored.
- All other values are shallow-copied (the same pointer/value is stored).
Clone acquires no lock of its own; it delegates to Items() which takes a read lock. This avoids re-entrant locking on Go's non-reentrant RWMutex.
Returns (nil, error) when the receiver is nil or when a value's Clone method returns an error.
func (*SyncStateBag) Delete ¶ added in v0.5.0
func (s *SyncStateBag) Delete(key Key) StateBag
Delete removes the entry for key from the bag. It is a no-op when the key is absent. Returns s for method chaining.
func (*SyncStateBag) Float ¶ added in v0.6.0
func (s *SyncStateBag) Float(key Key) (float64, bool)
Float retrieves a float64 value from the bag for key using FromState[float64].
Coercion rules (applied after an exact float64 match fails):
- All integer and float types are converted via float64 cast.
- Numeric strings and json.Number are parsed with strconv.ParseFloat.
Returns (0.0, false) when the key is absent or coercion fails.
func (*SyncStateBag) Float32 ¶ added in v0.6.0
func (s *SyncStateBag) Float32(key Key) (float32, bool)
Float32 retrieves a float32 value from the bag for key. See Float for coercion rules. Returns (0.0, false) when the key is absent or coercion fails.
func (*SyncStateBag) Float64 ¶ added in v0.6.0
func (s *SyncStateBag) Float64(key Key) (float64, bool)
Float64 retrieves a float64 value from the bag for key. See Float for coercion rules. Returns (0.0, false) when the key is absent or coercion fails.
func (*SyncStateBag) Get ¶ added in v0.5.0
func (s *SyncStateBag) Get(key Key) (interface{}, bool)
Get retrieves the raw value stored under key. Returns (value, true) when the key is present, or (nil, false) when it is absent. The caller receives the exact value that was stored — no coercion is applied.
func (*SyncStateBag) Int ¶ added in v0.6.0
func (s *SyncStateBag) Int(key Key) (int, bool)
Int retrieves an int value from the bag for key using FromState[int].
Coercion rules (applied after an exact int match fails):
- Other integer types are converted directly where the value fits.
- Float values are truncated toward zero (e.g. 3.9 → 3, -1.2 → -1).
- Numeric strings and json.Number are parsed; float strings are truncated.
Returns (0, false) when the key is absent or the value cannot be coerced.
func (*SyncStateBag) Int8 ¶ added in v0.6.0
func (s *SyncStateBag) Int8(key Key) (int8, bool)
Int8 retrieves an int8 value from the bag for key. See Int for coercion rules. Returns (0, false) when the key is absent or coercion fails.
func (*SyncStateBag) Int16 ¶ added in v0.6.0
func (s *SyncStateBag) Int16(key Key) (int16, bool)
Int16 retrieves an int16 value from the bag for key. See Int for coercion rules. Returns (0, false) when the key is absent or coercion fails.
func (*SyncStateBag) Int32 ¶ added in v0.6.0
func (s *SyncStateBag) Int32(key Key) (int32, bool)
Int32 retrieves an int32 value from the bag for key. See Int for coercion rules. Returns (0, false) when the key is absent or coercion fails.
func (*SyncStateBag) Int64 ¶ added in v0.6.0
func (s *SyncStateBag) Int64(key Key) (int64, bool)
Int64 retrieves an int64 value from the bag for key. See Int for coercion rules. Returns (0, false) when the key is absent or coercion fails.
func (*SyncStateBag) Items ¶ added in v0.5.0
func (s *SyncStateBag) Items() map[Key]interface{}
Items returns a shallow snapshot of all key-value pairs held by s as a new map[Key]interface{}. The snapshot is taken under a read lock, so it is consistent even when other goroutines are writing concurrently.
Callers must not use the returned map to modify s; it is a copy.
func (*SyncStateBag) Keys ¶ added in v0.5.0
func (s *SyncStateBag) Keys() []Key
Keys returns a snapshot of all keys currently stored in the bag. The order of keys is not guaranteed (Go map iteration order is random). The returned slice is a copy; modifying it does not affect the bag.
func (*SyncStateBag) MarshalJSON ¶ added in v0.10.0
func (s *SyncStateBag) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler. It serializes the bag as a JSON object whose keys are the string form of each Key and whose values are the JSON representations of the stored values.
A nil receiver marshals as JSON null.
Note: the snapshot is taken under a read lock, so concurrent writes that arrive during marshaling are not reflected in the output.
func (*SyncStateBag) MarshalYAML ¶ added in v0.10.0
func (s *SyncStateBag) MarshalYAML() (interface{}, error)
MarshalYAML implements yaml.Marshaler. It returns the bag contents as a map[string]interface{} that the YAML encoder will serialize. Keys are converted from Key to string.
A nil receiver returns (nil, nil), which the encoder renders as a YAML null.
Note: the snapshot is taken under a read lock; concurrent writes are not reflected in the output.
func (*SyncStateBag) Merge ¶ added in v0.5.0
func (s *SyncStateBag) Merge(other StateBag) (StateBag, error)
Merge copies every key-value pair from other into s, overwriting existing keys in s that also exist in other.
Deadlock prevention: the snapshot of other is taken via other.Items() before s's write lock is acquired. This eliminates two classes of deadlock:
- Self-merge (other == s): snapshotting before locking prevents an attempt to acquire an already-held read lock on the same RWMutex.
- Lock-order inversion: if two goroutines call a.Merge(b) and b.Merge(a) concurrently, neither holds a lock while calling the other bag's Items(), so they cannot deadlock waiting for each other.
Returns (s, nil) on success. Returns (s, nil) unchanged when other is nil.
func (*SyncStateBag) Set ¶ added in v0.5.0
func (s *SyncStateBag) Set(key Key, value interface{}) StateBag
Set stores value under key, replacing any previously stored value, and returns s to allow method chaining:
bag.Set("a", 1).Set("b", 2)
A nil value is permitted; it stores an explicit nil entry which is distinguishable from an absent key via Get.
func (*SyncStateBag) Size ¶ added in v0.5.0
func (s *SyncStateBag) Size() int
Size returns the number of key-value pairs currently stored in the bag.
func (*SyncStateBag) String ¶ added in v0.6.0
func (s *SyncStateBag) String(key Key) (string, bool)
String retrieves a string value from the bag for key using FromState[string].
Coercion rules (applied in order after an exact string match fails):
- Numeric types are formatted as their decimal string representation (integers without decimal point, floats using minimal precision).
- bool is formatted as "true" or "false".
- json.Number is returned as-is via its String() method.
- Types that implement the fmt.Stringer interface are formatted via their String() method.
Returns ("", false) when the key is absent, the stored value is nil, or the value cannot be coerced to a string.
func (*SyncStateBag) UnmarshalJSON ¶ added in v0.10.0
func (s *SyncStateBag) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler. It decodes a JSON object into the bag, replacing all existing contents. Keys in the JSON object are stored as Key values; JSON numbers become float64, objects become map[string]interface{}, and arrays become []interface{} — standard encoding/json behaviour.
Callers that need typed values after unmarshaling should use the typed accessors (Int, String, …) or FromState, which perform coercion from float64 and other JSON-decoded shapes.
Returns an error if the receiver is nil or if the JSON is malformed.
func (*SyncStateBag) UnmarshalYAML ¶ added in v0.10.0
func (s *SyncStateBag) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML implements yaml.Unmarshaler. It decodes a YAML mapping node into the bag, replacing all existing contents. Keys are stored as Key values. YAML integers are decoded as int by the yaml.v3 library; floats as float64; booleans as bool.
Returns an error if the receiver is nil or if the YAML node cannot be decoded into a map.
type TypeAction ¶ added in v0.4.0
type TypeAction uint8
TypeAction identifies which lifecycle phase of a Step produced a Report. It is serialized as a human-readable string in JSON and YAML output.
const ( // ActionPrepare indicates the report was produced during the Prepare phase. // This value is zero so it also serves as the default/unset state. ActionPrepare TypeAction = 0 // ActionExecute indicates the report was produced during the Execute phase. // This is the most common action type for step reports. ActionExecute TypeAction = 1 // ActionRollback indicates the report was produced during the Rollback phase. // Reports with this action are nested inside a parent report's Rollback field. ActionRollback TypeAction = 2 )
func (TypeAction) MarshalJSON ¶ added in v0.4.0
func (a TypeAction) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler. It serializes the action as its string name (e.g. "execute") rather than the numeric value, making JSON output self-describing.
func (TypeAction) MarshalYAML ¶ added in v0.4.0
func (a TypeAction) MarshalYAML() (interface{}, error)
MarshalYAML implements yaml.Marshaler. It serializes the action as its string name, consistent with MarshalJSON.
func (TypeAction) String ¶ added in v0.4.0
func (a TypeAction) String() string
String returns the human-readable name of the action: "prepare", "execute", "rollback", or "unknown" for unrecognised values.
func (*TypeAction) UnmarshalJSON ¶ added in v0.4.0
func (a *TypeAction) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler. It accepts the string names "prepare", "execute", and "rollback". Unrecognised values are silently mapped to 0 (ActionPrepare) rather than returning an error.
func (*TypeAction) UnmarshalYAML ¶ added in v0.4.0
func (a *TypeAction) UnmarshalYAML(value *yaml.Node) error
UnmarshalYAML implements yaml.Unmarshaler. It accepts the same string names as UnmarshalJSON. Unrecognised values are silently mapped to 0 (ActionPrepare).
type TypeMode ¶ added in v0.7.0
type TypeMode uint8
TypeMode controls the error-handling strategy applied by a Workflow during step execution or rollback. It answers the question: "when something goes wrong, what should happen next?"
Two independent TypeMode values are configured on each workflow:
- executionMode — governs what happens when a step's Execute fails.
- rollbackMode — governs what happens when a step's Rollback fails (only relevant when executionMode is RollbackOnError).
TypeMode is serialized as a human-readable string in JSON and YAML. Unknown values are treated as errors during unmarshaling (unlike TypeAction and TypeStatus which default silently).
const ( // ContinueOnError instructs the workflow to keep processing remaining steps // even after a failure. When used as executionMode, all steps run regardless // of earlier failures and the final workflow report reflects the aggregate // outcome. When used as rollbackMode, all previously executed steps are // rolled back even if an earlier rollback itself fails. // // Serialized as "continue". ContinueOnError TypeMode = 1 // StopOnError instructs the workflow to halt immediately when a failure is // encountered, skipping all remaining steps. No rollback is triggered. // This is the default executionMode. // // Serialized as "stop". StopOnError TypeMode = 2 // RollbackOnError instructs the workflow to halt on the first failure and // then roll back all previously executed steps in reverse order. The // rollbackMode setting controls whether the rollback loop itself stops or // continues when an individual rollback fails. // // Serialized as "rollback". RollbackOnError TypeMode = 3 )
func (TypeMode) MarshalJSON ¶ added in v0.7.0
MarshalJSON implements json.Marshaler. It serializes the mode as its string name (e.g. "rollback") rather than the numeric value, making JSON output self-describing and stable across future reordering of constant values.
func (TypeMode) MarshalYAML ¶ added in v0.7.0
MarshalYAML implements yaml.Marshaler. It serializes the mode as its string name, consistent with MarshalJSON.
func (TypeMode) String ¶ added in v0.7.0
String returns the human-readable name of the mode: "continue", "stop", "rollback", or "unknown" for unrecognised values.
func (*TypeMode) UnmarshalJSON ¶ added in v0.7.0
UnmarshalJSON implements json.Unmarshaler. It accepts the string names "continue", "stop", and "rollback". Unlike TypeAction and TypeStatus, an unrecognised value returns an error to prevent silent misconfiguration of critical workflow behaviour.
type TypeStatus ¶ added in v0.4.0
type TypeStatus uint8
TypeStatus represents the outcome of a Step lifecycle phase (Prepare, Execute, or Rollback). It is recorded in every Report and is the primary signal used by the workflow's execution-mode logic to decide whether to continue, stop, or roll back.
The zero value of TypeStatus is intentionally unused so that an uninitialised report is distinguishable from one with a real outcome. It is serialized as a human-readable string in JSON and YAML output.
const ( // StatusSuccess indicates the phase completed without error. A workflow // proceeds to the next step after a successful execute. StatusSuccess TypeStatus = 1 // StatusFailed indicates the phase encountered an error. Depending on the // workflow's [TypeMode], a failed execute may stop, continue, or trigger // rollback. StatusFailed TypeStatus = 2 // StatusSkipped indicates the phase was bypassed. This happens when a step // has no execute function configured, or when the step's own logic // determines that no work is needed. A skipped step does not count as a // failure and does not stop execution. StatusSkipped TypeStatus = 3 )
func (TypeStatus) MarshalJSON ¶ added in v0.4.0
func (s TypeStatus) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler. It serializes the status as its string name (e.g. "success") rather than the numeric value, making JSON output self-describing.
func (TypeStatus) MarshalYAML ¶ added in v0.4.0
func (s TypeStatus) MarshalYAML() (interface{}, error)
MarshalYAML implements yaml.Marshaler. It serializes the status as its string name, consistent with MarshalJSON.
func (TypeStatus) String ¶ added in v0.4.0
func (s TypeStatus) String() string
String returns the human-readable name of the status: "success", "failed", "skipped", or "unknown" for unrecognised values.
func (*TypeStatus) UnmarshalJSON ¶ added in v0.4.0
func (s *TypeStatus) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler. It accepts the string names "success", "failed", and "skipped". Unrecognised values are silently mapped to 0 (zero/uninitialised) rather than returning an error.
func (*TypeStatus) UnmarshalYAML ¶ added in v0.4.0
func (s *TypeStatus) UnmarshalYAML(value *yaml.Node) error
UnmarshalYAML implements yaml.Unmarshaler. It accepts the same string names as UnmarshalJSON. Unrecognised values are silently mapped to 0.
type Value ¶ added in v0.8.0
Value provides primitives for building and resolving typed configuration values used by the automa framework. It is centered around the concept of a runtime-resolvable value container represented by the Value[T] interface which supports producing a deep-cloned copy.
This file contains the default Value implementation and the RuntimeValue container which composes default values, optional user input and an optional effective resolution function.
func NewValue ¶ added in v0.8.0
NewValue constructs a new Value instance validated to be encodable by encoding/gob. It performs a test gob encode of the value to ensure that Clone (which uses gob) will succeed. Returns an error if the value (or nested types) are not encodable by gob.
For types that are not encodable by encoding/gob, use NewValueWithCloner to provide a custom cloner.
func NewValueWithCloner ¶ added in v0.8.0
NewValueWithCloner constructs a new Value instance with a custom cloner function. The provided clonerFunc is used to create a deep copy of the value when Clone is called. If clonerFunc is nil, the default gob-based cloning will be used.
type ValueOption ¶ added in v0.8.0
type ValueOption[T any] func(*RuntimeValue[T]) error
ValueOption is a functional option used to configure a RuntimeValue.
func WithEffectiveFunc ¶ added in v0.8.0
func WithEffectiveFunc[T any](f EffectiveFunc[T]) ValueOption[T]
WithEffectiveFunc returns a ValueOption that sets the effective function which will be invoked by Effective.
Note: these option setters mutate fields without internal locking and are intended for construction-time configuration only. For runtime-safe updates use the SetEffectiveFunc method on RuntimeValue.
func WithUserInput ¶ added in v0.8.0
func WithUserInput[T any](userInput Value[T]) ValueOption[T]
WithUserInput returns a ValueOption that sets a user input Value to be used as the effective value (unless an effectiveFunc is provided).
Note: see WithEffectiveFunc regarding runtime mutation.
type Workflow ¶
type Workflow interface {
Step
// Steps returns the ordered list of child Steps owned by this workflow.
// The list is the sequence in which steps are executed and (in reverse)
// rolled back.
Steps() []Step
}
Workflow is a Step that owns and drives an ordered list of child Steps.
A workflow satisfies the Step interface, which means workflows can be nested: a parent workflow may include a child workflow as one of its steps. The child receives a deep clone of the parent's global state so that its mutations do not propagate back to the parent unless the parent explicitly reads them after execution.
Use NewWorkflowBuilder to construct a Workflow.
type WorkflowBuilder ¶ added in v0.5.0
type WorkflowBuilder struct {
// contains filtered or unexported fields
}
WorkflowBuilder is a fluent builder for constructing Workflow instances.
Call NewWorkflowBuilder to obtain an instance, configure it with the With* methods, add steps with WorkflowBuilder.Steps or WorkflowBuilder.NamedSteps, and finalise it with WorkflowBuilder.Build:
wf, err := automa.NewWorkflowBuilder().
WithId("deploy").
WithExecutionMode(automa.RollbackOnError).
Steps(
automa.NewStepBuilder().WithId("step-a").WithExecute(execA),
automa.NewStepBuilder().WithId("step-b").WithExecute(execB),
).
Build()
Step ordering is preserved in the order of Steps/NamedSteps calls. Duplicate step IDs are silently ignored (the first registration wins).
Build resets the builder's internal [workflow] so the same WorkflowBuilder instance can immediately be used to construct a second, independent workflow.
WorkflowBuilder is not safe for concurrent use.
func NewWorkflowBuilder ¶ added in v0.5.0
func NewWorkflowBuilder() *WorkflowBuilder
NewWorkflowBuilder returns a new, empty WorkflowBuilder with sensible defaults (StopOnError execution, ContinueOnError rollback, state preservation enabled). Configure it with the With* methods and call WorkflowBuilder.Build when ready.
func (*WorkflowBuilder) Build ¶ added in v0.5.0
func (wb *WorkflowBuilder) Build() (Step, error)
Build validates the builder, constructs all registered steps in order, wires them into the workflow, and returns the finished Step (a *workflow).
Nested workflow propagation: if any registered step produces a *workflow (i.e. is itself a sub-workflow), its executionMode and rollbackMode are overwritten with the parent's modes so that all nested workflows follow the same error-handling and rollback strategy as the enclosing workflow — unless those modes have been explicitly set on the nested builder beforehand, in which case they are still overwritten. Use WorkflowBuilder.WithExecutionMode and WorkflowBuilder.WithRollbackMode on each nested builder before adding it if independent modes are required.
After Build returns the internal workflow is reset to a fresh default so the builder can be reused.
Returns an error if [Validate] fails or if any step's Build fails.
func (*WorkflowBuilder) Id ¶ added in v0.5.0
func (wb *WorkflowBuilder) Id() string
Id returns the workflow ID that has been configured so far. This satisfies the Builder interface.
func (*WorkflowBuilder) NamedSteps ¶ added in v0.5.0
func (wb *WorkflowBuilder) NamedSteps(stepIds ...string) *WorkflowBuilder
NamedSteps looks up step IDs in the configured Registry and registers the matching Builder instances. IDs that are not found in the registry or that are already registered are silently skipped. The relative order of the provided IDs is preserved.
NamedSteps is a no-op when no registry has been configured via WorkflowBuilder.WithRegistry or when stepIds is empty.
func (*WorkflowBuilder) Steps ¶ added in v0.5.0
func (wb *WorkflowBuilder) Steps(steps ...Builder) *WorkflowBuilder
Steps registers one or more Builder instances in the order they are provided. Each builder is keyed by its ID; if a builder with the same ID has already been registered, the duplicate is silently ignored (first-wins).
Steps can be called multiple times to append to the step sequence:
wb.Steps(a, b).Steps(c) // order: a, b, c
func (*WorkflowBuilder) Validate ¶ added in v0.5.0
func (wb *WorkflowBuilder) Validate() error
Validate checks that the workflow under construction is complete and consistent:
- The workflow ID must be non-empty.
- At least one step must be registered.
- Every registered step's Builder.Validate must pass.
Validate is called automatically by [Build]. It can also be called explicitly to surface configuration errors early.
func (*WorkflowBuilder) WithAsyncCallbacks ¶ added in v0.5.0
func (wb *WorkflowBuilder) WithAsyncCallbacks(enable bool) *WorkflowBuilder
WithAsyncCallbacks controls whether the onCompletion and onFailure hooks are invoked synchronously (false, the default) or in a new goroutine (true). When async is enabled the callback receives a deep clone of the Report so that mutations in the goroutine cannot race with the workflow.
func (*WorkflowBuilder) WithExecutionMode ¶ added in v0.7.0
func (wb *WorkflowBuilder) WithExecutionMode(mode TypeMode) *WorkflowBuilder
WithExecutionMode sets the TypeMode that controls how the workflow reacts when a step fails during execution:
- StopOnError (default) — stop immediately; no rollback.
- ContinueOnError — continue executing remaining steps; no rollback.
- RollbackOnError — stop and roll back all previously executed steps in reverse order.
The same mode is propagated to any nested sub-workflows at Build time unless those sub-workflows set their own mode explicitly.
func (*WorkflowBuilder) WithId ¶ added in v0.5.0
func (wb *WorkflowBuilder) WithId(id string) *WorkflowBuilder
WithId sets the unique identifier for the workflow being built. The ID must be non-empty; [Validate] returns an error otherwise.
func (*WorkflowBuilder) WithLogger ¶ added in v0.5.0
func (wb *WorkflowBuilder) WithLogger(logger *slog.Logger) *WorkflowBuilder
WithLogger attaches a slog.Logger to the workflow. The logger is passed to the workflow's internal execution and rollback loops for structured diagnostics. A nil logger is ignored, leaving the silent default in place.
To route automa's slog output through zerolog + lumberjack, use logx:
wb.WithLogger(slog.New(logx.NewSlogHandler()))
func (*WorkflowBuilder) WithOnCompletion ¶ added in v0.5.0
func (wb *WorkflowBuilder) WithOnCompletion(f OnCompletionFunc) *WorkflowBuilder
WithOnCompletion sets a callback that is invoked after the workflow completes successfully. The callback receives the execution context, the workflow as a Step, and the final Report. When [WithAsyncCallbacks] is enabled the callback runs in a new goroutine with a deep-cloned report.
func (*WorkflowBuilder) WithOnFailure ¶ added in v0.5.0
func (wb *WorkflowBuilder) WithOnFailure(f OnFailureFunc) *WorkflowBuilder
WithOnFailure sets a callback that is invoked after the workflow fails (i.e. the final report status is Failed). The callback receives the execution context, the workflow as a Step, and the failure Report. When [WithAsyncCallbacks] is enabled the callback runs in a new goroutine with a deep-cloned report.
func (*WorkflowBuilder) WithPrepare ¶ added in v0.5.0
func (wb *WorkflowBuilder) WithPrepare(prepareFunc PrepareFunc) *WorkflowBuilder
WithPrepare sets an optional PrepareFunc that is called once before the workflow's step loop begins. Use it to enrich the context (e.g. attach a trace span) or initialise global state that all steps will share.
func (*WorkflowBuilder) WithRegistry ¶ added in v0.5.0
func (wb *WorkflowBuilder) WithRegistry(sr Registry) *WorkflowBuilder
WithRegistry attaches a Registry that [NamedSteps] uses to resolve step IDs to Builder instances. Optional — omit if all steps are provided directly via WorkflowBuilder.Steps.
func (*WorkflowBuilder) WithRollback ¶ added in v0.6.0
func (wb *WorkflowBuilder) WithRollback(rollback RollbackFunc) *WorkflowBuilder
WithRollback sets an optional user-defined RollbackFunc for the workflow itself. This is called in addition to (or instead of, depending on configuration) the individual step rollbacks. Use it for workflow-level cleanup that does not belong to any single step.
func (*WorkflowBuilder) WithRollbackMode ¶ added in v0.5.0
func (wb *WorkflowBuilder) WithRollbackMode(mode TypeMode) *WorkflowBuilder
WithRollbackMode sets the TypeMode that controls how the workflow reacts when a step's RollbackFunc fails during rollback:
- StopOnError — stop rolling back further steps on the first rollback failure.
- ContinueOnError (default) — continue rolling back remaining steps even if an earlier rollback fails.
This mode is only relevant when WithExecutionMode is RollbackOnError. The same mode is propagated to nested sub-workflows at Build time.
func (*WorkflowBuilder) WithState ¶ added in v0.5.0
func (wb *WorkflowBuilder) WithState(state NamespacedStateBag) *WorkflowBuilder
WithState pre-populates the workflow's NamespacedStateBag. The global namespace of this bag is shared with all steps during execution; each step additionally receives its own private local namespace.
This is optional — if not set, the workflow lazily initialises an empty bag on first access.
func (*WorkflowBuilder) WithStatePreservation ¶ added in v0.9.0
func (wb *WorkflowBuilder) WithStatePreservation(enable bool) *WorkflowBuilder
WithStatePreservation controls whether per-step state snapshots are cloned and stored for use during rollback (default: true).
When true (default):
- After each step executes successfully, its NamespacedStateBag is deep-cloned and stored keyed by step ID.
- During rollback each step receives the snapshot taken at its execution time, so local namespaces and the exact global state at that moment are available.
- Higher memory usage due to deep cloning after every step.
When false:
- No cloning or storage occurs after execution.
- During rollback each step receives the workflow's current State() (global namespace only); per-step local namespaces from execution time are lost.
- Lower memory overhead.
Set to false when:
- Rollback functions are idempotent and only need global state.
- Rollback reads from external sources (database, files, APIs) rather than the in-memory state bag.
- The execution mode is StopOnError or ContinueOnError so rollback is never triggered.