workflow

package
v0.1.0 Latest Latest
Warning

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

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

Documentation

Overview

Package workflow implements the workflow engine: parsing and validating declarative workflow definitions, resolving their expressions, and the explicit state machines Run and Step transitions follow. It has no knowledge of persistence or of how actions actually execute — that is internal/runs's job, orchestrating on top of this package.

Index

Constants

View Source
const (
	ScaffoldTemplateMinimal = "minimal"
	ScaffoldTemplateForeach = "foreach"
)

ScaffoldTemplateMinimal and ScaffoldTemplateForeach are the values accepted by `workflow new --template` (internal/cli/workflow.go).

View Source
const FileExtension = ".patchcord-workflow"

FileExtension is the conventional file extension for a workflow definition file (vision document, section 9.3: ".patchcord-workflow" — "définition déclarative seule"). Unlike .patchcord-app or .patchcord-plugin, a workflow package is not an archive: it is exactly the plain YAML Definition below, and `workflow install`/`workflow export` read and write it without looking at this extension at all — it is a pure naming convention.

View Source
const SupportedSchemaVersion = 1

SupportedSchemaVersion is the only workflow schema version this engine understands.

Variables

View Source
var ErrInvalidInputs = errors.New("invalid workflow inputs")

ErrInvalidInputs is wrapped by every error PrepareInputs and validateInputDefs return, so a caller (internal/api's handleRunWorkflow) can tell "the run's inputs don't satisfy the workflow's declared schema" apart from an internal failure and respond 400 instead of 500.

Functions

func PrepareInputs

func PrepareInputs(defs []InputDef, provided map[string]any) (map[string]any, error)

PrepareInputs resolves provided against defs — the workflow's declared input schema — filling in defaults, rejecting a missing required input or an undeclared key, and coercing every value to its declared type. It returns a new map; provided is never mutated.

When defs is empty (the workflow declares no input schema), provided is returned unchanged: every workflow installed before this schema existed, and every workflow that doesn't need one, keeps working exactly as before.

func ResolveConnector

func ResolveConnector(connector string, ctx ExprContext) (string, error)

ResolveConnector resolves a step's Connector reference against ctx. An empty connector means the step uses none. A non-empty connector must be entirely one ${{ ... }} expression — Validate rejects anything else — so a published, immutable workflow version (ADR-0008) never bakes in one deployment's specific connector identity; the indirection is what keeps a workflow portable across environments (typically via ${{ bindings.<name> }}, though ${{ workflow.inputs.<key> }} and ${{ steps.<id>.outputs.<key> }} are equally legitimate indirections).

func ResolveForeach

func ResolveForeach(foreachValue any, ctx ExprContext) ([]any, error)

ResolveForeach resolves a step's Foreach declaration against ctx into the list of items to iterate over. Foreach nil means the step does not iterate at all — callers use this to tell a foreach step from a regular one. A non-nil Foreach is either a literal list or a ${{ ... }} expression — Validate rejects anything else — and the expression form must itself resolve to a list; anything else is a run-time error. ctx need not (and should not) have HasEach set: the list being iterated is resolved once, before any item is bound.

func ResolveIf

func ResolveIf(ifValue any, ctx ExprContext) (bool, error)

ResolveIf resolves a step's If condition against ctx, defaulting to true when the step declares none (ifValue is nil). A non-empty If is either a literal bool or a ${{ ... }} expression — Validate rejects anything else — and the expression form must itself resolve to a bool; anything else is a run-time error rather than a silently always-true or always-false step.

func ResolveInputs

func ResolveInputs(with map[string]any, ctx ExprContext) (map[string]any, error)

ResolveInputs evaluates every ${{ ... }} expression in with against ctx, including expressions nested inside list or object values, returning a new map with expressions replaced by their resolved values. Non-expression values are copied through unchanged.

func RewriteVersion

func RewriteVersion(source []byte, version int) []byte

RewriteVersion returns source with its top-level `version:` field replaced by version — byte-for-byte unchanged otherwise, including comments and formatting anywhere else in the file. It is a no-op (returns source unchanged, not even reallocated) when source's declared version already equals version.

This exists for runs.InstallWorkflowAtVersion (ADR-0055): a dev-mode auto-assigned version can differ from what the file itself declares (the file is never rewritten on disk — only InstallDir's dev-only install path picks a different version than the one written down). The *stored* copy must still declare the version it is actually recorded under, or re-parsing it (LatestWorkflow, WorkflowSource, `workflow export`) would report the stale, on-disk version instead of the one the row is keyed on.

func Scaffold

func Scaffold(path, id string, version int) error

Scaffold writes a minimal, valid workflow definition to path — equivalent to ScaffoldTemplate(path, id, version, ScaffoldTemplateMinimal). Kept as its own function since it is the one every other package's own scaffolding delegates to for a bundle's/plugin example's default embedded workflow (internal/bundles/scaffold.go): those always want the minimal template, never a caller-chosen one.

func ScaffoldTemplate

func ScaffoldTemplate(path, id string, version int, template string) error

ScaffoldTemplate writes a valid workflow definition to path: id, version, a manual trigger, and one step demonstrating template — one of ScaffoldTemplateMinimal or ScaffoldTemplateForeach. A workflow must declare at least one step (Validate, "workflow must declare at least one step"), so neither skeleton can be empty. It returns an error if path already exists — ScaffoldTemplate never overwrites — or if template is none of the above.

func Validate

func Validate(def *Definition, knownActions map[string]struct{}) error

Validate checks def against the rules a workflow must satisfy before it can be installed or run (vision document, section 12.5):

  • a supported schema version;
  • a non-empty id, a positive version, a "manual", "schedule" or "webhook" trigger;
  • for a "schedule" trigger: a valid 5-field cron expression, a recognized on_missed policy, no required input lacking a default and no connector-bound step — a schedule fires unattended, with nobody to supply inputs or bindings at run time (see ADR-0035);
  • for a "webhook" trigger: a valid secret reference and no connector-bound step — a webhook has an inbound caller to supply inputs (its request body), but never a bindings map, so a required input without a default is fine but a connector binding still isn't (see ADR-0037);
  • at least one step, with unique, non-empty step ids;
  • every step's action exists among knownActions;
  • every ${{ steps.<id>.outputs...}} expression refers to an earlier step in the same workflow, catching typos and forward references before a run ever starts;
  • a step's if, when set, is a literal bool or a ${{ ... }} expression of a supported shape — never any other literal type;
  • a step's foreach, when set, is a literal list or a ${{ ... }} expression of a supported shape;
  • "${{ each }}" only appears inside a foreach step's own with — every other spot (if, connector, foreach itself, another step's with) is rejected, since no iteration is in progress there;
  • a comparison expression's ("<path> <op> <literal>") left-hand side is a supported shape and its literal right-hand side is well-formed;
  • stop_if_false requires if to be set;
  • else_of, when set, names a step defined earlier in the same workflow.

knownActions is the set of action identifiers currently installed plugins contribute; the caller (internal/runs) is responsible for fetching it from the plugin catalog, keeping this package free of any persistence or process dependency.

func ValidateRunTransition

func ValidateRunTransition(from, to RunStatus) error

ValidateRunTransition returns an error unless a Run may move from from to to.

func ValidateStepTransition

func ValidateStepTransition(from, to StepStatus) error

ValidateStepTransition returns an error unless a Step may move from from to to.

Types

type Definition

type Definition struct {
	SchemaVersion int        `yaml:"schema_version"`
	ID            string     `yaml:"id"`
	Version       int        `yaml:"version"`
	Trigger       Trigger    `yaml:"trigger"`
	Inputs        []InputDef `yaml:"inputs,omitempty"`
	Steps         []Step     `yaml:"steps"`
}

Definition is a parsed, declarative workflow, as described in the vision document (section 7.5). It is serialized as YAML.

func Parse

func Parse(source []byte) (*Definition, error)

Parse parses a workflow definition from its YAML source. It only parses: call Validate to check it against the rules described in the vision document (section 12.5) before treating it as runnable.

type ExprContext

type ExprContext struct {
	Inputs      map[string]any
	StepOutputs map[string]map[string]any // by step id
	Bindings    map[string]string         // connector id by logical binding name
	// Each is the current item, set only while resolving a foreach step's
	// With for one iteration (see ResolveForeach and internal/runs's
	// runner). HasEach distinguishes "no iteration in progress" from an
	// item whose value happens to be nil — ${{ each }} must fail in the
	// former case, not resolve to nil.
	Each    any
	HasEach bool
}

ExprContext supplies the values ${{ ... }} expressions may resolve against: the workflow's own inputs, the recorded outputs of the steps that already ran, the connector ids bound to this run's bindings, and — only while a foreach step is iterating — the current item.

type InputDef

type InputDef struct {
	// Name is the key a step references as ${{ workflow.inputs.<Name> }}.
	Name string `yaml:"name"`
	// Type is one of "string" (the default when empty), "number",
	// "boolean" or "enum". See validateInputDefs and PrepareInputs.
	Type string `yaml:"type,omitempty"`
	// Required means a run must supply this input unless Default is set —
	// validateInputDefs rejects declaring both, since Default would
	// silently satisfy Required, making the flag meaningless.
	Required bool `yaml:"required,omitempty"`
	// Description is a human-readable hint for whoever fills this input in
	// (e.g. a generated form field's label or help text).
	Description string `yaml:"description,omitempty"`
	// Default is used when a run does not supply this input. Its Go type
	// (as YAML unmarshals it) must match Type.
	Default any `yaml:"default,omitempty"`
	// Enum lists the values a "enum"-typed input may take. Required for
	// type "enum", rejected for every other type.
	Enum []string `yaml:"enum,omitempty"`
}

InputDef declares one input a workflow expects, so a client (the CLI, the HTTP API, a dashboard) can validate and collect it before a run starts, instead of only discovering a missing or misspelled ${{ workflow.inputs.<key> }} reference deep inside a step at run time. A workflow with no declared Inputs keeps today's behavior: any input key may be passed, unvalidated (see PrepareInputs).

type ParseError

type ParseError struct {
	Err error
}

ParseError wraps a YAML syntax error encountered while parsing a workflow definition.

func (*ParseError) Error

func (e *ParseError) Error() string

func (*ParseError) Unwrap

func (e *ParseError) Unwrap() error

type RunStatus

type RunStatus string

RunStatus is one state in a Run's explicit state machine (vision document, section 12.2).

const (
	RunQueued    RunStatus = "queued"
	RunRunning   RunStatus = "running"
	RunSucceeded RunStatus = "succeeded"
	RunFailed    RunStatus = "failed"
	RunCancelled RunStatus = "cancelled"
)

type Step

type Step struct {
	// ID identifies this step within its workflow; other steps reference
	// its outputs as steps.<ID>.outputs.<key>.
	ID string `yaml:"id"`
	// Uses is the action identifier this step invokes, e.g. "text.uppercase@1".
	Uses string `yaml:"uses"`
	// With holds the action's input values. A string value that is
	// entirely one ${{ ... }} expression is resolved at run time against
	// the workflow's inputs or a prior step's outputs; every other value
	// is passed through unchanged.
	With map[string]any `yaml:"with"`
	// Connector, if non-empty, binds a connector to this step's action
	// call. It must be entirely one ${{ ... }} expression — never a
	// literal connector id — so a published, immutable workflow version
	// (ADR-0008) never bakes in one deployment's specific connector
	// identity; see ResolveConnector and ADR-0021.
	Connector string `yaml:"connector,omitempty"`
	// If, when set, gates whether this step runs at all: a literal bool,
	// or a ${{ ... }} expression that must resolve to one. A step whose
	// If resolves to false is recorded as skipped — never invoked, and its
	// outputs are unavailable to later steps — without failing the run.
	// Nil (the field omitted) means the step always runs. See ResolveIf
	// and ADR-0031.
	If any `yaml:"if,omitempty"`
	// Foreach, when set, runs this step's action once per item of a
	// literal list or a ${{ ... }} expression that must resolve to one,
	// with the current item available to With as ${{ each }}. Outputs are
	// collected into lists under the action's own output keys — a
	// foreach step's steps.<ID>.outputs.<key> is an array, not a scalar.
	// Nil (the field omitted) means the step runs exactly once, as if it
	// had no Foreach at all. See ResolveForeach and ADR-0032.
	Foreach any `yaml:"foreach,omitempty"`
	// StopIfFalse changes what a false If does: instead of skipping only
	// this step (the default), it skips this step and every step after it,
	// ending the run as succeeded — no error, exactly a guard clause's
	// early return. Ignored when If is nil (nothing to be false) or when
	// ElseOf causes this step to be skipped before If is even evaluated.
	// See ADR-0033.
	StopIfFalse bool `yaml:"stop_if_false,omitempty"`
	// ElseOf, when set, names an earlier step (by ID): this step is
	// skipped whenever that step actually ran — regardless of this step's
	// own If, which (if present) still applies on top, as an additional
	// condition. Chaining ElseOf onto the previous link in a sequence of
	// steps — not onto the first one — builds an if/elseif/else chain
	// without nesting: a step with ElseOf but no If is that chain's
	// unconditional "else". See ADR-0033.
	ElseOf string `yaml:"else_of,omitempty"`
}

Step is one action invocation within a workflow.

type StepStatus

type StepStatus string

StepStatus is one state in a Step's explicit state machine (vision document, section 12.2).

const (
	StepPending   StepStatus = "pending"
	StepRunning   StepStatus = "running"
	StepSucceeded StepStatus = "succeeded"
	StepFailed    StepStatus = "failed"
	StepSkipped   StepStatus = "skipped"
	StepCancelled StepStatus = "cancelled"
)

type Trigger

type Trigger struct {
	Type string `yaml:"type"`
	// Cron is a standard 5-field cron expression ("minute hour dom month
	// dow"). Required when Type is "schedule", rejected otherwise. See
	// ADR-0035.
	Cron string `yaml:"cron,omitempty"`
	// OnMissed controls what happens to occurrences the scheduler could not
	// fire because the agent was offline past them: "skip" (the default,
	// used when empty) drops the backlog and resumes at the next future
	// occurrence; "fire_once" runs once for the most recently missed
	// occurrence, then resumes normal cadence. Only meaningful when Type is
	// "schedule". See ADR-0035.
	OnMissed string `yaml:"on_missed,omitempty"`
	// SecretRef is a reference (never the secret's actual value — ADR-0009)
	// resolved at request time to check an inbound webhook request's shared
	// secret header. Required when Type is "webhook", rejected otherwise.
	// See ADR-0037.
	SecretRef secrets.Reference `yaml:"secret_ref,omitempty"`
}

Trigger declares how a workflow starts: "manual" (the default so far), "schedule" (fired unattended by internal/scheduler on a cron cadence, ADR-0035), or "webhook" (fired by an inbound HTTP request, ADR-0037). Event triggers remain a later phase.

Jump to

Keyboard shortcuts

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