Documentation
¶
Overview ¶
Package workflow is wowapi's small custom Postgres-backed workflow engine: a closed-step-type approval/state-machine runtime that shares the caller's tenant transaction (RLS + outbox + audit) exactly as blueprint 02 §1 and decisions D-0051/D-0053 specify.
The kernel owns the runtime; modules own the definitions (seeded JSON/YAML) via a boot-validated Registry. Definitions are immutable per version and running instances pin their version. Every transition re-checks the actor (assignee + optional `workflow.task.decide` permission), mutates instance and task rows with optimistic locking, and writes the matching outbox event in the SAME tenant transaction as the state change.
Import boundary (depguard): stdlib + kernel/{database,authz,resource,outbox, errors,model,pagination} + pgx + uuid + yaml. NEVER module/app/adapters/ testkit in production. Domain-neutral vocabulary only.
Index ¶
- Constants
- type Assignee
- type AssigneeResolver
- type AssigneeSpec
- type AutoAction
- type AutoInput
- type Branch
- type Condition
- type Decision
- type DecisionType
- type Definition
- type Electorate
- type Fraction
- type Instance
- type Policy
- type Registry
- type ResolveInput
- type ResolvedKind
- type Runtime
- func (rt *Runtime) CompleteTask(ctx context.Context, taskID uuid.UUID, output map[string]any) error
- func (rt *Runtime) Decide(ctx context.Context, taskID uuid.UUID, d Decision) error
- func (rt *Runtime) Delegate(ctx context.Context, taskID, to uuid.UUID, until time.Time) error
- func (rt *Runtime) Instance(ctx context.Context, id uuid.UUID) (Instance, error)
- func (rt *Runtime) OpenTasksFor(ctx context.Context, a authz.Actor, cur pagination.Request) (pagination.CursorPage[Task], error)
- func (rt *Runtime) Override(ctx context.Context, actor authz.Actor, instanceID uuid.UUID, to string, ...) error
- func (rt *Runtime) StartIn(ctx context.Context, db database.TenantDB, defKey string, res resource.Ref, ...) (uuid.UUID, error)
- func (rt *Runtime) SweepSLA(ctx context.Context, db database.TenantDB, now time.Time) (reminders, escalations int, err error)
- type SLA
- type Step
- type StepType
- type Task
- type Transition
Constants ¶
const ( SpecActor = "actor" // explicit acting capacity SpecRole = "role" // role-at-scope SpecRelationship = "relationship" // relationship-holder SpecResourceOwner = "resource_owner" // owner of the target resource SpecResolver = "resolver" // module-registered resolver func )
Assignee spec kinds (closed set). These describe how a step's assignees are derived at task-creation time; they are resolved into concrete Assignee rows.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Assignee ¶
type Assignee struct {
Kind ResolvedKind
Ref string
}
Assignee is a concrete, resolved assignee row for a task.
type AssigneeResolver ¶
type AssigneeResolver func(ctx context.Context, in ResolveInput) ([]Assignee, error)
AssigneeResolver resolves a `resolver`-kind AssigneeSpec into concrete assignees at task-creation time.
type AssigneeSpec ¶
type AssigneeSpec struct {
Kind string `yaml:"kind"`
Actor string `yaml:"actor,omitempty"` // capacity id (kind=actor)
Role string `yaml:"role,omitempty"` // role key (kind=role)
Scope string `yaml:"scope,omitempty"` // scope hint (kind=role)
Rel string `yaml:"rel,omitempty"` // relationship type (kind=relationship)
Resolver string `yaml:"resolver,omitempty"` // resolver key (kind=resolver)
}
AssigneeSpec describes one source of assignees for a step.
type AutoAction ¶
AutoAction is a module Go action bound to an `auto` step. On success the runtime merges its output into the instance context and advances; on error it follows the step's on_error transition.
type AutoInput ¶
type AutoInput struct {
InstanceID string
Resource resource.Ref
Step string
Context map[string]any
}
AutoInput is what a registered auto-action receives: the instance context and its target resource. The returned map is merged into the instance context.
type Decision ¶
type Decision struct {
Actor authz.Actor
Type DecisionType
Comment string
}
Decision is the input to Decide: who acted, the outcome, and an optional comment (required when the transition sets require_comment).
type DecisionType ¶
type DecisionType string
DecisionType is the outcome an actor records on an approval/vote task.
const ( // DecisionApprove advances via the step's on_approve transition. DecisionApprove DecisionType = "approve" // DecisionReject advances via the step's on_reject transition. DecisionReject DecisionType = "reject" // DecisionAbstain records a non-committal vote (vote steps). DecisionAbstain DecisionType = "abstain" )
type Definition ¶
type Definition struct {
Key string `yaml:"key"`
Version int `yaml:"version"`
AppliesTo string `yaml:"applies_to"`
InitialStep string `yaml:"initial_step"`
Steps map[string]Step `yaml:"steps"`
}
Definition is the JSON/YAML workflow definition: a versioned, seedable graph of steps. It is immutable per (Key, Version); running instances pin a version.
func ParseDefinition ¶
func ParseDefinition(raw []byte) (Definition, error)
ParseDefinition parses a strict JSON/YAML definition. Unknown keys are an error (KnownFields), so a typo in a seed fails loudly at load rather than silently dropping a step or transition. JSON is a subset of YAML, so this one path covers both.
func (Definition) Validate ¶
func (d Definition) Validate(autoActions, resolvers map[string]bool) error
Validate checks the definition graph and its external references, accumulating ALL problems into a single error. autoActions and resolvers are the sets of registered keys the definition may reference; unknown keys fail.
Checks: initial_step exists; every step type is in the closed set; every transition target exists; every step is reachable from initial_step (no orphans); at least one terminal is reachable; every auto action key is registered; every resolver key is registered.
type Electorate ¶
type Electorate struct {
Kind string `yaml:"kind"`
Rel string `yaml:"rel,omitempty"`
Of string `yaml:"of,omitempty"`
}
Electorate describes the voter set for a vote step (minimal).
type Instance ¶
type Instance struct {
ID uuid.UUID
DefinitionID uuid.UUID
Resource resource.Ref
CurrentStep string
Status string
Context map[string]any
Version int
}
Instance is a running (or ended) workflow instance.
type Policy ¶
type Policy struct {
MinApprovals int `yaml:"min_approvals,omitempty"`
SelfApproval *bool `yaml:"self_approval,omitempty"` // pointer: distinguish unset from explicit false
}
Policy governs an approval/vote step's decision rules.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the boot-time workflow catalog: definitions plus the module Go actions and assignee resolvers they reference. Like authz.Registry it accumulates registration errors and validates every definition in Err(), so a dangling transition or unknown auto-action fails boot, never a running instance (D-0053).
func (*Registry) Err ¶
Err returns accumulated registration errors AND each definition's Validate() error, joined, or nil. It must gate boot.
func (*Registry) RegisterAssigneeResolver ¶
func (r *Registry) RegisterAssigneeResolver(key string, fn AssigneeResolver)
RegisterAssigneeResolver binds a resolver func to a resolver key.
func (*Registry) RegisterAutoAction ¶
func (r *Registry) RegisterAutoAction(key string, fn AutoAction)
RegisterAutoAction binds a Go action to an auto step's action key.
func (*Registry) RegisterDefinition ¶
func (r *Registry) RegisterDefinition(def Definition) error
RegisterDefinition adds a definition. A duplicate (key, version) is an error. Full graph validation is deferred to Err() so it runs after all auto-actions and resolvers are registered.
type ResolveInput ¶
type ResolveInput struct {
InstanceID string
Resource resource.Ref
Step string
Context map[string]any
}
ResolveInput is what an assignee resolver receives when a task is created.
type ResolvedKind ¶
type ResolvedKind string
ResolvedKind is the concrete assignee kind persisted in workflow_task_assignees (capacity|role|relationship|system).
const ( // KindCapacity addresses a specific acting capacity (the assignee-check unit). KindCapacity ResolvedKind = "capacity" // KindRole addresses anyone holding a role (authz-resolved at decide time). KindRole ResolvedKind = "role" // KindRelationship addresses relationship-holders on the resource. KindRelationship ResolvedKind = "relationship" // KindSystem addresses an automated principal. KindSystem ResolvedKind = "system" )
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime is the workflow engine. Every method mutates state inside a tenant transaction (RLS-scoped), writing the matching outbox event in the same tx. StartIn joins the caller's transaction; the other mutators open their own.
func NewRuntime ¶
func NewRuntime(txm database.TxManager, reg *Registry, ev authz.Evaluator, ob outbox.Writer, idgen model.IDGen) *Runtime
NewRuntime wires the runtime. authz may be nil (assignee check is then the sole gate); the others are required.
func (*Runtime) CompleteTask ¶
CompleteTask marks a `task`-type task done (with optional output) and advances via the step's next transition, in its own tenant transaction.
func (*Runtime) Decide ¶
Decide records an approve/reject on a task and drives the resulting transition, in its own tenant transaction.
func (*Runtime) Delegate ¶
Delegate records a delegate on an OPEN task: delegated_to is set and the delegate is ADDED as an assignee so the original assignee retains visibility. The task stays open (blueprint §1.3).
func (*Runtime) OpenTasksFor ¶
func (rt *Runtime) OpenTasksFor(ctx context.Context, a authz.Actor, cur pagination.Request) (pagination.CursorPage[Task], error)
OpenTasksFor lists the open tasks an actor may act on (capacity assignee or delegate), cursor-paginated by (created_at, id) with a signed (versioned) keyset cursor.
func (*Runtime) Override ¶
func (rt *Runtime) Override(ctx context.Context, actor authz.Actor, instanceID uuid.UUID, to string, reason string) error
Override is a privileged transition: it requires a reason and jumps the instance to a step or terminal, emitting workflow.<def>.overridden. Any open tasks on the current step are marked skipped.
TODO(ratify): auto-create a ratification task when the definition declares a ratify_by role (the definition model does not yet carry ratify_by; blueprint §1.3). The jump + event are implemented; ratification is a documented gap.
func (*Runtime) StartIn ¶
func (rt *Runtime) StartIn(ctx context.Context, db database.TenantDB, defKey string, res resource.Ref, input map[string]any) (uuid.UUID, error)
StartIn creates an instance and enters its initial step INSIDE the caller's tenant transaction, so a business write and its workflow start commit or roll back together (blueprint §1.3).
func (*Runtime) SweepSLA ¶
func (rt *Runtime) SweepSLA(ctx context.Context, db database.TenantDB, now time.Time) (reminders, escalations int, err error)
SweepSLA processes SLA timers for open tasks in the caller's tenant tx and is idempotent: reminders are guarded by last_reminded_at, escalations by the task transitioning out of 'open'. It is invoked by a registered per-tenant job (the lead wires the job; this is the method).
- reminder: an open task past remind_after that has not been reminded since that time gets a workflow.<def>.reminded event and last_reminded_at = now. Running twice does not double-remind.
- escalation: an open task past due_at is marked expired, a workflow.<def>.escalated event is emitted, and if its step declares an escalate_to step an escalation task is created there.
type SLA ¶
type SLA struct {
Due string `yaml:"due,omitempty"`
RemindAfter string `yaml:"remind_after,omitempty"`
EscalateTo string `yaml:"escalate_to,omitempty"` // "step:key" or "key"
}
SLA carries the reminder/escalation timings for a step (ISO-8601 durations).
type Step ¶
type Step struct {
Type StepType `yaml:"type"`
Assignees []AssigneeSpec `yaml:"assignees,omitempty"`
Policy *Policy `yaml:"policy,omitempty"`
SLA *SLA `yaml:"sla,omitempty"`
// approval / vote transitions.
OnApprove *Transition `yaml:"on_approve,omitempty"`
OnReject *Transition `yaml:"on_reject,omitempty"`
// task / auto / gateway default transition.
Next *Transition `yaml:"next,omitempty"`
// auto step.
Action string `yaml:"action,omitempty"`
OnError *Transition `yaml:"on_error,omitempty"`
// gateway step.
Branches []Branch `yaml:"branches,omitempty"`
// vote step (minimal).
Electorate *Electorate `yaml:"electorate,omitempty"`
Quorum *Fraction `yaml:"quorum,omitempty"`
Pass *Fraction `yaml:"pass,omitempty"`
Window string `yaml:"window,omitempty"`
// terminal step.
Outcome string `yaml:"outcome,omitempty"`
}
Step is one node in the definition graph. Which fields are meaningful depends on Type; validation and the runtime read only the relevant ones.
type StepType ¶
type StepType string
StepType is the closed set of workflow step kinds (D-0053). A definition that carries any other type fails validation.
const ( // StepApproval is an approve/reject decision by one or more assignees. StepApproval StepType = "approval" // StepTask is a do-something step marked done (with optional output). StepTask StepType = "task" // StepAuto invokes a registered module Go action, then advances. StepAuto StepType = "auto" // StepGateway branches on a simple predicate over the instance context. StepGateway StepType = "gateway" // StepVote is a quorum/threshold decision over an electorate. StepVote StepType = "vote" // StepTerminal ends the instance with an outcome. StepTerminal StepType = "terminal" )
type Task ¶
type Task struct {
ID uuid.UUID
InstanceID uuid.UUID
StepKey string
TaskType string
Status string
DueAt *time.Time
RemindAfter *time.Time
DecidedBy *uuid.UUID
DelegatedTo *uuid.UUID
Output map[string]any
Version int
}
Task is a unit of work in an instance (an approval, a to-do, etc.).
type Transition ¶
type Transition struct {
Next string `yaml:"next,omitempty"`
RequireComment bool `yaml:"require_comment,omitempty"`
Retry string `yaml:"retry,omitempty"` // auto on_error retry policy (advisory)
Then string `yaml:"then,omitempty"` // auto on_error target step
}
Transition is an edge to another step (Next) with optional decision flags.