Context and Serialization
Stem injects context objects at specific points in the workflow/task lifecycle. Everything else that crosses a durable boundary must be serializable.
Supported context injection points
- flow steps:
FlowContextorWorkflowExecutionContext - script runs:
WorkflowScriptContext - script checkpoints:
WorkflowScriptStepContextorWorkflowExecutionContext - tasks:
TaskExecutionContext
Those context objects are not part of the persisted payload shape. They are injected by the runtime when the handler executes.
For annotated workflows/tasks, the preferred shape is an optional named context parameter:
Future<T> run(String email, {WorkflowScriptContext? context})Future<T> checkpoint(String email, {WorkflowExecutionContext? context})Future<T> step({WorkflowExecutionContext? context})Future<void> task(String id, {TaskExecutionContext? context})
What context gives you
Depending on the context type, you can access:
workflowrunIdstepNamestepIndexiteration- workflow params and previous results
param<T>()/requiredParam<T>()for typed access to workflow start paramsparamsAs(codec: ...),paramsJson<T>(), orparamsVersionedJson<T>()for decoding the full workflow start payload as one DTOparamJson<T>(),paramVersionedJson<T>(), orrequiredParamJson<T>()for nested DTO params without a separate codec constantparamListJson<T>(),paramListVersionedJson<T>(), orrequiredParamListJson<T>()for lists of nested DTO params without a separate codec constantpreviousValue<T>()/requiredPreviousValue<T>()for typed access to the prior step or checkpoint resultpreviousJson<T>(),previousVersionedJson<T>(),requiredPreviousJson<T>(), orrequiredPreviousVersionedJson<T>()for prior DTO results without a separate codec constantsleepUntilResumed(...)for common sleep/retry loopswaitForEventValue<T>(...)for common event waitswaitForEventValueJson<T>(...)orwaitForEventValueVersionedJson<T>(...)for DTO event waits without a separate codec constantevent.awaitOn(step)when a flow deliberately wants the lower-levelFlowStepControlsuspend-first path on a typed event refsleepJson(...),sleepVersionedJson(...),awaitEventJson(...),awaitEventVersionedJson(...), andFlowStepControl.awaitTopicJson(...)when lower-level suspension directives still need DTO metadata without a separate codec constantcontrol.dataJson(...),control.dataVersionedJson(...), orcontrol.dataAs(codec: ...)when you inspect a lower-levelFlowStepControldirectlytakeResumeData()for event-driven resumestakeResumeValue<T>(codec: ...)for typed event-driven resumestakeResumeJson<T>(...)ortakeResumeVersionedJson<T>(...)for DTO event-driven resumes without a separate codec constant- for read-side
...VersionedJson(...)helpers,defaultVersion:is only the fallback used when an older stored payload does not already carry__stemPayloadVersion idempotencyKey(...)- direct child-workflow start helpers such as
ref.start(context, params: value)andref.startAndWait(context, params: value) - direct task enqueue APIs because
WorkflowExecutionContextandTaskExecutionContextboth implementTaskEnqueuer argsAs(codec: ...),argsJson<T>(), orargsVersionedJson<T>()for decoding the full task-arg payload as one DTO inside manual task handlersargJson<T>(),argVersionedJson<T>(),argListJson<T>(), orargListVersionedJson<T>()when only one nested arg entry needs DTO decode- task metadata like
id,attempt,meta
Child workflow starts belong in durable boundaries:
ref.start(context, params: value)inside flow stepsref.startAndWait(context, params: value)inside script checkpoints- pass
ttl:,parentRunId:, orcancellationPolicy:directly to those helpers for the normal override cases - keep
ref.buildStart(...)for the rarer cases where you explicitly want a reusableWorkflowStartCallbuilt with its final overrides
Do not treat the raw WorkflowScriptContext body as a safe place for child
starts or other replay-sensitive side effects.
Serializable parameter rules
Supported shapes:
Stringboolintdoublenum- JSON-like scalar values (
Object?only when the runtime value is itself serializable) List<T>whereTis serializableMap<String, T>whereTis serializable
Unsupported directly:
- arbitrary Dart class instances
- non-string map keys
- annotated workflow/task method signatures with optional or named business parameters
If you have a domain object, prefer a codec-backed DTO:
class OrderRequest {
const OrderRequest({required this.id, required this.customerId});
final String id;
final String customerId;
Map<String, dynamic> toJson() => {'id': id, 'customerId': customerId};
factory OrderRequest.fromJson(Map<String, dynamic> json) {
return OrderRequest(
id: json['id'] as String,
customerId: json['customerId'] as String,
);
}
}
Generated workflow refs and task definitions will persist the JSON form while
your workflow/task code keeps working with the typed object. The restriction
still applies to the annotated business method signatures that stem_builder
lowers into workflow/task definitions.
The same rule applies to workflow resume events: emitValue(...) can take a
typed DTO plus a PayloadCodec<T>, but the codec must still encode to a
string-keyed map because watcher persistence and event delivery are map-based
today.
For normal DTOs that expose toJson() and Type.fromJson(...), prefer
PayloadCodec<T>.json(...). Drop down to PayloadCodec<T>.map(...) when you
need a custom map encoder or a nonstandard decode function.
If the DTO payload shape is expected to evolve, use
PayloadCodec<T>.versionedJson(...). That persists a reserved
__stemPayloadVersion field beside the JSON payload and gives the decoder the
stored version so it can read older shapes explicitly.
When a DTO evolves through multiple persisted shapes, prefer
PayloadVersionRegistry<T> with PayloadCodec<T>.versionedJsonRegistry(...)
so version-specific decoders live in one reusable registry instead of being
repeated inline at every call site.
Use PayloadCodec<T>.versionedMap(...) instead when the payload still needs a
custom map encoder or a nonstandard version-aware decode function.
PayloadCodec<T>.versionedMapRegistry(...) gives the same reusable-registry
shape for that case.
The same registry-backed model is available on the higher-level authoring factories too:
TaskDefinition.versionedJsonRegistry(...)TaskDefinition.versionedMapRegistry(...)WorkflowRef.versionedJsonRegistry(...)WorkflowRef.versionedMapRegistry(...)WorkflowEventRef.versionedJsonRegistry(...)WorkflowEventRef.versionedMapRegistry(...)Flow.versionedJsonRegistry(...)/Flow.versionedMapRegistry(...)WorkflowScript.versionedJsonRegistry(...)/WorkflowScript.versionedMapRegistry(...)
For manual flows and scripts, prefer the typed workflow param helpers before dropping to raw map casts:
final request = ctx.paramsJson<OrderRequest>(
decode: OrderRequest.fromJson,
);
final userId = ctx.requiredParam<String>('userId');
final draft = ctx.requiredParam<ApprovalDraft>(
'draft',
codec: approvalDraftCodec,
);
For manual tasks, the same pattern applies to the full arg payload:
final request = context.argsJson<OrderRequest>(
decode: OrderRequest.fromJson,
);
Practical rule
When you need context metadata, add the appropriate optional named context parameter. When you need business input, make it a required positional serializable value.
Prefer the higher-level helpers first:
sleepUntilResumed(...)when the step/checkpoint should pause once and continue on resumewaitForEventValue<T>(...)when the step/checkpoint is waiting on one event
Drop down to takeResumeData(), takeResumeValue<T>(...),
takeResumeJson<T>(...), or takeResumeVersionedJson<T>(...) only when you
need custom branching around resume payloads.
The runnable annotated_workflows example demonstrates both the context-aware
and plain serializable forms.