ZenWave Flow Language (ZFL)
A domain specific language (DSL) for modeling business flows discovered with Event Storming.
ZFL gives Event Storming findings a structured home: readable by humans, parseable by tools, and stable enough to version, review, search, and generate from. - ZenWave 360
Guide: How and Why to Use ZFL
What is ZenWave Flow Language
ZFL is a language for describing the flow of business events, commands, policies, systems, and outcomes across one or more Bounded Contexts.
Where ZDL describes the inside of a Bounded Context, ZFL describes the flow that crosses boundaries. It is the textual counterpart of an Event Storming board.
ZFL is designed to be compact, readable, and precise enough for tooling. It works well when you want to keep the business story visible while also giving generators, agents, and architecture tools a machine-readable model.
For a narrative walkthrough of translating an Event Storming board into ZFL, see From Event Storming to ZFL.
What Problems ZFL Solves
- Capturing Event Storming findings in a format that can be versioned and reviewed
- Making policies explicit with
when ... do ...blocks - Keeping happy paths, failure paths, retries, compensations, and timeouts visible
- Connecting flow steps to the systems and services that own them
- Providing enough structure for downstream generation of ZDL models, AsyncAPI definitions, tests, documentation, and architecture views
How ZFL Fits into a Real Project
- Event Storming discovers the flow as events, commands, policies, actors, and time-based rules
- ZFL turns that discovery into a durable flow model
- Systems and services in ZFL connect the flow to Bounded Contexts and ZDL files
- ZDL then describes each Bounded Context in detail
- ZenWave SDK and AI agents can use both models to generate consistent implementation artifacts
ZFL and ZDL are complementary:
| Language | Main Question | Typical Scope |
|---|---|---|
| ZFL | What happens across the business flow? | Cross-context process, policies, events, commands, systems |
| ZDL | What is inside this Bounded Context? | Aggregates, entities, services, commands, events, APIs |
Minimal Complete ZFL Example
This example captures a very small checkout flow: a customer starts checkout, the system authorizes payment, and the flow ends either completed or cancelled.
flow CheckoutFlow {@actor(Customer)start StartCheckout {orderId String}when StartCheckout do authorizePayment {service Payments.PaymentServiceemits PaymentAuthorizedemits PaymentDeclined}when PaymentDeclined do cancelOrder {service Orders.OrderServiceemits OrderCancelled}end {completed: PaymentAuthorizedcancelled: OrderCancelled}}
Building a Flow Incrementally with ZFL
Starts and Actors
A start block describes something that opens the flow. It can be initiated by a user, an external system, a scheduler, or any other actor.
@actor(Customer)start StartOrderCheckout {items SKU[]}
@actor(Customer) tells readers who starts the flow. The body describes the data that enters the flow.
Policies
Policies are the main building block of ZFL. They translate the Event Storming pattern "when this happens, do that".
when OrderCreated do authorizePayment {service PaymentsProcessing.PaymentsProcessingServiceemits PaymentAuthorizedemits PaymentDeclinedemits PaymentFailed}
The left side is the trigger. The right side is the command. The body lists the service responsible for the work and the possible outcomes.
Branches
Business flows rarely have only one outcome. ZFL keeps branches explicit by naming each event that a command can emit.
when PaymentFailed do retryPayment {service PaymentsProcessing.PaymentsProcessingServiceemits PaymentRetriedemits PaymentRetryExhausted}when PaymentDeclined, PaymentRetryExhausted do releaseStock {service CatalogProducts.CatalogProductsServiceemits StockReleased}
Multiple triggers can be separated by commas, pipes, or and depending on the relationship you want to express.
Direct Calls and Responses
Most flow steps are modeled as event-to-command policies. Sometimes a command needs an immediate response from another command. ZFL uses call for that.
when StartOrderCheckout do startOrderCheckout {service OrdersCheckout.OrdersCheckoutServicecall reserveStockon StockReserved emits OrderCreatedon StockUnavailable emits StockUnavailable}do reserveStock {service CatalogProducts.CatalogProductsServiceemits response StockReservedresponse StockUnavailable}
Use call when the caller waits for an answer. Use response for outcomes returned directly to the caller. Use emits response when the same outcome is both returned synchronously and published as an event.
Time-Based Policies
Timers and deadlines are part of the business flow. ZFL models them with annotations on start blocks.
@actor(Scheduler)@time("10 minutes after OrderCreated and not PaymentAuthorized or PaymentDeclined or PaymentRetryExhausted")start ReservationExpired {orderId String}when ReservationExpired do releaseStock {service CatalogProducts.CatalogProductsServiceemits StockReleased}
The timer becomes a named trigger that can participate in the rest of the flow like any other event.
Compensations
Compensating actions can be documented with annotations. The syntax is intentionally lightweight, so teams can keep the business reason visible without forcing an implementation strategy too early.
@compensates(reserveStock)when PaymentDeclined, PaymentRetryExhausted, ReservationExpired do releaseStock {service CatalogProducts.CatalogProductsServiceemits StockReleased}
End States
Every flow should make its terminal outcomes explicit.
end {completed: PaymentCapturedstockGone: StockUnavailableNotificationSentorderCancelled: OrderCancelledNotificationSent}
Each key is a business-friendly outcome name. Each value is an event that marks that outcome.
Systems
The optional systems block connects the flow to the systems and services that own the work.
systems {@zdl("orders-checkout-api/domain-model.zdl")OrdersCheckout {service OrdersCheckoutService}@zdl("payments-processing-api/domain-model.zdl")PaymentsProcessing {service PaymentsProcessingService}}
You can start without systems while the Event Storming model is still fluid. Add them later when Bounded Contexts and ownership become clear.
Service references inside flow steps use the same names:
when OrderCreated do authorizePayment {service PaymentsProcessing.PaymentsProcessingServiceemits PaymentAuthorized}
Complete Example
This is a shortened version of the Arcadia Editions PlaceOrderFlow.
systems {@zdl("catalog-products-api/domain-model.zdl")CatalogProducts {service CatalogProductsService for (Product)}@zdl("orders-checkout-api/domain-model.zdl")OrdersCheckout {service OrdersCheckoutService}@zdl("payments-processing-api/domain-model.zdl")PaymentsProcessing {service PaymentsProcessingService}}flow PlaceOrderFlow {@actor(Customer)start StartOrderCheckout {items SKU[]}when StartOrderCheckout do startOrderCheckout {service OrdersCheckout.OrdersCheckoutServicecall reserveStockon StockReserved emits OrderCreatedon StockUnavailable emits StockUnavailable}do reserveStock {service CatalogProducts.CatalogProductsServiceemits response StockReservedresponse StockUnavailable}when OrderCreated do authorizePayment {service PaymentsProcessing.PaymentsProcessingServiceemits PaymentAuthorizedemits PaymentDeclinedemits PaymentFailed}@actor(Scheduler)@time("10 minutes after OrderCreated and not PaymentAuthorized or PaymentDeclined or PaymentRetryExhausted")start ReservationExpired {orderId String}end {completed: PaymentCapturedstockGone: StockUnavailableorderCancelled: OrderCancelled}}
Grammar & Reference
The sections below describe the ZFL language in detail. They are intended as a reference for syntax and structure, not as a step-by-step guide.
File Structure
ZFL files follow a structured format:
File Types and Extensions
.zflfiles - Flow model files that describe business flows, systems, policies, commands, events, and terminal outcomes
1. Imports Section (Optional)
Imports can be declared at the top of the file.
@import("shared-types.zfl")@import(name: "shared-types.zfl")
2. Configuration Section (Optional)
The config block can contain options for tooling.
config {namespace "arcadia"}
3. Systems Section (Optional)
The systems block describes participating systems and their services.
systems {OrdersCheckout {service OrdersCheckoutService}}
4. Flow Definitions
A file can contain one or more flow blocks.
flow PlaceOrderFlow {start StartOrderCheckout {items SKU[]}when StartOrderCheckout do startOrderCheckout {emits OrderCreated}end {completed: OrderCreated}}
Syntax Pattern
[<import>*][<config>][<systems>][<flow>*]
Annotations
Annotations decorate systems, services, starts, policies, commands, fields, and flow elements.
@actor(Customer)@time("10 minutes after OrderCreated")@compensates(reserveStock)
Annotation values can be simple values, arrays, objects, or key-value pairs depending on the annotation.
Comments
ZFL supports standard comment styles:
// line comment/* block comment *//** documentation comment */
Documentation comments are useful for preserving business context directly in the model.
/*** Payment is captured only when the order is ready to ship.*/when FulfillmentScheduled do capturePayment {emits PaymentCaptured}
Fields
start blocks can declare fields using a compact name-type syntax.
start StartOrderCheckout {orderId Stringitems SKU[]couponCode String?}
Field types can be custom identifiers. Arrays use []. Optional fields use ?.
Available validations:
required,uniquemin,maxminlength,maxlengthemailpattern
Flows
A flow groups starts, policies, command definitions, and end states.
flow <FlowName> {[<start> | <when> | <do> | <end>]*}
Starts
Starts are triggers that open or inject work into a flow.
[<javadoc>][<annotation>*]start <StartName> {[<field>*]}
When Blocks
When blocks model event-to-command policies.
[<javadoc>][<annotation>*]when <Trigger> do <commandName> {[service <SystemName>.<ServiceName>][call <commandName>][on <EventName> call <commandName>][on <EventName> emits <EventName>][emits <EventName>][emits response <EventName>][response <EventName>]}
Triggers can be grouped:
when PaymentDeclined, PaymentRetryExhausted do releaseStock {emits StockReleased}when InventoryReserved and PaymentAuthorized do confirmOrder {emits OrderConfirmed}
Do Blocks
Do blocks define commands that can be called directly or documented independently.
do reserveStock {service CatalogProducts.CatalogProductsServiceemits response StockReservedresponse StockUnavailable}
Service References
Service references connect a flow command to the service that owns it.
service OrdersCheckout.OrdersCheckoutService
Inside a systems block, services can optionally declare the aggregates they work with.
service CatalogProductsService for (Product, StockReservation)
End Blocks
End blocks name the terminal outcomes of a flow.
end {<outcomeName>: <EventName>[, <EventName>]*}
Example:
end {completed: PaymentCapturedcancelled: OrderCancelled}
Modeling Guidelines
- Use past-tense names for events:
OrderCreated,PaymentCaptured - Use imperative command names:
authorizePayment,releaseStock - Start with the flow before assigning systems if ownership is not clear yet
- Add
systemsandservicereferences when Bounded Contexts are discovered - Keep failure paths explicit instead of hiding them in prose
- Use
callonly when the caller needs an immediate response - Use
endto make the terminal business outcomes visible
ZFL to ZDL
ZFL is often the input for creating ZDL models.
The mapping is not purely mechanical, but the structure gives strong guidance:
- Each system can become a Bounded Context or service repository
- Each service reference identifies the command surface owned by that system
- Each emitted event can become a ZDL
event - Each
startor command payload can become a ZDLinput - Flow sequencing can suggest aggregate lifecycle transitions
ZFL tells the story of the whole flow. ZDL describes each part in detail.
Grammar Source
For code generators, programming agents, and tools that need the exact language syntax, the canonical grammar source is available in the ZenWave DSL repository: