Skip to content

Quick Start ​

Build and test a definition-first durable approval with WorkflowRuntime.

What You Will Build ​

  1. Reuse schemas at workflow and operation boundaries.
  2. Declare a Workflow and implement its WorkflowHandler.
  3. Execute an Operation through an effectful handler.
  4. Request assigned work and wait for the actor's response.
  5. Verify the terminal result with WorkflowTestHarness.

1. Describe the Payloads ​

dart
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 ​

dart
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 ​

dart
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 ​

dart
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 ​

dart
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.

Next Steps ​