Draft -> In Progress -> Done is one workflow. Not the only one.
Every tool ships with a default lifecycle. Three stages, two transitions, zero nuance. It works until it doesn't -- which is roughly the second week of any project with compliance requirements, multiple teams, or a QA department that actually reads the acceptance criteria.
tspec treats workflows as first-class citizens. You define the stages, the transitions, and the gates that block those transitions. The scanner enforces the gates. The compiler enforces the scanner. The chain is unbroken from definition to deployment.
The Problem: One Workflow Fits None
A startup shipping an MVP does not need the same ceremony as a team building avionics software. A maintenance squad triaging production bugs has different state transitions than a product team running SAFe PI planning.
Yet most requirements tools give you one workflow. Maybe two if you pay for enterprise. You bend your process to fit the tool instead of the other way around.
The consequences are predictable. Teams skip stages that don't apply. They mark things "Done" when they mean "Done enough for our actual process." The workflow becomes decoration -- present in the UI, absent from reality.
What you need is a workflow engine that is both flexible enough for any process and strict enough to enforce the parts that matter. Specifically: the coverage gates that prevent a feature from reaching Done until every acceptance criterion is verified.
The Dual Authoring Surface
tspec provides two ways to define workflows. Same state machine engine underneath, different authoring surfaces on top.
.NET teams use the CMF Workflow DSL -- C# attributes on a partial class. The source generator reads the attributes and emits a fully typed state machine with transition methods, guard checks, and coverage validation built in.
Everyone else uses YAML workflow definitions. The YAML is parsed at build time, validated against the same schema, and produces the same state machine artifacts. A TypeScript team, a documentation team, a product owner who has never opened Visual Studio -- they all get the same workflow engine.
The YAML surface exists because workflows are organizational decisions, not implementation decisions. The person defining the process should not need to know C#.
YAML Definition
Here is the full feature-lifecycle workflow -- seven stages, eight transitions, three coverage gates:
workflow: feature-lifecycle
stages:
- name: Draft
initial: true
- name: Proposed
- name: Approved
- name: InProgress
- name: Review
- name: Done
final: true
- name: Blocked
transitions:
- name: propose
from: Draft
to: Proposed
requires: [Author]
- name: approve
from: Proposed
to: Approved
requires: [ProductOwner]
gate: allACsDefined
- name: start
from: Approved
to: InProgress
- name: submit
from: InProgress
to: Review
gate: coverageAbove80
- name: complete
from: Review
to: Done
requires: [QALead]
gate: coverageIs100
- name: reject
from: Review
to: InProgress
- name: block
from: InProgress
to: Blocked
- name: unblock
from: Blocked
to: InProgress
gates:
allACsDefined:
type: acCount
min: 1
coverageAbove80:
type: coverage
min: 80
coverageIs100:
type: coverage
min: 100workflow: feature-lifecycle
stages:
- name: Draft
initial: true
- name: Proposed
- name: Approved
- name: InProgress
- name: Review
- name: Done
final: true
- name: Blocked
transitions:
- name: propose
from: Draft
to: Proposed
requires: [Author]
- name: approve
from: Proposed
to: Approved
requires: [ProductOwner]
gate: allACsDefined
- name: start
from: Approved
to: InProgress
- name: submit
from: InProgress
to: Review
gate: coverageAbove80
- name: complete
from: Review
to: Done
requires: [QALead]
gate: coverageIs100
- name: reject
from: Review
to: InProgress
- name: block
from: InProgress
to: Blocked
- name: unblock
from: Blocked
to: InProgress
gates:
allACsDefined:
type: acCount
min: 1
coverageAbove80:
type: coverage
min: 80
coverageIs100:
type: coverage
min: 100Read it top to bottom. A feature starts in Draft. It moves to Proposed when the author submits it. A ProductOwner can approve it -- but only if at least one acceptance criterion is defined. Work begins. When a developer submits for review, the coverage gate checks that 80% of ACs have matching implementations. The QA lead can mark it Done only when coverage hits 100%.
Blocked is a parking state. Features go in, features come out. No gates required because blocking is never a quality problem -- staying blocked is.
C# DSL Equivalent
The same workflow expressed with CMF Workflow DSL attributes:
[Workflow("FeatureLifecycle", Description = "Standard feature workflow")]
[Stage("Draft", IsInitial = true)]
[Stage("Proposed")]
[Stage("Approved")]
[Stage("InProgress")]
[Stage("Review")]
[Stage("Done", IsFinal = true)]
[Stage("Blocked")]
[Transition("Propose", From = "Draft", To = "Proposed")]
[Transition("Approve", From = "Proposed", To = "Approved")]
[Transition("Start", From = "Approved", To = "InProgress")]
[Transition("Submit", From = "InProgress", To = "Review")]
[Transition("Complete", From = "Review", To = "Done")]
[Transition("Reject", From = "Review", To = "InProgress")]
[RequiresRole("Approve", Role = "ProductOwner")]
[RequiresRole("Complete", Role = "QALead")]
[RequiresCoverage("Submit", MinPercentage = 80)]
[RequiresCoverage("Complete", MinPercentage = 100)]
[RequiresACCount("Approve", Min = 1)]
public partial class FeatureLifecycleWorkflow { }[Workflow("FeatureLifecycle", Description = "Standard feature workflow")]
[Stage("Draft", IsInitial = true)]
[Stage("Proposed")]
[Stage("Approved")]
[Stage("InProgress")]
[Stage("Review")]
[Stage("Done", IsFinal = true)]
[Stage("Blocked")]
[Transition("Propose", From = "Draft", To = "Proposed")]
[Transition("Approve", From = "Proposed", To = "Approved")]
[Transition("Start", From = "Approved", To = "InProgress")]
[Transition("Submit", From = "InProgress", To = "Review")]
[Transition("Complete", From = "Review", To = "Done")]
[Transition("Reject", From = "Review", To = "InProgress")]
[RequiresRole("Approve", Role = "ProductOwner")]
[RequiresRole("Complete", Role = "QALead")]
[RequiresCoverage("Submit", MinPercentage = 80)]
[RequiresCoverage("Complete", MinPercentage = 100)]
[RequiresACCount("Approve", Min = 1)]
public partial class FeatureLifecycleWorkflow { }The source generator emits FeatureLifecycleWorkflow.TryPropose(), FeatureLifecycleWorkflow.TryApprove(), and so on. Each method checks the current state, evaluates the gate, and returns a result with either the new state or a structured error explaining why the transition was blocked.
Same semantics. Different syntax. Pick the one your team reads.
Gate Types
Gates are the enforcement mechanism. A transition without a gate is a suggestion. A transition with a gate is a rule.
| Gate | What It Checks |
|---|---|
| coverage | Blocks the transition unless the latest scan shows at least N% of acceptance criteria covered by implementations and tests. |
| acCount | Blocks unless the feature has at least N acceptance criteria defined. Prevents empty features from advancing. |
| role | Only users with the specified role can trigger the transition. Structural, not coverage-related, but essential for process control. |
| approval | Requires N distinct approvers to have signed off before the transition fires. For regulated environments where one person cannot approve their own work. |
| timeInStage | Minimum duration before the transition is allowed. Prevents rush-to-done patterns where a feature moves from InProgress to Review in minutes. |
| custom | An expression evaluated against the scan JSON. For gates that don't fit the built-in types -- external API checks, dependency status, or deployment prerequisites. |
Gates compose. A single transition can require a role, a coverage threshold, and two approvals. All must pass. Any failure blocks the transition and returns a structured explanation.
Coverage-Gated Transitions -- The Key Insight
This is where the compiler-enforced chain from Part II extends into workflow.
You cannot mark a feature "Done" until the scanner says 100%. Not "the developer says 100%." Not "the PM says 100%." The scanner -- which reads the source code, finds the [Implements] and [FeatureTest] attributes, and counts coverage against the declared acceptance criteria.
The gate reads the latest scan result from the Diem API. If coverage is below the threshold, the transition is blocked. The dashboard shows exactly why: which acceptance criteria are missing implementations, which have implementations but no tests, which tests exist but are failing.
This closes the loop that most tools leave open. Requirements are defined. Acceptance criteria are declared. Code is linked to criteria via typed attributes. Tests are linked to criteria. The scanner counts everything. The workflow gate reads the count. Done means done.
Pre-Built Workflow Templates
Not every team needs to author a workflow from scratch. tspec ships with templates that cover the most common patterns:
| Template | Stages | Gates | Use Case |
|---|---|---|---|
| Agile | Draft -> InProgress -> Review -> Done | Coverage 80%/100% | Small teams, quick iteration |
| SAFe | Draft -> Proposed -> Approved -> InProgress -> Review -> Done | Role + Coverage + Approval | Scaled teams, PI planning |
| Regulatory | Draft -> Proposed -> Approved -> InProgress -> QA -> Compliance -> Done | Coverage 100% + 2 approvers + compliance sign-off | Healthcare, finance, aerospace |
| Startup | Draft -> InProgress -> Done | None (trust the team) | Minimum ceremony |
| Maintenance | Triage -> Diagnose -> Fix -> Verify -> Done | Regression test required | Bug-fix and support mode |
Templates are starting points. Fork one, add a stage, change a gate threshold, remove a role requirement. The YAML is yours.
Before and After
Without workflow integration, quality enforcement is a binary exit code:
With tspec workflows, enforcement is continuous and staged:
The difference is not just more stages. It is that each stage transition carries proof. The feature did not arrive at Done because someone clicked a button. It arrived because the code, the tests, and the scanner all agree.
Previous: Part III: TypeScript Hierarchy Redesign | Next: Part V: Language Backends