Skip to main content

Flows and Scripts

Stem supports two workflow models. Both are durable. They differ in where the execution plan lives.

The distinction

ModelSource of truthBest for
FlowDeclared stepsExplicit orchestration, clearer admin views, fixed step order
WorkflowScriptThe Dart code in run(...)Branching, loops, and function-style workflow authoring

The confusing part is that both models expose step-like metadata. The difference is that for script workflows those are checkpoints, not the plan itself.

  • Flow: the runtime advances through the declared step list.
  • WorkflowScript: the runtime re-enters run(...) and durable boundaries are created when script.step(...) executes.
  • Script checkpoints exist for replay boundaries, manifests, dashboards, and tooling.

Flow example

lib/workflows/approvals_flow.dart
class ApprovalsFlow {
static final flow = Flow<String>(
name: 'approvals.flow',
build: (flow) {
flow.step('draft', (ctx) async {
final draft = ctx.requiredParamJson<ApprovalDraft>(
'draft',
decode: ApprovalDraft.fromJson,
);
return draft.documentId;
});

flow.step('manager-review', (ctx) async {
final resume = ctx.waitForEventValueJson<ApprovalDecision>(
'approvals.manager',
decode: ApprovalDecision.fromJson,
);
if (resume == null) {
return null;
}
return resume.approvedBy;
});

flow.step('finalize', (ctx) async {
final approvedBy = ctx.previousValue<String>();
return 'approved-by:$approvedBy';
});
},
);

static final ref = flow.refJson<ApprovalDraft>();
}

Future<void> registerFlow(StemWorkflowApp workflowApp) async {
workflowApp.registerFlows([ApprovalsFlow.flow]);
}

Future<void> registerWorkflowDefinition(StemWorkflowApp workflowApp) async {
workflowApp.registerWorkflows([ApprovalsFlow.flow.definition]);
}

Manual flows can also derive a typed workflow ref from the definition:

final approvalsRef = approvalsFlow.ref<Map<String, Object?>>(
encodeParams: (draft) => <String, Object?>{'draft': draft},
);

When a flow has no start params, start directly from the flow itself with flow.start(...) or flow.startAndWait(...). Keep flow.ref0().asRef.buildStart(params: ()) for the rarer cases where you want to assemble or adjust overrides before dispatch. Use ref0() only when another API specifically needs a NoArgsWorkflowRef.

Use Flow when:

  • the sequence of durable actions should be obvious from the definition
  • each step maps cleanly to one business stage
  • your operators care about a stable, declared step list

Script example

lib/workflows/retry_script.dart
final retryScript = WorkflowScript(
name: 'billing.retry-script',
run: (script) async {
final chargeId = await script.step<String>('charge', (ctx) async {
final resume = ctx.waitForEventValueJson<ChargePrepared>(
'billing.charge.prepared',
decode: ChargePrepared.fromJson,
);
if (resume == null) {
return 'pending';
}
return resume.chargeId;
});

final receipt = await script.step<String>('confirm', (ctx) async {
ctx.idempotencyKey('confirm-$chargeId');
return 'receipt-$chargeId';
});

return receipt;
},
);

final retryDefinition = retryScript.definition;

Future<void> registerScript(StemWorkflowApp workflowApp) async {
workflowApp.registerScripts([retryScript]);
}

Manual scripts support the same pattern:

final retryRef = retryScript.ref<Map<String, Object?>>(
encodeParams: (params) => params,
);

When a script has no start params, start directly from the script itself with retryScript.start(...) or retryScript.startAndWait(...). Keep retryScript.ref0().asRef.buildStart(params: ()) for the rarer cases where you want to assemble or adjust overrides before dispatch. Use ref0() only when another API specifically needs a NoArgsWorkflowRef.

Use WorkflowScript when:

  • you want normal Dart control flow to define the run
  • the workflow has branching or repeated patterns
  • you want a more function-like authoring model

Contexts in each model

  • flow steps receive FlowContext
  • script runs may receive WorkflowScriptContext
  • script checkpoints may receive WorkflowScriptStepContext

The full injection and parameter rules are documented in Context and Serialization.