Documentation
¶
Overview ¶
Package engine implements a Temporal-based graph interpreter workflow engine.
Index ¶
- Constants
- Variables
- func EvaluateCondition(condition string, context map[string]any) (bool, error)
- func FormatChildWorkflowID(caseID, parentWorkflowID, nodeID, branchID string) string
- func ValidateBatchGateways(def WorkflowDefinition) error
- func ValidateParallelGateways(def WorkflowDefinition) error
- type ActivationRef
- type Activities
- func (a *Activities) AdminParkActivity(_ context.Context, payload AdminParkPayload) error
- func (a *Activities) ExecuteTaskActivity(ctx context.Context, taskTemplateID string, inputs map[string]any, ...) (map[string]any, error)
- func (a *Activities) FetchWorkflowDefinitionActivity(_ context.Context, templateID string) (WorkflowDefinition, error)
- func (a *Activities) WorkflowCompletedActivity(_ context.Context, workflowID string, finalContext map[string]any, seq int64) error
- type AdminInterventionResolver
- type AdminParkHandler
- type AdminParkPayload
- type AdminResolutionAction
- type AdminResolutionSignal
- type BatchGatewayConfig
- type BatchJoinConfig
- type BroadcastMessage
- type Edge
- type ExecutionStatus
- type FailureMode
- type GatewayType
- type Manager
- type Node
- type NodeInfo
- type NodeStatus
- type NodeType
- type ParallelJoinConfig
- type ParkCategory
- type SignalingConfig
- type SignalingType
- type SplitMode
- type SplitTaskConfig
- type SplitTaskItem
- type TaskActivationHandler
- type TaskPayload
- type TemporalManager
- type TimerConfig
- type UpdateEvent
- type WorkflowCompletion
- type WorkflowCompletionHandler
- type WorkflowDefinition
- type WorkflowInstance
Constants ¶
const ( // DefaultIterationKey is the default variable name injected into a child workflow's // state containing iteration details (e.g., _iter.index, _iter.branch_id, _iter.input). DefaultIterationKey = "_iter" // ChildBroadcastSignalName is the base name of the Temporal signal used to route cross-branch // signals from a child workflow back up to the parent for brokerage to other sibling branches. // It is scoped per SPLIT_TASK node via childBroadcastSignalName so that two SPLIT_TASK nodes // running concurrently in the same workflow execution (e.g. under a PARALLEL_SPLIT gateway) // don't share a channel and cross-deliver each other's broadcasts. ChildBroadcastSignalName = "child_broadcast_signal" // Keys injected into the child's workspace variables // VarSplitNodeID identifies the ID of the SplitTask node in the parent workflow. VarSplitNodeID = "_split_node_id" // VarParentWorkflowID contains the workflow ID of the parent/orchestrator workflow. VarParentWorkflowID = "_parent_workflow_id" // VarCaseID holds the case a workflow belongs to: the key the task store groups tasks by // (store.TaskRecord.CaseID) and the prefix of every child workflow ID. A host that runs // several top-level workflows for one real-world matter (a consignment, an agency case) // starts each with the same VarCaseID; a top-level workflow started without one is its own // case, under its own workflow ID. It is carried unchanged through every level of nesting, // so a descendant at any depth can read it directly without walking a parent-to-parent // chain. VarCaseID = "_case_id" // VarBranchID contains the unique branch ID assigned to the specific child workflow branch. VarBranchID = "_branch_id" // Iteration context sub-keys (e.g., used to access _iter.index, _iter.branch_id, _iter.input) // IterIndexKey is the sub-key for the 0-based index of this branch within the items array. IterIndexKey = "index" // IterBranchIDKey is the sub-key for the unique branch identifier. IterBranchIDKey = "branch_id" // IterInputKey is the sub-key pointing to the input payload mapped to this branch. IterInputKey = "input" )
Core structural execution constants
const ( DefaultItemsVariable = "_items" DefaultIDField = "id" )
Default values for BatchGatewayConfig / BatchJoinConfig fields.
const ( // DefaultMaxBatchDepth is the maximum nesting depth of BATCH_SPLIT gateways. DefaultMaxBatchDepth = 4 // DefaultMaxChildrenPerGateway is the maximum number of partitions a single // BATCH_SPLIT gateway may spawn. DefaultMaxChildrenPerGateway = 20 )
Engine-level guardrail defaults for batch gateways.
const AdminResolutionSignalName = "AdminResolutionSignal"
AdminResolutionSignalName is the Temporal signal name used to deliver AdminResolutionSignal.
const VarScopePath = "_scope_path"
VarScopePath is the workflow variable key holding the hierarchical scope path string for batch gateway nesting (e.g. "root/gw_type/lab/gw_result/fail").
Variables ¶
var ErrActivationNotPending = errors.New("step is not pending: it already completed or does not exist")
ErrActivationNotPending is returned by Manager.CompleteActivation when the addressed step is not waiting to be completed: it already completed, or the workflow ended, or no such step ever existed. Nothing was changed. It is how a caller learns its call was late or a duplicate, for example one for an earlier run of a node the workflow has since moved past. Check for it with errors.Is.
var ErrWorkflowNotFound = errors.New("workflow execution not found")
ErrWorkflowNotFound is returned by Manager.GetStatus when the backing engine has no queryable execution for the given ID. This covers an ID that was never used, but also one whose execution is no longer queryable for any other reason (e.g. its history has aged out of the engine's retention) — a Manager implementation cannot always tell the two apart. Callers should check for ErrWorkflowNotFound with errors.Is rather than inspecting the underlying runtime's error types, so they stay agnostic to whatever engine backs the Manager.
Functions ¶
func EvaluateCondition ¶
EvaluateCondition parses and runs a string expression against the global context.
func FormatChildWorkflowID ¶
FormatChildWorkflowID constructs a deterministic child workflow ID from the case ID, the parent's own workflow ID, the gateway node ID, and the branch ID.
The result is always "<root>--<16-hex-char hash>": each level's hash folds in the parent's full workflow ID, not just this level's nodeID/branchID, so the ID uniquely commits to the full ancestor chain no matter how deep the nesting goes. Fixed-width output keeps the ID within Temporal's varchar(255) current_executions.workflow_id column regardless of branch depth — an oversized case ID is capped via boundPrefixForChildID first, so the guarantee holds independent of how long the caller's case ID is. Two workflows of the same case (a host's several top-level workflows) share the prefix; the hash, which folds in the parent's workflow ID, keeps their children apart.
caseID is taken as given rather than parsed out of parentWorkflowID — callers already have it via VarCaseID, which the engine propagates explicitly to every level of nesting.
func ValidateBatchGateways ¶
func ValidateBatchGateways(def WorkflowDefinition) error
ValidateBatchGateways checks structural invariants and topological containment for BATCH_SPLIT / BATCH_JOIN pairs in the workflow definition. Call this at parse-time or at the start of GraphInterpreterWorkflow before execution begins.
func ValidateParallelGateways ¶
func ValidateParallelGateways(def WorkflowDefinition) error
ValidateParallelGateways checks structural invariants and topological containment for PARALLEL_SPLIT / PARALLEL_JOIN pairs in the workflow definition, mirroring ValidateBatchGateways for BATCH_SPLIT / BATCH_JOIN. Call this at parse-time or at the start of GraphInterpreterWorkflow before execution begins.
Types ¶
type ActivationRef ¶
type ActivationRef struct {
// NodeID is the node's ID in the workflow definition (Node.ID).
NodeID string `json:"node_id"`
// Seq is the workflow-wide step counter value for this run.
Seq int64 `json:"seq"`
}
ActivationRef is what the workflow tells ExecuteTaskActivity about the run it is starting, beyond what the Activity's own info carries.
type Activities ¶
type Activities struct {
// ExecuteTaskActivityHandler is invoked when the workflow engine encounters a task node.
// - For synchronous execution, it should return a nil error with a map containing the results.
// - For asynchronous execution, it should return a nil map and an ErrResultPending error,
// which pauses the workflow activity until an external handler triggers CompleteActivation.
ExecuteTaskActivityHandler func(TaskPayload) (map[string]any, error)
// WorkflowCompletedActivityHandler is invoked when the overall workflow execution succeeds and reaches
// an End node. It receives the workflow ID and the final accumulated workflow variables, allowing the
// host application to run any necessary completion triggers, notify listeners, or persist final state.
WorkflowCompletedActivityHandler func(WorkflowCompletion) error
// FetchWorkflowDefinitionHandler is invoked to dynamically retrieve the workflow definition structure
// for a given template ID during SPLIT_TASK execution.
FetchWorkflowDefinitionHandler func(templateID string) (WorkflowDefinition, error)
// AdminParkHandler is invoked whenever a node parks for admin intervention. See the
// AdminParkHandler type doc for what a nil handler means.
AdminParkHandler AdminParkHandler
}
Activities encapsulates the Temporal activity implementations utilized by the workflow engine. It maps the activity execution flow to custom callback handlers provided by the host application.
func (*Activities) AdminParkActivity ¶
func (a *Activities) AdminParkActivity(_ context.Context, payload AdminParkPayload) error
AdminParkActivity is a Temporal activity that notifies the host application whenever a node parks for admin intervention. It is a no-op if no AdminParkHandler is registered — unlike the other handlers here, notification is optional, so an unset handler is not an error.
func (*Activities) ExecuteTaskActivity ¶
func (a *Activities) ExecuteTaskActivity(ctx context.Context, taskTemplateID string, inputs map[string]any, caseID string, step ActivationRef) (map[string]any, error)
ExecuteTaskActivity pushes the task to your application and sleeps waiting for it or completes synchronously
func (*Activities) FetchWorkflowDefinitionActivity ¶
func (a *Activities) FetchWorkflowDefinitionActivity(_ context.Context, templateID string) (WorkflowDefinition, error)
FetchWorkflowDefinitionActivity is a Temporal activity that retrieves the workflow definition for a template ID.
func (*Activities) WorkflowCompletedActivity ¶
func (a *Activities) WorkflowCompletedActivity(_ context.Context, workflowID string, finalContext map[string]any, seq int64) error
WorkflowCompletedActivity is a Temporal activity that executes when a workflow completes successfully.
type AdminInterventionResolver ¶
type AdminInterventionResolver interface {
// ResolveAdminIntervention sends an admin's resolution decision to a node that is
// currently parked in NodeStatusAwaitingAdmin. workflowID/runID address the execution, and
// resolution.ActivationID picks the parking within it: it must echo AdminParkPayload.ActivationID
// (or NodeInfo.ActivationID from GetStatus) for the current park, or the signal is dropped.
// resolution.NodeID is for display only. A nil return means the signal was delivered, not that
// it matched a parked node.
ResolveAdminIntervention(ctx context.Context, workflowID, runID string, resolution AdminResolutionSignal) error
}
AdminInterventionResolver is implemented by managers that support resolving a node parked in NodeStatusAwaitingAdmin. It is kept separate from Manager so that existing Manager implementations (e.g. test doubles) aren't forced to implement it. Callers that want this capability can type-assert: if r, ok := mgr.(AdminInterventionResolver); ok { ... }.
type AdminParkHandler ¶
type AdminParkHandler func(AdminParkPayload) error
AdminParkHandler is invoked once every time a node parks in NodeStatusAwaitingAdmin — including a re-park after a failed AdminActionRetry, since that's newly actionable information. It hands the host application full control over how to surface the event (log, metric, DB write, page, etc.); the engine itself does not prescribe a mechanism. Registering one is optional — a nil handler (the default) means the engine takes no notification action at all, and parking still proceeds correctly either way. A handler error is recorded on the workflow's own AuditTrail but never blocks the park or the admin resolution flow.
type AdminParkPayload ¶
type AdminParkPayload struct {
// WorkflowID is the unique identifier for the workflow execution that parked.
WorkflowID string
// RunID is the unique identifier for this specific execution attempt.
RunID string
// CaseID is the case the parked workflow belongs to (see VarCaseID).
CaseID string
// NodeID is the ID of the graph node that parked. It names the definition, not this one run of
// it; see ActivationID for a run.
NodeID string
// ActivationID identifies this specific run of the node — the same value NodeInfo.ActivationID
// held at the moment it parked. A resolution must echo it back (AdminResolutionSignal.ActivationID)
// to be matched to this parking rather than some other one, including an earlier parking of the
// same NodeID (a retry or loop revisit mints a new ActivationID each time it parks).
ActivationID string
// NodeType is the type of the node that parked (TASK, GATEWAY, etc.).
NodeType string
// TaskTemplateID identifies the task template the parked node was running. Empty for
// non-TASK nodes (e.g. a GATEWAY parked on "no matching conditions").
TaskTemplateID string
// Cause is the error message that caused the node to park (NodeInfo.LastError).
Cause string
// ParkCategory says why the node parked (see NodeInfo.ParkCategory).
ParkCategory ParkCategory
// InputMapping and OutputMapping are the node's mappings from its definition, carried over
// from NodeInfo.InputMapping/OutputMapping so a handler can report which workflow variable a
// RETRY reads and which a COMPLETE patch should write, without querying the workflow itself.
InputMapping map[string]string
OutputMapping map[string]string
// CachedTaskResult holds the Activity's raw result if it had already completed before the
// node parked (see NodeInfo.CachedTaskResult) — nil if execution never reached that point.
CachedTaskResult map[string]any
}
AdminParkPayload carries the context of a node parking in NodeStatusAwaitingAdmin, delivered to an AdminParkHandler so the host application can decide how to surface the event — log it, emit a metric, persist it, page someone, etc. The engine itself takes no position on that; see AdminParkHandler.
type AdminResolutionAction ¶
type AdminResolutionAction string
AdminResolutionAction describes how an admin chooses to resolve a node that is parked in NodeStatusAwaitingAdmin.
const ( // AdminActionRetry applies WorkflowVariablesPatch, then re-runs the node's handler from // scratch. It is the admin's responsibility to ensure this is safe — e.g. a TASK node with a // populated CachedTaskResult has already run its Activity, so retrying re-invokes it. AdminActionRetry AdminResolutionAction = "RETRY" // AdminActionComplete applies WorkflowVariablesPatch and marks the node completed without // running it (or re-running it): the patch stands in for the node's output. An empty patch // just moves past the node. Always safe, but not allowed for GATEWAY nodes. AdminActionComplete AdminResolutionAction = "COMPLETE" // AdminActionAbort fails the node and the workflow with the original error. AdminActionAbort AdminResolutionAction = "ABORT" )
Admin resolution actions.
type AdminResolutionSignal ¶
type AdminResolutionSignal struct {
// NodeID is the template node ID (Node.ID) of the parked node, for display/logging. It is not
// the routing key: ActivationID is.
NodeID string `json:"nodeID"`
// ActivationID is the routing key: it must match the ActivationID the engine sent in
// AdminParkPayload for the parking being resolved. A signal whose ActivationID doesn't match any
// currently-parked node is dropped — including one echoing an earlier parking of the same
// NodeID, since a retry or loop revisit mints a new ActivationID each time it parks. Its JSON
// name, step_id, matches NodeInfo.ActivationID's, so a value read from GetStatus is sent back
// under the same name.
ActivationID string `json:"step_id"`
// Action determines how the node is resolved. See AdminResolutionAction constants.
Action AdminResolutionAction `json:"action"`
// WorkflowVariablesPatch sets workflow variables before the action takes effect, for
// AdminActionRetry and AdminActionComplete. Keys are dotted paths (e.g. "review.outcome"),
// applied in sorted order, and each value is written the way a task's output mapping writes
// one: a map value is merged into an existing map at that path (fields it doesn't name are
// kept), and any other value replaces what is there. It is a patch, not the full variable set
// — variables not named here are untouched. It is not an RFC 6902 JSON Patch, and a nil value
// sets the path to nil rather than deleting it. To drop a map's other fields, send it as nil
// alongside the fields to keep: {"review": nil, "review.outcome": true}. The variables are
// workflow-wide and persist, so they affect every later node too.
WorkflowVariablesPatch map[string]any `json:"workflow_variables_patch,omitempty"`
// Reason is a free-text admin justification, appended to the workflow's AuditTrail.
Reason string `json:"reason,omitempty"`
}
AdminResolutionSignal is sent by an external admin tool to resolve a node that is parked in NodeStatusAwaitingAdmin.
type BatchGatewayConfig ¶
type BatchGatewayConfig struct {
// ItemsVariable is the dot-path in WorkflowVariables pointing to the item slice.
// Defaults to "_items" if empty.
ItemsVariable string `json:"items_variable,omitempty"`
// IDField is the field name within each item used as the unique identifier for
// ID-based merging at the paired BATCH_JOIN. Defaults to "id" if empty.
IDField string `json:"id_field,omitempty"`
}
BatchGatewayConfig configures item-level partitioning for a BATCH_SPLIT gateway node.
type BatchJoinConfig ¶
type BatchJoinConfig struct {
// GatewayNodeID is the node ID of the paired BATCH_SPLIT gateway.
GatewayNodeID string `json:"gateway_node_id"`
// ItemsVariable is the dot-path in WorkflowVariables pointing to the item slice.
// Must match the paired BATCH_SPLIT's items_variable. Defaults to "_items" if empty.
ItemsVariable string `json:"items_variable,omitempty"`
// IDField is the field name within each item used as the unique identifier.
// Must match the paired BATCH_SPLIT's id_field. Defaults to "id" if empty.
IDField string `json:"id_field,omitempty"`
}
BatchJoinConfig configures how a BATCH_JOIN gateway merges child partition results.
type BroadcastMessage ¶
type BroadcastMessage struct {
SenderBranchID string `json:"sender_branch_id"`
SignalName string `json:"signal_name"`
Payload map[string]any `json:"payload"`
}
BroadcastMessage defines a unified Message Wrapper for parent brokerage.
type Edge ¶
type Edge struct {
ID string `json:"id"`
SourceID string `json:"source_id"`
TargetID string `json:"target_id"`
Condition string `json:"condition,omitempty"` // Expression mapped against WorkflowVariables
}
Edge represents a directed connection between two nodes.
type ExecutionStatus ¶
type ExecutionStatus string
ExecutionStatus defines the allowed states for a workflow instance.
const ( StatusRunning ExecutionStatus = "RUNNING" StatusCompleted ExecutionStatus = "COMPLETED" StatusFailed ExecutionStatus = "FAILED" )
Execution status constants for workflows.
type FailureMode ¶
type FailureMode string
FailureMode represents the failure handling strategy for split task executions.
const ( FailureModeFailFast FailureMode = "FAIL_FAST" FailureModeCollectAll FailureMode = "COLLECT_ALL" )
Core split task failure handling modes.
type GatewayType ¶
type GatewayType string
GatewayType represents the type of a gateway controlling execution flow.
const ( GatewayTypeExclusiveSplit GatewayType = "EXCLUSIVE_SPLIT" // XOR Split GatewayTypeParallelSplit GatewayType = "PARALLEL_SPLIT" // AND Split GatewayTypeExclusiveJoin GatewayType = "EXCLUSIVE_JOIN" // XOR Join GatewayTypeParallelJoin GatewayType = "PARALLEL_JOIN" // AND Join GatewayTypeBatchSplit GatewayType = "BATCH_SPLIT" // Item-partitioning Split GatewayTypeBatchJoin GatewayType = "BATCH_JOIN" // Item-merging Join )
Gateway types controlling branching and merging.
type Manager ¶
type Manager interface {
// StartWorkflow starts a workflow using the provided ID.
// The WorkflowDefinition defines the structure of the workflow graph (nodes and edges).
// initialWorkflowVariables sets the starting state for the graph's
// data payload. Returns an error if submission fails.
StartWorkflow(ctx context.Context, ID string, def WorkflowDefinition, initialWorkflowVariables map[string]any) error
// CompleteActivation is called by the external system to resume a paused workflow node.
// It routes the output data back into the specific workflow's WorkflowVariables using the
// workflowID and activationID (TaskPayload.ActivationID) that were originally emitted via the
// TaskActivationHandler. runID may be empty to address the workflow's current run. It fails if
// that step is not pending, so a call for a step that has already completed, or that never
// existed, is rejected rather than applied to whichever step is active now.
CompleteActivation(ctx context.Context, workflowID, runID, activationID string, output map[string]any) error
// TaskUpdate is used to send an update about the task to the workflow.
// This is typically used to append messages to the workflow's internal state or update
// UI with hints, progress updates, or audit trail messages.
// It does not advance the graph's execution state.
TaskUpdate(ctx context.Context, workflowID, runID string, update UpdateEvent) error
// GetStatus retrieves a running workflow's in-memory state (the WorkflowInstance), including
// current variables, and audit trails. Returns ErrWorkflowNotFound if there is no queryable
// current execution for workflowID — see ErrWorkflowNotFound's doc for what that covers.
GetStatus(ctx context.Context, workflowID string) (*WorkflowInstance, error)
}
Manager acts as the bridge between the external host application and the underlying execution engine. It handles workflow lifecycles, external task routing, and state queries.
type Node ¶
type Node struct {
ID string `json:"id"`
Type NodeType `json:"type"` // See NodeType constants
GatewayType GatewayType `json:"gateway_type,omitempty"` // See Gateway Types constants
TaskTemplateID string `json:"task_template_id,omitempty"` // Identifier for the task template to run
InputMapping map[string]string `json:"input_mapping,omitempty"` // Maps WorkflowVariables Key -> Task Input Key
OutputMapping map[string]string `json:"output_mapping,omitempty"` // Maps Task Output Key -> WorkflowVariables Key
// Extensions
SplitTask *SplitTaskConfig `json:"split_task,omitempty"`
Timer *TimerConfig `json:"timer,omitempty"`
Signaling *SignalingConfig `json:"signaling,omitempty"`
BatchGateway *BatchGatewayConfig `json:"batch_gateway,omitempty"`
BatchJoin *BatchJoinConfig `json:"batch_join,omitempty"`
ParallelJoin *ParallelJoinConfig `json:"parallel_join,omitempty"`
}
Node represents a step in the workflow graph.
type NodeInfo ¶
type NodeInfo struct {
// ID is the node's ID in the workflow definition (Node.ID). It names the definition, not one
// run of it; see ActivationID for a run.
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"` // Timestamp of node creation
UpdatedAt time.Time `json:"updatedAt"` // Timestamp of last node update
Type NodeType `json:"type"` // Type of the node.
GatewayType GatewayType `json:"gateway_type,omitempty"` // See Gateway Types constants
TaskTemplateID string `json:"task_template_id,omitempty"` // Identifier for the task template to run
Status NodeStatus `json:"status"` // Status of the node
// ActivationID and Seq identify the most recent run of this node, of any type (see
// TaskPayload.ActivationID and TaskPayload.Seq, which name the same values for a TASK node
// specifically). Empty and zero until the node first runs. A node that is revisited gets a new
// ActivationID and a higher Seq each time; these always describe the latest run.
ActivationID string `json:"step_id,omitempty"`
Seq int64 `json:"seq,omitempty"`
// LastError holds the error that caused the node to enter NodeStatusAwaitingAdmin
// (or the terminal error if it was ultimately aborted). Cleared on successful resolution.
LastError string `json:"last_error,omitempty"`
// ParkCategory says why the node is parked. Set together with LastError; cleared on resolution.
ParkCategory ParkCategory `json:"park_category,omitempty"`
// InputMapping and OutputMapping are the node's mappings from its definition (workflow variable
// -> task input, task result -> workflow variable), so an admin can see which variables a
// RETRY reads and which a COMPLETE should write. Set together with LastError, only while the
// node is parked (not on every node); cleared on resolution.
InputMapping map[string]string `json:"input_mapping,omitempty"`
OutputMapping map[string]string `json:"output_mapping,omitempty"`
// CachedTaskResult holds the most recent raw Activity result for a TASK node, set right
// after the Activity succeeds and cleared once the node fully completes. It is purely
// informational: if a node parks with this populated, the Activity has already run, so
// an admin should prefer AdminActionComplete over AdminActionRetry to avoid re-running it.
CachedTaskResult map[string]any `json:"cached_task_result,omitempty"`
// ChildWorkflowIDs lists the workflow IDs of any child GraphInterpreterWorkflow executions
// spawned by this node (SPLIT_TASK, BATCH_SPLIT, or PARALLEL_SPLIT). It is set once, at
// spawn time, and is not derived from the children's current status — so an ID remains
// listed here even after that child workflow completes. A caller can query each ID with the
// same GetStatus query used on this workflow to walk the execution tree to arbitrary depth.
ChildWorkflowIDs []string `json:"child_workflow_ids,omitempty"`
}
NodeInfo holds information about the state of one of the nodes in the workflow.
type NodeStatus ¶
type NodeStatus string
NodeStatus represents the status of a specific workflow node.
const ( NodeStatusNotStarted NodeStatus = "NOT_STARTED" NodeStatusRunning NodeStatus = "RUNNING" NodeStatusCompleted NodeStatus = "COMPLETED" NodeStatusFailed NodeStatus = "FAILED" NodeStatusAwaitingAdmin NodeStatus = "AWAITING_ADMIN" )
Node status constants.
type NodeType ¶
type NodeType string
NodeType represents the type of a workflow node (e.g. START, END, TASK, GATEWAY).
type ParallelJoinConfig ¶
type ParallelJoinConfig struct {
// GatewayNodeID is the node ID of the paired PARALLEL_SPLIT gateway.
GatewayNodeID string `json:"gateway_node_id"`
// MergeByID maps a top-level workflow variable name (holding a []map[string]any shared by
// multiple branches) to the field name within each item used as its unique ID.
// Nested dot-paths are not currently supported; see ValidateParallelGateways.
// TODO: Support nested dot-paths (e.g. "order.items") in MergeByID and mergeVariablesInto.
MergeByID map[string]string `json:"merge_by_id,omitempty"`
}
ParallelJoinConfig configures how a PARALLEL_JOIN gateway isolates its branches and merges their final workflow variable states back into the parent scope.
WARNING: CONFLICT RESOLUTION AND LAST-WRITE-WINS (LWW) When concurrent parallel branches write conflicting data, the engine cannot infer intent and applies a deterministic "last-write-wins" policy. Precedence is determined strictly by branch edge order (sorted ascending by source edge ID; later branches in that order overwrite earlier ones), NOT wall-clock completion time.
Specifically, LAST-WRITE-WINS applies in the following three places:
- Conflicting item fields in MergeByID: If two branches modify the SAME field on the SAME item (e.g. branch A sets item["status"]="PASS" and branch B sets item["status"]="FAIL"), the later branch's field value overwrites the earlier one.
- Conflicting map keys: If two branches set the SAME leaf key in a map[string]any, the later branch's value overwrites the earlier one.
- Scalars and unlisted arrays: Any scalar variable (string, number, boolean) or slice not listed in MergeByID that is written by multiple branches will be completely overwritten by the later branch.
Recommendation for workflow authors: Ensure concurrent parallel branches write to distinct variable paths or distinct field names on shared items if overwriting cannot be tolerated.
Execution and Reconciliation Model: Each matching outgoing edge of the paired PARALLEL_SPLIT runs as its own child workflow with a deep-copied, isolated set of WorkflowVariables — no branch can see another branch's writes while any of them are still running. When all branches complete, PARALLEL_JOIN reconciles their (possibly divergent) final states back into one:
- map[string]any values are deep-merged key by key (recursive map merge): each branch's mutated sub-fields combine, so two branches changing different fields of the same object both survive.
- Variables listed in MergeByID (each a []map[string]any) are merged element-by-element by that id_field across ALL branches — not "one branch's array replaces another's": for a given item ID, the fields each branch's copy of that item carries are unioned into one item, so lab writing sample_test_result and visual writing visual_result to the same underlying item both survive regardless of which branch finishes last. A field two branches BOTH carry a (possibly different) value for is a genuine conflict this cannot resolve: branches are applied in a fixed, deterministic order (sorted by source edge ID) and the last one wins for that field — give each branch its own field name if that ambiguity isn't acceptable.
- Any other variable (scalar, or array not listed in MergeByID) falls back to the same fixed deterministic branch order, last one wins.
type ParkCategory ¶
type ParkCategory string
ParkCategory says why a node parked in NodeStatusAwaitingAdmin, so tooling can guide an admin without parsing NodeInfo.LastError.
const ( // ParkCategoryInputMapping: a required global variable for the node's input mapping is missing. ParkCategoryInputMapping ParkCategory = "INPUT_MAPPING" // ParkCategoryOutputMapping: the Activity ran, but its result lacks a required mapped field. ParkCategoryOutputMapping ParkCategory = "OUTPUT_MAPPING" // ParkCategoryTaskFailure: the node's own work (the Activity, a signal send) failed. ParkCategoryTaskFailure ParkCategory = "TASK_FAILURE" // ParkCategoryGatewayCondition: a gateway condition failed to evaluate, or no edge matched. ParkCategoryGatewayCondition ParkCategory = "GATEWAY_CONDITION" // ParkCategorySplitData: the items or branch data a split or join needs is missing or malformed. ParkCategorySplitData ParkCategory = "SPLIT_DATA" // ParkCategoryChildFailure: a spawned child workflow failed. ParkCategoryChildFailure ParkCategory = "CHILD_FAILURE" // ParkCategoryDefinitionError: the workflow definition itself is invalid, so no variable fixes it. ParkCategoryDefinitionError ParkCategory = "DEFINITION_ERROR" // ParkCategoryUnknown: the error was never categorized. ParkCategoryUnknown ParkCategory = "UNKNOWN" )
type SignalingConfig ¶
type SignalingConfig struct {
// Type is either EMIT or WAIT.
Type SignalingType `json:"type"`
// SignalName is the name of the signal channel. Required.
SignalName string `json:"signal_name"`
// Payload holds the data to emit (EMIT nodes only). Ignored for WAIT nodes.
Payload map[string]any `json:"payload,omitempty"`
}
SignalingConfig defines the parameters for a SIGNALING node. SIGNALING nodes let sibling branches spawned by the same SPLIT_TASK node coordinate with each other: one hop up to the parent, one hop back down to that node's other children. They do not bubble further up an ancestor chain or cascade down into a sibling's own nested sub-splits — see the "Signaling Nodes" section in README.md.
type SignalingType ¶
type SignalingType string
SignalingType represents the sub-type of a SIGNALING node.
const ( SignalingTypeEmit SignalingType = "EMIT" // Emits a signal to sibling branches SignalingTypeWait SignalingType = "WAIT" // Blocks until a named signal is received )
Signaling sub-types.
type SplitMode ¶
type SplitMode string
SplitMode represents the split task dynamic fan-out mode.
const ( // SplitModeSameTemplate spawns one child workflow per item running the same external template. // // Deprecated: Prefer using BATCH_SPLIT gateway nodes instead. BATCH_SPLIT defines sub-graphs // inline, supports conditional partitioning, aggregates tasks over item slices, and merges results // by item ID without requiring external template registration. SplitModeSameTemplate SplitMode = "SAME_TEMPLATE" // SplitModeDifferentTemplates spawns child workflows dynamically based on each item's template_id. SplitModeDifferentTemplates SplitMode = "DIFFERENT_TEMPLATES" )
Core split task execution modes.
type SplitTaskConfig ¶
type SplitTaskConfig struct {
Mode SplitMode `json:"mode"` // SAME_TEMPLATE or DIFFERENT_TEMPLATES
ItemsVariable string `json:"items_variable"` // Global context variable dot-path pointing to []map[string]any
ResultsVariable string `json:"results_variable,omitempty"` // Destination path to save aggregated sub-workflow outputs
FailureMode FailureMode `json:"failure_mode"` // FAIL_FAST or COLLECT_ALL
IterationKey string `json:"iteration_key,omitempty"` // Override key for sub-context namespace. Defaults to "_iter"
}
SplitTaskConfig defines dynamic fan-out execution configuration.
type SplitTaskItem ¶
type SplitTaskItem struct {
TemplateID string `json:"template_id"`
BranchID string `json:"branch_id"`
Payload map[string]any `json:"payload"`
}
SplitTaskItem defines the structure for individual branch items inside the items collection.
func ParseSplitTaskItem ¶
func ParseSplitTaskItem(itemRaw any) (SplitTaskItem, error)
ParseSplitTaskItem parses a raw interface item into a SplitTaskItem.
type TaskActivationHandler ¶
type TaskActivationHandler func(payload TaskPayload) (map[string]any, error)
TaskActivationHandler is invoked by the engine whenever the workflow reaches a "TASK" node. The handler must support two execution paths:
1. Synchronous Execution:
- If the work completes immediately, return a nil error and a map containing the output variables.
- The workflow immediately consumes these outputs and proceeds to the next node.
2. Asynchronous Execution:
- If the work is long-running (e.g. awaits external API callback, human UI interaction, etc.), return a nil map and an ErrResultPending error.
- The workflow activity pauses and awaits completion. The host application must eventually resume it by calling Manager.CompleteActivation() with the matching workflow, run, and step IDs.
type TaskPayload ¶
type TaskPayload struct {
// WorkflowID is the unique identifier for the overall business process instance.
WorkflowID string
// RunID is the unique identifier for this specific execution attempt.
RunID string
// NodeID is the ID of the graph node in the workflow definition (Node.ID). It names the
// definition, not one run: a node revisited by a loop has the same NodeID each time. Use
// ActivationID to address one run.
NodeID string
// ActivationID identifies this run of the node. It is unique within the workflow, is the ID of the
// Temporal Activity, and is what Manager.CompleteActivation takes to complete this run. It is derived
// deterministically from the workflow ID, NodeID and Seq (a UUIDv5), so it is a valid UUID.
ActivationID string
// Seq is the workflow-wide step counter value for this run. Every node run (not just TASK)
// consumes the next value, so a TASK node's Seq need not be contiguous with the previous TASK
// node's, but it never repeats or goes backwards. Hosts use it to order writes so a late write
// from an earlier step can be told apart from the current one.
Seq int64
// TaskTemplateID identifies the specific type of external work/script the task executor should run.
TaskTemplateID string
// Inputs contains the specific subset of WorkflowVariables mapped to this task's requirements.
Inputs map[string]any
// CaseID is the case the workflow belongs to (see VarCaseID). It is the same at every level
// of nesting, so it names the case of a task in a deeply nested child, which WorkflowID
// alone does not.
CaseID string
}
TaskPayload represents the contextual data sent to the task executor when the workflow engine reaches a "TASK" node. It contains the necessary coordinates for the task executor to identify the work and eventually report back.
type TemporalManager ¶
type TemporalManager interface {
Manager
// RegisterDefinitionHandler registers the handler function for fetching sub-workflow definitions.
RegisterDefinitionHandler(handler func(templateID string) (WorkflowDefinition, error))
// RegisterAdminParkHandler registers the handler invoked whenever a node parks for admin
// intervention. Optional — see AdminParkHandler's doc for what a nil/unregistered handler
// means.
RegisterAdminParkHandler(handler AdminParkHandler)
// StartWorker connects the internal Temporal Worker to the Temporal Server and
// begins polling the task queue for workflow and activity tasks.
StartWorker() error
// StopWorker gracefully shuts down the internal Temporal Worker, stopping it from
// pulling new tasks while allowing currently executing tasks to finish.
StopWorker()
}
TemporalManager extends the Manager interface with worker control methods.
func NewTemporalManager ¶
func NewTemporalManager( c client.Client, namespace string, taskQueue string, taskHandler TaskActivationHandler, completionHandler WorkflowCompletionHandler) TemporalManager
NewTemporalManager creates a new instance of TemporalManager.
namespace must be the namespace c was dialed with. client.Client does not expose it, and some calls (e.g. CompleteActivityByID) require it explicitly.
type TimerConfig ¶
type TimerConfig struct {
// Duration is how long to wait, as a Go duration string ("30s", "1m", "2h").
// Required, and must be positive.
Duration string `json:"duration"`
// CounterKey is the workflow-variable dot-path the node writes its fire
// count to, so a downstream gateway can bound a polling loop (for example
// "status.poll_attempts" against a condition of "< 60"). The count is
// written before the wait begins and starts at 1. Defaults to
// "<node id>.iterations".
CounterKey string `json:"counter_key,omitempty"`
}
TimerConfig defines how long a TIMER node waits before following its single outgoing edge. The wait is a durable Temporal timer, so it survives worker restarts and holds no activity or worker slot while it runs.
type UpdateEvent ¶
type UpdateEvent struct {
// EventType categorizes the signal (e.g., "AUDIT", "PROGRESS_UPDATE", "UI_HINT")
// so the workflow knows how to route the data internally.
EventType string `json:"eventType"`
// NodeID is the ID of the graph node being updated.
NodeID string `json:"nodeID"`
// Payload contains the contextual data for the event (e.g., percentage complete, or audit text).
Payload map[string]any `json:"payload,omitempty"`
}
UpdateEvent allows the task executor to send asynchronous signals into a running workflow (e.g., while a task is active).
type WorkflowCompletion ¶
type WorkflowCompletion struct {
// WorkflowID is the ID of the workflow that completed.
WorkflowID string
// Seq is the workflow's step counter taken for the END node's own run, so higher than every
// other node's Seq in this workflow. A host that orders its writes by Seq uses it to write the
// completion so a late write from any earlier step matches nothing.
Seq int64
// FinalVariables is the workflow's accumulated variables.
FinalVariables map[string]any
}
WorkflowCompletion is what the engine reports when a top-level workflow reaches its END node.
type WorkflowCompletionHandler ¶
type WorkflowCompletionHandler func(WorkflowCompletion) error
WorkflowCompletionHandler is invoked when the generic DAG workflow successfully reaches an "End" node, providing the final, accumulated state of the workflow variables. It may be invoked more than once for the same workflow (Temporal retries the Activity), so it must be idempotent.
type WorkflowDefinition ¶
type WorkflowDefinition struct {
// ID is the unique identifier for this specific workflow template.
ID string `json:"id"`
// Name is a human-readable label used for display and organizational purposes.
Name string `json:"name"`
// Version tracks iterations of the workflow logic, allowing for side-by-side
// deployment of different logic versions.
Version int `json:"version"`
// Nodes defines the individual steps, gateways, and boundary events
// that make up the workflow.
Nodes []Node `json:"nodes"`
// Edges defines the directed connections between nodes, including
// any conditional logic required for branching.
Edges []Edge `json:"edges"`
}
WorkflowDefinition represents the structural blueprint of a workflow process. It serves as the parsed representation of the JSON DSL, defining how nodes and edges form a directed graph for the execution engine.
type WorkflowInstance ¶
type WorkflowInstance struct {
// ID is the unique ID for this instance of the workflow.
ID string `json:"id"`
// Status represents the current execution state.
Status ExecutionStatus `json:"status"`
// WorkflowVariables holds the shared, dynamic business data passed between nodes.
WorkflowVariables map[string]any `json:"workflow_variables"`
// AuditTrail is a chronologically ordered log of events, milestones, or external signals.
AuditTrail []string `json:"audit_trail"`
NodeInfo map[string]*NodeInfo `json:"node_states"`
// Edges contains the workflow graph connections from the workflow definition.
Edges []Edge `json:"edges"`
}
WorkflowInstance holds the dynamic runtime state of the workflow execution. This struct is returned by the GetStatus query and represents a deterministic snapshot of the engine's memory at a given point in time.
func GraphInterpreterWorkflow ¶
func GraphInterpreterWorkflow(ctx workflow.Context, def WorkflowDefinition, initialWorkflowVariables map[string]any) (*WorkflowInstance, error)
GraphInterpreterWorkflow is the entry point for the Temporal workflow that interprets a graph definition.