Skip to content

AGWF / AGSR — Workflow & State Reducer Diagnostics

The AGWF (workflow) and AGSR (state reducer) diagnostics are emitted by the Strategos workflow source generator at compile time. They catch workflow definition errors before runtime — empty names, missing entry points, fork/join mismatches, invalid reducer attribute targets, and unsound ontology-to-workflow bindings. The AONT-prefixed diagnostics on the sibling pages cover the ontology layer; this page covers the workflow analyzer.

PrefixCategoryDescription
AGWFWorkflowWorkflow definition validation
AGSRState ReducerState reducer attribute validation
CodeSeverityDescription
AGWF001ErrorWorkflow name cannot be empty
AGWF002WarningNo steps found in workflow
AGWF003ErrorDuplicate step name
AGWF004ErrorInvalid namespace (global namespace not supported)
AGWF009ErrorWorkflow must begin with StartWith<T>()
AGWF010WarningWorkflow should end with Finally<T>()
AGWF012ErrorFork must be followed by Join<T>()
AGWF014ErrorLoop body cannot be empty
AGWF039ErrorBound workflow identity does not resolve exactly once
AGWF040ErrorReachable step action identity is missing, invalid, dynamic, or does not resolve exactly once
AGWF041ErrorA closed workflow contract definitely fails behavioral refinement
AGWF042ErrorA workflow contract cannot be proved statically
AGWF043ErrorWorkflow identities collide after generated-name normalization
AGWF044ErrorAn authored compensation action disagrees with the derived inverse
AGWF045ErrorA compensation scope is not mechanically derivable
AGWF046ErrorA declared binding’s contract cannot be exported for a referencing compilation
AGWF047ErrorA referenced assembly’s proof catalog cannot be read
AGWF048ErrorTwo assemblies declare one ordinal action identity
AGWF049ErrorA closed-enum wire slot carries a value outside its vocabulary
AGSR001Error[Append] can only be applied to collection types
AGSR002Error[Merge] can only be applied to dictionary types

An ontology action bound through WorkflowBindingReference is accepted only when the generator can construct and prove a closed contract for the referenced workflow. These diagnostics distinguish lookup and identity failures from proof results:

CodeCauseFix
AGWF039The binding’s exact ordinal WorkflowId matches zero or multiple C# or imported workflow definitions.Declare exactly one workflow under that name, or correct the binding.
AGWF040A reachable C# step occurrence is missing .Performs(...); the reference is blank, duplicated, or dynamic; or an accepted C#/JSON reference’s exact (DomainName, ObjectTypeName, ActionName) tuple matches zero or multiple actions. Malformed imported JSON is rejected earlier as AGWF023.Give the occurrence one direct new WorkflowActionReference(...) whose compile-time constant names resolve exactly once.
AGWF041All inputs are closed, and the solver finds a definite counterexample or a subject, frame, authority, or fork-isolation violation.Use the reported obligation and witness to correct the leaf contract or, only when the public contract was too strict, the bound action specification.
AGWF042A binding, workflow topology, action contract, authority lattice, or predicate is dynamic, invalid, opaque, contradictory, unrealizable, absent from the closed proof representation, or otherwise outside the closed proof fragment.Replace the unprovable input with an analyzer-visible, supported closed form. A runtime check or custom predicate is not a substitute for the binding proof.
AGWF043Two or more ordinal workflow identities normalize to the same generated PascalCase type and source-hint namespace.Rename the workflows so every normalized generated name is unique.
AGWF044A configured inverse is legacy, dynamic, missing, ambiguous, opaque, or not semantically equivalent to the inverse derived from its forward action.Use .Compensate<T>(new WorkflowActionReference(...)) and give the inverse the same subject, frame, and semantic authority, the forward effective guarantee as its requirement, and the forward hard requirement as its effective guarantee.
AGWF045A workflow or bound action claims rollback, but a rollback-reachable non-empty-frame leaf has no proved inverse, the typed compensation program lacks one shared closed subject/binding boundary, or the workflow uses event-sourced persistence whose consumer-defined replay fold cannot be proved.Make every state-changing leaf in the reported scope compensable. Do not mix legacy, dynamic, and typed declarations into a derived program. Typed derived compensation requires saga-document persistence in 3.0. A contract declared in a referenced assembly is read from that assembly’s exported proof catalog, so it is visible here; a contract the exporter refused (AGWF046) is not. AllowDiagnosticFork is not represented in the closed proof, so remove it from a workflow that also declares typed or dynamic compensation.
AGWF046This assembly declares a workflow binding whose action contract cannot be projected into the portable ProofCatalogV1 form — it carries an opaque term, or a link or relation atom whose declared read set the wire shape cannot carry.Move the action to the closed predicate vocabulary, or keep the binding in the compilation that lowers its workflow. The diagnostic names the action and the reason.
AGWF047A referenced assembly’s proof catalog declares an unknown schemaVersion, is malformed, carries an unknown predicate / literal / operator / scalar discriminator or an authority its own lattice does not define, or its contentHash does not match its bytes.Rebuild the referencing project against a Strategos version that understands the catalog, or against a producer built by one this version emits. An unreadable catalog is refused, never skipped.
AGWF048One ordinal action identity is declared by two catalogs, or by a catalog and this compilation.Give the action exactly one declaring assembly. There is no tie-break by design: preferring either side would make the proved contract depend on reference order.

The proof reads one OntologyActionCatalog. An assembly that declares a BoundToWorkflow action exports its contracts as a ProofCatalogV1 document on an assembly-level attribute and defers the binding; the compilation that lowers the workflow imports the catalog, merges it into that same catalog, and proves the binding with the same code that proves a local one. An assembly that declares a binding and exports nothing is refused with AONT222.

Deferral is per action, not per assembly, and it has a limit: a library cannot know whether some consumer will supply the workflow, so a binding nothing ever declares is never proved by anyone. A leaf application says it is the last compilation with <StrategosProofRequireLocalBindings>true</StrategosProofRequireLocalBindings>, and a deferral there is an error. See cross-assembly binding proof for the manifest, its refusals, and adoption.

The refinement rule is contravariant in requirements and covariant in guarantees: the bound action’s requirement must imply the workflow entry requirement, while every successful workflow exit must imply the bound action’s declared guarantee. The union of leaf frames must remain within the bound frame, and the pointwise join of leaf authority requirements must be no stronger than the bound action permits. Internal seams and parallel-path interference are also proved. See Typed action calculus for the full model.

Compensation proof is based on executable occurrences, not an authored rollback list. Which occurrences the rollback covers depends on the failure ingress. For an in-flight failure at C after the prefix A ; B completed, the derived order is B^-1 ; A^-1; C is absent, because it never journaled a completed entry. For a post-completion failure — the reducer flags Failed on an occurrence whose journal entry already reads Completed — the derived order is C^-1 ; B^-1 ; A^-1, because the plan reverses every Completed entry in the selected scope and the post-completion failure claim requires and preserves that entry. An inverse can therefore run for a forward step that did complete; write it to be safe on that path. Nested scope failures do not unwind enclosing scopes. See mechanically derived compensation for inverse equivalence, scope propagation, and runtime journal semantics.


Four slots in a .workflow.json are typed by a closed contract enum: a gate’s class (GateClass), a permitted fork trigger (ForkTrigger), a step’s runtime (StepRuntime) and a failure handler’s scope (FailureHandlerScope). Both runtimes match these by value, so the member name is not an accepted spelling of the value, and a class retired in an earlier contract major is not an accepted value at all.

CodeCauseFix
AGWF049A closed-enum wire slot declares a token the enum does not define. The diagnostic names the import file, the construct, the JSON path, the enum, and its full vocabulary.Use the wire value. The two common cases are a member name written where its value belongs (Typecheck for typecheck, RatificationFailure for ratification_failure) and a value retired by an earlier contract major.

The accepted set is not written in the generator. It is emitted from the same TypeSpec that produces the enum, linked into the generator as source, and pinned against the emitted JSON Schema by a test — so adding a member widens the check with no edit to it. Until 3.0 the import front-end read these slots as opaque strings and accepted any token; the emitted Zod arm always rejected them, which is how the gap surfaced.

PropertyValue
SeverityError
CategoryDSL Completeness

The [Workflow] attribute requires a non-empty workflow name.

Invalid:

[Workflow("")] // Error: Empty name
[Workflow(" ")] // Error: Whitespace only

Valid:

[Workflow("process-order")]

PropertyValue
SeverityWarning
CategoryDSL Completeness

The workflow definition doesn’t contain any recognizable step methods.

Trigger:

[Workflow("empty-workflow")]
public static partial class EmptyWorkflow
{
public static WorkflowDefinition<State> Definition =>
Workflow<State>.Create("empty-workflow"); // Warning: No steps
}

Resolution:

Workflow<State>.Create("my-workflow")
.StartWith<FirstStep>()
.Then<SecondStep>()
.Finally<LastStep>();

PropertyValue
SeverityError
CategoryStructural Validation

The same step type appears multiple times where each step must be unique (linear flow, fork paths).

Invalid:

.StartWith<ValidateStep>()
.Then<ProcessStep>()
.Then<ValidateStep>() // Error: Duplicate

Resolution: Use instance names to disambiguate:

.StartWith<ValidateStep>()
.Then<ProcessStep>()
.Then<ValidateStep>("FinalValidation") // OK: Different instance

::: info Duplicates in mutually exclusive branch paths are allowed since only one path executes. :::


PropertyValue
SeverityError
CategoryDSL Completeness

Workflows must be declared in a namespace. Global namespace is not supported.

Invalid:

// No namespace declaration
[Workflow("orphan-workflow")]
public static partial class OrphanWorkflow { } // Error

Valid:

namespace MyApp.Workflows;
[Workflow("my-workflow")]
public static partial class MyWorkflow { }

PropertyValue
SeverityError
CategoryDSL Completeness

Every workflow must begin with StartWith<T>() to define the entry point.

Invalid:

Workflow<State>.Create("bad-workflow")
.Then<FirstStep>() // Error: Should be StartWith
.Finally<LastStep>();

Valid:

Workflow<State>.Create("good-workflow")
.StartWith<FirstStep>()
.Finally<LastStep>();

PropertyValue
SeverityWarning
CategoryDSL Completeness

Workflows should end with Finally<T>() to mark completion.

Trigger:

Workflow<State>.Create("incomplete-workflow")
.StartWith<FirstStep>()
.Then<SecondStep>(); // Warning: No Finally

Resolution:

Workflow<State>.Create("complete-workflow")
.StartWith<FirstStep>()
.Then<SecondStep>()
.Finally<CompletionStep>();

::: info Why Warning (not Error) Some patterns intentionally short-circuit via Complete() in branches. The DSL allows this, so we warn rather than block. :::


PropertyValue
SeverityError
CategoryStructural Validation

Every Fork() construct must be followed by Join<T>() to merge parallel paths.

Invalid:

.StartWith<PrepareStep>()
.Fork(
path => path.Then<PathA>(),
path => path.Then<PathB>())
.Then<NextStep>() // Error: Fork not followed by Join
.Finally<EndStep>();

Valid:

.StartWith<PrepareStep>()
.Fork(
path => path.Then<PathA>(),
path => path.Then<PathB>())
.Join<MergeResultsStep>() // Correct: Join after Fork
.Finally<EndStep>();

PropertyValue
SeverityError
CategoryStructural Validation

RepeatUntil loops must contain at least one step in their body.

Invalid:

.StartWith<InitStep>()
.RepeatUntil(s => s.Done, "process", loop => { }) // Error: Empty body
.Finally<EndStep>();

Valid:

.StartWith<InitStep>()
.RepeatUntil(s => s.Done, "process", loop => loop
.Then<ProcessItemStep>()
.Then<CheckProgressStep>())
.Finally<EndStep>();

PropertyValue
SeverityError
CategoryState Reducer

The [Append] attribute can only be applied to collection types (e.g., List<T>, IList<T>).

Invalid:

[WorkflowState]
public record State
{
[Append]
public string Name { get; init; } // Error: Not a collection
}

Valid:

[WorkflowState]
public record State
{
[Append]
public List<string> Items { get; init; }
}

PropertyValue
SeverityError
CategoryState Reducer

The [Merge] attribute can only be applied to dictionary types (e.g., Dictionary<TKey, TValue>).

Invalid:

[WorkflowState]
public record State
{
[Merge]
public List<string> Items { get; init; } // Error: Not a dictionary
}

Valid:

[WorkflowState]
public record State
{
[Merge]
public Dictionary<string, int> Scores { get; init; }
}

Ensures the workflow definition has all required components:

  • Valid name (AGWF001)
  • Proper namespace (AGWF004)
  • Entry point via StartWith (AGWF009)
  • Exit point via Finally (AGWF010)
  • At least one step (AGWF002)

Validates the logical structure of the workflow:

  • No ambiguous step references (AGWF003)
  • Proper construct pairing like Fork/Join (AGWF012)
  • Non-empty constructs like loop bodies (AGWF014)

Validates state attribute usage:

  • [Append] on collections (AGSR001)
  • [Merge] on dictionaries (AGSR002)

SeverityMeaningCode Generation
ErrorWorkflow cannot execute correctlyBlocked
WarningPattern may be intentional but warrants reviewAllowed