Skip to main content

Getting Started

This is the quickest path to a working durable workflow in Stem.

1. Create a workflow app

bin/workflows.dart
  final client = await StemClient.fromUrl(
'redis://127.0.0.1:56379',
adapters: const [StemRedisAdapter(), StemPostgresAdapter()],
overrides: const StemStoreOverrides(
backend: 'redis://127.0.0.1:56379/1',
workflow: 'postgresql://<user>:<password>@127.0.0.1:65432/stem',
),
);
final workflowApp = await client.createWorkflowApp(
flows: [ApprovalsFlow.flow],
scripts: [retryScript],
eventBusFactory: WorkflowEventBusFactory.inMemory(),
workerConfig: const StemWorkerConfig(queue: 'workflow'),
);

Pass normal task handlers through tasks: if the workflow also needs to enqueue regular Stem tasks.

If you need separate workflow lanes, pass continuationQueue: and executionQueue: into client.createWorkflowApp(...). When the app is creating the managed worker for you, those queue names are inferred into the worker subscription automatically.

2. Start the managed worker

bin/workflows.dart
  await workflowApp.start();

StemWorkflowApp.start() starts both the runtime and the underlying worker. The managed worker subscribes to the workflow orchestration queue, so you do not need to manually register the internal stem.workflow.run task.

If you prefer a minimal example, startWorkflow(...), startWorkflowValue(...), and startWorkflowJson(...) also lazy-start the runtime and managed worker on first use. Explicit start() is still the better choice when you want deterministic application lifecycle control. Use those name-based APIs when workflow names come from config or external input. For workflows you define in code, prefer direct workflow helpers or generated workflow refs.

3. Start a run and wait for the result

bin/run_workflow.dart
Future<void> runWorkflow(StemWorkflowApp workflowApp) async {
final runId = await ApprovalsFlow.ref.start(
workflowApp,
params: const ApprovalDraft(documentId: 'doc-42'),
cancellationPolicy: const WorkflowCancellationPolicy(
maxRunDuration: Duration(hours: 2),
maxSuspendDuration: Duration(minutes: 30),
),
);

final result = await ApprovalsFlow.ref.waitFor(
workflowApp,
runId,
timeout: const Duration(minutes: 5),
);

if (result?.isCompleted == true) {
print('Workflow finished with ${result!.requiredValue()}');
} else {
print('Workflow state: ${result?.status}');
}
}

The returned WorkflowResult<T> includes:

  • the decoded value
  • the persisted RunState
  • a timedOut flag when the caller stops waiting before the run finishes

4. Reuse existing bootstrap when needed

bin/workflows_client.dart
Future<void> bootstrapWorkflowClient() async {
final client = await StemClient.fromUrl('memory://', module: stemModule);
final app = await client.createWorkflowApp();
await app.close();
await client.close();
}

Use StemClient when one service wants to own broker, backend, and workflow setup in one place. The clean path there is client.createWorkflowApp(...).

If your service already owns a StemApp, layer workflows on top of it with stemApp.createWorkflowApp(...). That path reuses the current worker, so the underlying app must already subscribe to the workflow queue plus the task queues your workflows need.

For late registration, use the app helpers instead of reaching through the runtime registry:

  • registerWorkflow(...) / registerWorkflows(...)
  • registerFlow(...) / registerFlows(...)
  • registerScript(...) / registerScripts(...)
  • registerModule(...) / registerModules(...)

If you are registering raw WorkflowDefinition values directly, prefer WorkflowDefinition.flowJson(...) / .scriptJson(...) for the common DTO path, WorkflowDefinition.flowVersionedJson(...) / .scriptVersionedJson(...) when the stored result should carry an explicit schema version, and WorkflowDefinition.flowCodec(...) / .scriptCodec(...) when the result needs a custom codec.

5. Move to the right next page