Skip to main content

Starting and Waiting

Workflow runs are started through the runtime, through StemWorkflowApp, or through typed workflow refs.

Start by workflow name

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}');
}
}

Use params: to supply workflow input and WorkflowCancellationPolicy to cap wall-clock runtime or maximum suspension time.

That low-level API is useful when workflow names come from config or external input. For workflows you define in code, prefer a typed workflow ref instead.

Start through manual workflow refs

Manual Flow(...) and WorkflowScript(...) definitions can derive a typed ref without repeating the workflow-name string:

const approvalDraftCodec = PayloadCodec<ApprovalDraft>.json(
decode: ApprovalDraft.fromJson,
typeName: 'ApprovalDraft',
);

final approvalsRef = approvalsFlow.refCodec<ApprovalDraft>(
paramsCodec: approvalDraftCodec,
);

final runId = await approvalsRef.start(
workflowApp,
params: const ApprovalDraft(documentId: 'doc-42'),
);

final result = await approvalsRef.waitFor(workflowApp, runId);

Use this path when you want the same typed start/wait surface as generated workflow refs, but the workflow itself is still hand-written.

When you want to add advanced start options, keep using the direct typed ref helpers:

final runId = await approvalsRef.start(
workflowApp,
params: const ApprovalDraft(documentId: 'doc-42'),
parentRunId: 'parent-run',
ttl: const Duration(hours: 1),
cancellationPolicy: const WorkflowCancellationPolicy(
maxRuntime: Duration(minutes: 10),
),
);

refJson(...) is the shortest manual DTO path when the params already have toJson(), or when the final result also needs a Type.fromJson(...) decoder. Use refVersionedJson(...) when the persisted start payload should carry an explicit __stemPayloadVersion. Use refCodec(...) when you need a custom PayloadCodec<T>. Workflow params still need to encode to a string-keyed map (typically Map<String, dynamic>) because they are stored as JSON-shaped data. If the params need a custom map encoder and still need an explicit stored schema version, use refVersionedMap(...) / WorkflowRef.versionedMap(...).

Inside manual flow steps and script checkpoints, prefer ctx.param<T>() / ctx.requiredParam<T>() for workflow start params and ctx.previousValue<T>() / ctx.requiredPreviousValue<T>() over repeating raw previousResult as ... casts.

If the params stay unversioned but the stored result carries an explicit schema version, refJson(...) / WorkflowRef.json(...) also accept decodeResultVersionedJson: plus defaultDecodeVersion:.

If a manual flow or script returns a DTO, prefer Flow.json(...) or WorkflowScript.json(...) in the common toJson() / Type.fromJson(...) case. Use Flow.versionedJson(...) / WorkflowScript.versionedJson(...) when the stored result should carry an explicit schema version. Use Flow.codec(...) or WorkflowScript.codec(...) when the result needs a custom payload codec. If the result still needs a custom map encoder plus an explicit stored schema version, use Flow.versionedMap(...) / WorkflowScript.versionedMap(...). For manual typed refs, refVersionedJson(...) / WorkflowRef.versionedJson(...) also accept decodeResultVersionedJson: when the stored result should carry an explicit schema version. When the persisted workflow result or suspension payload carries an explicit __stemPayloadVersion, use workflowResult.payloadVersionedJson(...), runState.resultVersionedJson(...), or runState.suspensionPayloadVersionedJson(...) on the low-level snapshots. Those read-side helpers take defaultVersion: as the fallback for older payloads that do not yet carry a stored version marker.

For workflows without start params, start directly from the flow or script itself with start(...) or startAndWait(...). When you need an explicit low-level transport object for startWorkflowCall(...), build it with ref0().asRef.buildStart(params: ()). Treat WorkflowStartCall as the explicit low-level transport object, not the normal happy path. Use ref0() when another API specifically needs a NoArgsWorkflowRef.

When you need an explicit start request, prefer ref.buildStart(...) with the final overrides you already know.

Wait for completion

For workflows defined in code, prefer direct workflow helpers or typed refs like ordersFlow.startAndWait(...) and StemWorkflowDefinitions.orders.startAndWait(...).

waitForCompletion<T> is the low-level completion API for name-based runs. It polls the store until the run finishes or the caller times out. For DTO results, prefer decodeJson: for plain DTOs or decodeVersionedJson: when the persisted payload carries an explicit schema version. If you already have a raw WorkflowResult<Object?>, use result.payloadJson(...) or result.payloadAs(codec: ...) to decode the stored workflow result without another cast/closure. If you are inspecting the underlying RunState directly, use state.paramsJson(...), state.paramsAs(codec: ...), state.resultJson(...), state.resultAs(codec: ...), state.resultVersionedJson(...), state.suspensionPayloadJson(...), state.suspensionPayloadVersionedJson(...), state.lastErrorJson(...), state.runtimeJson(...), state.cancellationDataJson(...), or state.suspensionPayloadAs(codec: ...) instead of manual raw-map casts. Workflow run detail views expose the same convenience surface via runView.paramsJson(...), runView.paramsAs(codec: ...), runView.resultJson(...), runView.resultAs(codec: ...), runView.resultVersionedJson(...), runView.suspensionPayloadJson(...), runView.suspensionPayloadVersionedJson(...), runView.lastErrorJson(...), runView.runtimeJson(...), and runView.suspensionPayloadAs(codec: ...). Checkpoint entries from viewCheckpoints(...) and WorkflowCheckpointView.fromEntry(...) expose the same surface via entry.valueJson(...), entry.valueVersionedJson(...), and entry.valueAs(codec: ...).

Use the returned WorkflowResult<T> when you need:

  • value for a completed run
  • requiredValue() when completion is already guaranteed and you want a fail-fast typed read
  • status for partial progress
  • timedOut to decide whether to keep polling

Start through generated workflow refs

When you use stem_builder, generated workflow refs remove the raw workflow-name strings and give you one typed handle for both start and wait:

final result = await StemWorkflowDefinitions.userSignup.startAndWait(
workflowApp,
params: 'user@example.com',
);

The same definitions work on WorkflowRuntime by passing the runtime as the WorkflowCaller:

final runId = await StemWorkflowDefinitions.userSignup.start(
runtime,
params: 'user@example.com',
);

When you already have a WorkflowCaller like FlowContext, WorkflowScriptStepContext, WorkflowRuntime, or StemWorkflowApp, prefer the direct typed ref helpers, even when you need start overrides:

final result = await StemWorkflowDefinitions.userSignup.startAndWait(
context,
params: 'user@example.com',
ttl: const Duration(hours: 1),
timeout: const Duration(seconds: 5),
);

If you still need the run identifier for inspection or operator tooling, read it from result.runId.

Keep ref.buildStart(...) for the rarer cases where you need to assemble or adjust an explicit start request before dispatch.

Parent runs and TTL

WorkflowRuntime.startWorkflow(...), startWorkflowValue(...), startWorkflowJson(...), and startWorkflowVersionedJson(...) also support:

  • parentRunId when one workflow needs to track provenance from another run
  • ttl when you want run metadata to expire after a bounded retention period

Those are advanced controls. Most applications only need params: and an optional cancellation policy.