workflow

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 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