Help us shape the future of Go! Take the 2026 Go Developer Survey

workflow

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

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

View Source
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

type AutoAction func(ctx context.Context, in AutoInput) (map[string]any, error)

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 Branch

type Branch struct {
	When *Condition `yaml:"when,omitempty"`
	Next string     `yaml:"next"`
}

Branch is one gateway edge. A nil When is the default (fallthrough).

type Condition

type Condition struct {
	Key    string `yaml:"key"`
	Equals any    `yaml:"equals"`
}

Condition is a minimal equality predicate over 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 Fraction

type Fraction struct {
	Kind  string `yaml:"kind,omitempty"`
	Value string `yaml:"value,omitempty"`
}

Fraction is a "n/d" quorum or pass threshold, e.g. "2/3".

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 NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) Err

func (r *Registry) Err() error

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

func (rt *Runtime) CompleteTask(ctx context.Context, taskID uuid.UUID, output map[string]any) error

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

func (rt *Runtime) Decide(ctx context.Context, taskID uuid.UUID, d Decision) error

Decide records an approve/reject on a task and drives the resulting transition, in its own tenant transaction.

func (*Runtime) Delegate

func (rt *Runtime) Delegate(ctx context.Context, taskID, to uuid.UUID, until time.Time) error

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) Instance

func (rt *Runtime) Instance(ctx context.Context, id uuid.UUID) (Instance, error)

Instance loads an instance by id in a read-only tenant transaction.

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.

Jump to

Keyboard shortcuts

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