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.
Diagnostic Namespaces
Section titled “Diagnostic Namespaces”| Prefix | Category | Description |
|---|---|---|
| AGWF | Workflow | Workflow definition validation |
| AGSR | State Reducer | State reducer attribute validation |
Quick Reference
Section titled “Quick Reference”| Code | Severity | Description |
|---|---|---|
| AGWF001 | Error | Workflow name cannot be empty |
| AGWF002 | Warning | No steps found in workflow |
| AGWF003 | Error | Duplicate step name |
| AGWF004 | Error | Invalid namespace (global namespace not supported) |
| AGWF009 | Error | Workflow must begin with StartWith<T>() |
| AGWF010 | Warning | Workflow should end with Finally<T>() |
| AGWF012 | Error | Fork must be followed by Join<T>() |
| AGWF014 | Error | Loop body cannot be empty |
| AGWF039 | Error | Bound workflow identity does not resolve exactly once |
| AGWF040 | Error | Reachable step action identity is missing, invalid, dynamic, or does not resolve exactly once |
| AGWF041 | Error | A closed workflow contract definitely fails behavioral refinement |
| AGWF042 | Error | A workflow contract cannot be proved statically |
| AGWF043 | Error | Workflow identities collide after generated-name normalization |
| AGWF044 | Error | An authored compensation action disagrees with the derived inverse |
| AGWF045 | Error | A compensation scope is not mechanically derivable |
| AGWF046 | Error | A declared binding’s contract cannot be exported for a referencing compilation |
| AGWF047 | Error | A referenced assembly’s proof catalog cannot be read |
| AGWF048 | Error | Two assemblies declare one ordinal action identity |
| AGWF049 | Error | A closed-enum wire slot carries a value outside its vocabulary |
| AGSR001 | Error | [Append] can only be applied to collection types |
| AGSR002 | Error | [Merge] can only be applied to dictionary types |
Workflow binding diagnostics
Section titled “Workflow binding diagnostics”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:
| Code | Cause | Fix |
|---|---|---|
AGWF039 | The 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. |
AGWF040 | A 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. |
AGWF041 | All 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. |
AGWF042 | A 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. |
AGWF043 | Two 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. |
AGWF044 | A 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. |
AGWF045 | A 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. |
AGWF046 | This 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. |
AGWF047 | A 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. |
AGWF048 | One 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. |
Across an assembly boundary
Section titled “Across an assembly boundary”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.
Closed-enum wire slots
Section titled “Closed-enum wire slots”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.
| Code | Cause | Fix |
|---|---|---|
AGWF049 | A 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.
Workflow Diagnostics (AGWF)
Section titled “Workflow Diagnostics (AGWF)”AGWF001: Empty Workflow Name
Section titled “AGWF001: Empty Workflow Name”| Property | Value |
|---|---|
| Severity | Error |
| Category | DSL Completeness |
The [Workflow] attribute requires a non-empty workflow name.
Invalid:
[Workflow("")] // Error: Empty name[Workflow(" ")] // Error: Whitespace onlyValid:
[Workflow("process-order")]AGWF002: No Steps Found
Section titled “AGWF002: No Steps Found”| Property | Value |
|---|---|
| Severity | Warning |
| Category | DSL 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>();AGWF003: Duplicate Step Name
Section titled “AGWF003: Duplicate Step Name”| Property | Value |
|---|---|
| Severity | Error |
| Category | Structural 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: DuplicateResolution: 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. :::
AGWF004: Invalid Namespace
Section titled “AGWF004: Invalid Namespace”| Property | Value |
|---|---|
| Severity | Error |
| Category | DSL 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 { } // ErrorValid:
namespace MyApp.Workflows;
[Workflow("my-workflow")]public static partial class MyWorkflow { }AGWF009: Missing StartWith
Section titled “AGWF009: Missing StartWith”| Property | Value |
|---|---|
| Severity | Error |
| Category | DSL 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>();AGWF010: Missing Finally
Section titled “AGWF010: Missing Finally”| Property | Value |
|---|---|
| Severity | Warning |
| Category | DSL Completeness |
Workflows should end with Finally<T>() to mark completion.
Trigger:
Workflow<State>.Create("incomplete-workflow") .StartWith<FirstStep>() .Then<SecondStep>(); // Warning: No FinallyResolution:
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.
:::
AGWF012: Fork without Join
Section titled “AGWF012: Fork without Join”| Property | Value |
|---|---|
| Severity | Error |
| Category | Structural 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>();AGWF014: Loop without Body
Section titled “AGWF014: Loop without Body”| Property | Value |
|---|---|
| Severity | Error |
| Category | Structural 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>();State Reducer Diagnostics (AGSR)
Section titled “State Reducer Diagnostics (AGSR)”AGSR001: Invalid Append Target
Section titled “AGSR001: Invalid Append Target”| Property | Value |
|---|---|
| Severity | Error |
| Category | State 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; }}AGSR002: Invalid Merge Target
Section titled “AGSR002: Invalid Merge Target”| Property | Value |
|---|---|
| Severity | Error |
| Category | State 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; }}Diagnostic Categories
Section titled “Diagnostic Categories”DSL Completeness
Section titled “DSL Completeness”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)
Structural Validation
Section titled “Structural Validation”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)
State Reducer
Section titled “State Reducer”Validates state attribute usage:
[Append]on collections (AGSR001)[Merge]on dictionaries (AGSR002)
Error vs Warning
Section titled “Error vs Warning”| Severity | Meaning | Code Generation |
|---|---|---|
| Error | Workflow cannot execute correctly | Blocked |
| Warning | Pattern may be intentional but warrants review | Allowed |