Quick Start
Build and test a definition-first durable approval with WorkflowRuntime.
What You Will Build
- Reuse schemas at workflow and operation boundaries.
- Declare a
Workflowand implement itsWorkflowHandler. - Execute an
Operationthrough an effectful handler. - Request assigned work and wait for the actor's response.
- Verify the terminal result with
WorkflowTestHarness.
1. Describe the Payloads
import 'package:json_schema_builder/json_schema_builder.dart' as json_schema;
final orderSchema = Schema<Order>(
name: 'orders.order',
jsonSchema: json_schema.S.object(
properties: {
'id': json_schema.S.string(minLength: 1),
'tags': json_schema.S.list(items: json_schema.S.string()),
},
required: const ['id'],
additionalProperties: false,
),
encode: (value) => value.toJson(),
decode: Order.fromJson,
);
final reservationSchema = Schema<Reservation>(
name: 'orders.reservation',
jsonSchema: json_schema.S.object(
properties: {
'id': json_schema.S.string(minLength: 1),
'orderId': json_schema.S.string(minLength: 1),
},
required: const ['id', 'orderId'],
additionalProperties: false,
),
encode: (value) => value.toJson(),
decode: Reservation.fromJson,
);
final resultSchema = Schema<Result>(
name: 'orders.result',
jsonSchema: json_schema.S.object(
properties: {
'reservationId': json_schema.S.string(minLength: 1),
'approved': json_schema.S.boolean(),
},
required: const ['reservationId', 'approved'],
additionalProperties: false,
),
encode: (value) => value.toJson(),
decode: Result.fromJson,
);
final failureSchema = Schema<OrderFailure>(
name: 'orders.failure',
jsonSchema: json_schema.S.object(
properties: {
'code': json_schema.S.string(minLength: 1),
'message': json_schema.S.string(minLength: 1),
},
required: const ['code', 'message'],
additionalProperties: false,
),
encode: (value) => value.toJson(),
decode: OrderFailure.fromJson,
);Schema<T> is a domain-neutral wrapper around a typed Dart codec and a json_schema_builder.Schema. The same schema can be reused by an entity, command, event, configuration, stored record, workflow, or operation. For annotated models, @JsonSerializable(createJsonSchema: true) can generate the codec and JSON Schema together.
Every custom Schema<T> requires jsonSchema. For JSON-native values, Schema.identity should normally receive an explicit structural schema; omitting it deliberately accepts any JSON value. Use Schema.never for the failure seam of an infallible workflow or operation.
The runtime validates before every decode and after every encode. A malformed API input is rejected before a run is created, and malformed workflow, operation, work, event, query, child-workflow, or expected-failure output is rejected before persistence or delivery.
2. Declare the Definitions
final reserve = Operation<
Order,
Reservation,
OrderFailure
>(
name: 'orders.reserve',
version: 1,
input: orderSchema,
output: reservationSchema,
failure: failureSchema,
);
final approve = Work<Reservation, Approval>(
name: 'orders.approve',
version: 1,
input: reservationSchema,
response: approvalSchema,
);
final orderApprovalDefinition = Workflow<
Order,
Result,
OrderFailure
>(
code: 'orders.approval',
version: 1,
input: orderSchema,
output: resultSchema,
failure: failureSchema,
title: 'Order approval',
);Definitions contain identity and payload boundaries. They do not contain orchestration or side effects.
3. Implement the Workflow Handler
final orderApprovalHandler = orderApprovalDefinition.implement(
fingerprint: 'orders.approval:v1',
execute: (workflow, order) async {
final reservation = await workflow.perform(
reserve,
order,
id: 'reserve',
schedule: WorkflowSchedule.exponential(
const Duration(seconds: 1),
attempts: 3,
),
idempotencyKey: 'reserve:${order.id}',
);
final decision = await workflow.request(
approve,
reservation,
id: 'approve',
title: 'Approve order',
audience: const Audience(
roleIds: ['order-approver'],
),
);
return decision.approved ? Result.approved() : Result.rejected();
},
);The workflow handler is replayed, so it must remain deterministic. Normal Dart control flow is safe when it depends only on input and recorded durable facts.
4. Implement the Operation Handler
final ordersModule = WorkflowModule(
name: 'orders',
workflows: [orderApprovalHandler],
operations: [
reserve.implement((operation, order) async {
return reservationService.reserve(
order,
idempotencyKey: operation.context.idempotencyKey,
);
}),
],
work: [approve],
);Database calls, HTTP, clocks, randomness, and other effects belong in operation handlers. The durable idempotency key makes retries safe.
5. Test the Workflow
final test = WorkflowTestHarness(modules: [ordersModule]);
final run = await test.start(
orderApprovalDefinition.ref,
Order(id: 'ORD-001'),
);
await run.drain();
await run.respond(approve, Approval.approved());
final completed = await run.run;
expect(completed.status, WorkflowRunStatus.completed);
expect(
orderApprovalHandler.exitOf(completed),
isA<WorkflowSuccess<Result, OrderFailure>>(),
);Runtime Shape
The process may stop after any durable await. On restart, the runtime invokes the handler from the beginning, returns recorded results for completed awaits, and stops at the first unresolved operation.