Getting Started
This is the quickest path to a working durable workflow in Stem.
1. Create a workflow app
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
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
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
timedOutflag when the caller stops waiting before the run finishes
4. Reuse existing bootstrap when needed
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
- If you need a mental model first, read Flows and Scripts.
- If you want the decorator/codegen path, read Annotated Workflows.
- If you need to suspend and resume runs, read Suspensions and Events.