1 - How to: Author and run workflows with the JavaScript SDK
Register workflows and activities, coordinate durable tasks, and handle retries and events
Workflows are generator functions that yield SDK tasks. The runtime records task results in history and
replays the generator to reconstruct its state. Activities are ordinary functions, optionally asynchronous,
that perform I/O or other work outside the deterministic workflow.
Prerequisites
- Node.js 18 or later and
@dapr/dapr 3.19.0 or later. - An initialized self-hosted Dapr environment with runtime 1.19 or later.
- Workflow infrastructure: an actor state store, placement, and scheduler. The default
dapr init environment
provides these for local development.
Register and run a workflow
Install the package in your application directory:
npm init -y
npm install @dapr/dapr@3.19.0
Save this complete example as app.mjs. It uses JavaScript ES modules; the same APIs are available in TypeScript.
import {
DaprWorkflowClient,
WorkflowRuntime,
WorkflowRuntimeStatus,
} from "@dapr/dapr";
async function hello(_ctx, name) {
return `Hello ${name}!`;
}
function* sequence(ctx) {
const cities = [];
for (const city of ["Tokyo", "Seattle", "London"]) {
cities.push(yield ctx.callActivity(hello, city));
}
ctx.logger.info("Greeting sequence completed");
return cities;
}
const options = { daprHost: "127.0.0.1", daprPort: "50001" };
const runtime = new WorkflowRuntime(options);
const client = new DaprWorkflowClient(options);
runtime.registerWorkflow(sequence).registerActivity(hello);
try {
await runtime.start();
const id = await client.scheduleNewWorkflow(sequence);
const state = await client.waitForWorkflowCompletion(id, true, 30);
if (!state || state.runtimeStatus !== WorkflowRuntimeStatus.COMPLETED) {
throw new Error(
state?.workflowFailureDetails?.getErrorMessage() ?? "Workflow did not complete successfully",
);
}
console.log(state.getOutput());
} finally {
await runtime.stop();
await client.stop();
}
Run it with a sidecar:
dapr run --app-id sequence-workflow --dapr-grpc-port 50001 -- node app.mjs
The output includes ["Hello Tokyo!", "Hello Seattle!", "Hello London!"].
Registration names
registerWorkflow(fn) and registerActivity(fn) use the function name as the durable registration name.
For explicit, stable names, use:
runtime.registerWorkflowByName("greeting-v1", sequence);
runtime.registerActivityWithName("hello-v1", hello);
Call these registrations by string, for example ctx.callActivity("hello-v1", city) or
client.scheduleNewWorkflow("greeting-v1"). Keep names stable across deployments and minification.
registerWorkflowWithName is a deprecated alias of registerWorkflowByName.
Keep workflow execution deterministic
Put network calls, database operations, filesystem access, and nondeterministic computation in activities.
Use yield with ctx.callActivity, timers, event waits, and task-composition helpers; do not substitute
await or Promise.all for durable tasks. Both synchronous and async generators are supported, but an
async generator does not make arbitrary I/O replay-safe.
| Need | Workflow API |
|---|
| Current time | ctx.getCurrentUtcDateTime(), not new Date() or Date.now() |
| Generated identifier | ctx.newGuid(), not a random UUID |
| Instance or registration name | ctx.getWorkflowInstanceId(), ctx.getWorkflowName() |
| Logging without duplicate replay messages | ctx.logger.info(...), .warn(...), .error(...), or .debug(...) |
| Conditional replay handling | ctx.isReplaying() |
| Externally visible progress | ctx.setCustomStatus({ step: "processing" }) |
Activities can be delivered more than once. Make external side effects idempotent. An activity receives
WorkflowActivityContext with getWorkflowInstanceId(), getWorkflowActivityId(), and
getTaskExecutionId(). The execution ID is a persisted activity key, also exposed as task.executionId
on the workflow’s activity task, and is reused across SDK retry attempts. It can be undefined for legacy
history or runtimes that do not supply it; do not assume it is always present or that it guarantees exactly-once delivery.
Run activities in parallel
Create all tasks before yielding whenAll:
function* parallelGreetings(ctx, cities) {
const tasks = cities.map((city) => ctx.callActivity("hello", city));
return yield ctx.whenAll(tasks);
}
whenAll returns results in input order and fails if a task fails. whenAny returns the first completed
task, not its result, and does not cancel the other tasks. Yield the winning task or call its getResult()
to observe its result or error.
Retry activities and child workflows
Pass retryPolicy in activity or child-workflow options:
function* resilientGreeting(ctx, name) {
return yield ctx.callActivity("hello", name, {
retryPolicy: {
firstRetryInterval: 1,
maxNumberOfAttempts: 4,
backoffCoefficient: 2,
maxRetryInterval: 30,
retryTimeout: 120,
},
});
}
All intervals are in seconds. maxNumberOfAttempts includes the initial call; backoffCoefficient
defaults to 1. The first interval and optional limits must be positive and finite; attempts must be a
positive integer and the coefficient must be at least 1. A non-retriable failure stops retries.
retryTimeout limits scheduling further attempts; it is not a timeout that interrupts a running activity.
Failures are thrown into the generator at yield, where you can catch them and schedule compensating activities.
Wait for external events
An external event lets another process signal an instance. The SDK matches event names case-insensitively
and buffers events that arrive before their wait is created.
import { TaskTimeoutError } from "@dapr/dapr";
function* approval(ctx) {
ctx.setCustomStatus({ step: "awaiting-approval" });
try {
const decision = yield ctx.waitForExternalEvent("approval", {
timeoutInSeconds: 3600,
});
return decision;
} catch (error) {
if (!(error instanceof TaskTimeoutError)) throw error;
return { approved: false, reason: "Approval deadline expired" };
}
}
From a client, call await client.raiseEvent(instanceId, "approval", { approved: true }).
From another workflow in the same app, use ctx.sendEvent(instanceId, "approval", { approved: true });
sendEvent does not return a task and is not yielded. Do not expose the client or make client RPCs inside
workflow code.
Use durable timers and cancellation
ctx.createTimer(60) waits 60 seconds of workflow time. You can also supply an absolute Date computed
from ctx.getCurrentUtcDateTime(). Do not use setTimeout for a durable delay.
For a race between an event and a timer:
function* approvalRace(ctx) {
const event = ctx.waitForExternalEvent("approval");
const timer = ctx.createTimer(3600);
const winner = yield ctx.whenAny([event, timer]);
if (winner === event) {
timer.cancel();
return event.getResult();
}
event.cancel();
timer.getResult();
return { approved: false, reason: "Approval deadline expired" };
}
Timers and event waits implement CancellableTask, exposing cancel() and isCanceled. Canceling stops
the SDK’s local wait and rejects it with TaskCanceledError; it does not cancel runtime-side work,
an activity, or an entire workflow. Cancel unused waits explicitly after a race.
Continue as new
For long-running loops, start a fresh history generation under the same instance ID:
function* batchLoop(ctx, input) {
const next = yield ctx.callActivity("processBatch", input);
ctx.continueAsNew(next, true);
return;
}
Return immediately after continueAsNew. The second argument, saveEvents, defaults to true and retains
unconsumed external events; set it to false to discard them. Do not schedule more work after requesting
continuation. Register the activity used by this example before starting the worker.
Next steps
2 - Manage workflow instances with the JavaScript SDK
Schedule, inspect, control, list, and rerun workflow instances with DaprWorkflowClient
Use DaprWorkflowClient from application or administrative code, not inside a workflow generator.
This guide targets @dapr/dapr 3.19.0 or later and runtime 1.19 or later.
import { DaprWorkflowClient, WorkflowRuntimeStatus } from "@dapr/dapr";
const client = new DaprWorkflowClient({
daprHost: "127.0.0.1",
daprPort: "50001",
});
The following snippets reuse client. The workflow name must be registered on the destination worker.
Call await client.stop() when the application shuts down.
Choose the management client
DaprWorkflowClient is separate from DaprClient.workflow, the
basic workflow API.
The basic API supports HTTP or gRPC with scheduleNewWorkflow, getWorkflowState, raiseEvent, pause,
resume, terminate, and purge. Its old start, get, and raise names are deprecated aliases.
It returns a plain WorkflowGetResponseType with properties, rather than the WorkflowState described here.
Use DaprWorkflowClient for durable start/completion waits, listing, history retrieval, reruns, and integrated
payload-reference handling. Its methods and options are not interchangeable with the basic API:
for example, it uses suspendWorkflow rather than pause, and getWorkflowState requires the
getInputsAndOutputs argument.
Schedule and wait
const instanceId = await client.scheduleNewWorkflow(
"orderWorkflow",
{ orderId: "order-123" },
"order-123",
new Date(Date.now() + 60_000),
);
await client.waitForWorkflowStart(instanceId, false, 120);
const state = await client.waitForWorkflowCompletion(instanceId, true, 300);
if (!state) throw new Error(`Workflow ${instanceId} was not found`);
if (state.runtimeStatus === WorkflowRuntimeStatus.COMPLETED) {
console.log(state.getOutput());
} else {
console.error(state.runtimeStatus, state.workflowFailureDetails);
}
The positional scheduling arguments are (workflow, input?, instanceId?, startAt?, signal?).
workflow can be a registered name or a function; omitted instance IDs are generated. startAt is an optional
absolute Date. Using wall-clock time here is safe because this is client code, not replayed workflow code.
Both wait methods accept (instanceId, fetchPayloads = true, timeoutInSeconds = 60, signal?).
A wait deadline or RPC failure rejects the promise. A workflow that reaches a failed or terminated state
can return a state rather than throw: check runtimeStatus, not just whether the wait resolved.
Inspect state and failure details
await client.getWorkflowState(instanceId, true) returns a WorkflowState snapshot, or undefined for a
missing instance. Pass false to omit inputs and outputs when only metadata is needed.
| Member | Meaning |
|---|
name, instanceId, exists | Registration name, instance ID, and existence (true for a returned state). |
runtimeStatus | Compare with WorkflowRuntimeStatus.RUNNING, COMPLETED, CONTINUED_AS_NEW, FAILED, CANCELED, TERMINATED, PENDING, SUSPENDED, or STALLED. |
createdAt, lastUpdatedAt | JavaScript Date values. |
getInput(), getOutput(), getCustomStatus() | Deserialize with the client’s configured serializer; TypeScript callers can provide a generic result type. |
serializedInput, serializedOutput, customStatus | Serialized strings, not already-parsed objects. With payload support, resolved strings are prepared where possible. |
workflowFailureDetails | getErrorType(), getErrorMessage(), getStackTrace(), isNonRetriable, and getInnerFailure() for nested details. innerFailure exposes the raw nested protobuf value. |
getInputReference(), getOutputReference(), getCustomStatusReference() | Original top-level binary-store reference, if present. See data references. |
Typed getters do not validate application schemas or reconstruct class instances automatically. Missing payload
fields return undefined. Missing binary-store objects or integrity failures are errors, not “workflow not found.”
Send events and control execution
| Method | Behavior |
|---|
raiseEvent(id, eventName, payload?, signal?) | Deliver an event to a workflow’s durable event wait. |
suspendWorkflow(id, reason?, signal?) | Suspend instance execution. |
resumeWorkflow(id, reason?, signal?) | Resume a suspended instance. |
terminateWorkflow(id, output?, signal?) | Request termination with optional output; does not execute application compensation logic. |
purgeWorkflow(id, signal?) | Purge retained instance state/history; returns whether an instance was deleted. Subject to runtime eligibility checks. |
For example:
await client.suspendWorkflow(instanceId, "Investigating an order");
await client.resumeWorkflow(instanceId, "Investigation complete");
await client.raiseEvent(instanceId, "approval", { approved: true });
Do not use suspension or termination as a way to undo activity side effects. A canceled client request also
does not terminate the instance. Purging history does not delete binary-store payloads.
Cancel client requests
Scheduling, state reads, waits, event delivery, suspend/resume, termination, and purge accept an optional
AbortSignal in their final argument:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
await client.waitForWorkflowCompletion(instanceId, true, 60, controller.signal);
} finally {
clearTimeout(timeout);
}
Aborting stops the request and associated payload I/O where supported, not the durable workflow. A request
may already have been accepted by the runtime before cancellation. Listing, history retrieval, and rerun
do not expose an AbortSignal parameter in this release.
List instances
listWorkflowInstanceIds(continuationToken?, pageSize?) returns { instanceIds, continuationToken }.
Continue until no token remains:
let token;
do {
const page = await client.listWorkflowInstanceIds(token, 100);
for (const id of page.instanceIds) {
console.log(id);
}
token = page.continuationToken;
} while (token);
The SDK validates pageSize as an integer from 1 to 4294967295; the runtime may impose additional limits.
The response contains IDs, not state snapshots or filtered query results.
Retrieve execution history
const history = await client.getWorkflowHistory(instanceId);
for (const event of history) {
console.log(event.eventId, event.eventType.case);
}
This returns the complete HistoryEvent[] using the SDK’s protobuf types. Payload fields remain serialized
wire data, including any reference envelopes; retrieving history is not automatic payload hydration.
Missing-instance and other runtime errors are propagated.
Rerun from an event
Inspect the source history to choose the event ID accepted by the runtime, then create a new instance:
const rerunId = await client.rerunWorkflowFromEvent(instanceId, eventId, {
newInstanceId: "order-123-rerun",
input: { correctedAmount: 125 },
});
eventId is a history event ID, not the array index, and must be an unsigned 32-bit integer. This operation
creates a new instance; it does not modify the source instance. Replacement input applies to the
activity targeted by the event, not the original workflow input.
newInstanceId is optional but must not be empty. overwriteInput defaults to true when input is supplied;
set it to false to preserve the original activity input. The runtime decides which histories and events are
eligible and propagates errors for missing instances or invalid events. Reruns can repeat side effects:
review idempotency and retain any binary objects still referenced by the copied history.
Next steps
3 - Multi-app workflows and history propagation in the JavaScript SDK
Coordinate remote activities, child workflows, and detached workflows and inspect ancestor history
Use targetAppId to route activity and workflow calls to another Dapr application. This guide uses
@dapr/dapr 3.19.0 or later and Dapr runtime 1.19 or later.
See Multi Application Workflows for runtime architecture and constraints.
Requirements
Participating apps must run in the same namespace and use the same workflow actor state store. Each destination
must register the name being called and run a workflow worker. Omit targetAppId for a local call.
For cross-language calls, agree on names, input/output schemas, and serialization.
Call an activity or child workflow
import { HistoryPropagationScope } from "@dapr/dapr";
function* orderWorkflow(ctx, order) {
const payment = yield ctx.callActivity("chargePayment", order, {
targetAppId: "payments",
retryPolicy: { firstRetryInterval: 2, maxNumberOfAttempts: 3 },
propagationScope: HistoryPropagationScope.OWN_HISTORY,
});
return yield ctx.callChildWorkflow(
"fulfillmentWorkflow",
{ order, payment },
undefined,
{
targetAppId: "fulfillment",
propagationScope: HistoryPropagationScope.LINEAGE,
},
);
}
An activity call returns the activity result; a child call waits for the child and returns its output.
Failures are thrown at the corresponding yield. callSubWorkflow is an alias of callChildWorkflow.
The positional child arguments are (workflow, input?, instanceId?, options?); omit the child ID to let the
SDK derive it. A retry policy can retry a child workflow as well as an activity. A custom child ID applies
to the first attempt; later retry attempts receive generated child IDs.
Schedule an independent, detached workflow
Detached workflows require Dapr runtime 1.19 or later:
function* dispatchWorkflow(ctx, input) {
const startAt = new Date(ctx.getCurrentUtcDateTime().getTime() + 60_000);
const detachedId = yield ctx.scheduleNewDetachedWorkflow(
"notificationWorkflow",
input,
undefined,
startAt,
{ targetAppId: "notifications" },
);
return { detachedId };
}
The task returns the detached workflow’s instance ID, not its output. The workflow runs independently:
its completion or failure does not flow back to the caller. Use a client to inspect the returned ID through
the destination app’s sidecar. The arguments are (workflow, input?, instanceId?, startAt?, options?).
Detached options support targetAppId and allowPayloadReferences, but not retries or history propagation.
Always yield the scheduling task so the workflow can record the scheduling action.
Inspect propagated history
Activity and child-workflow options accept:
HistoryPropagationScope | History shared with the callee |
|---|
NONE | No history propagation; the default. |
OWN_HISTORY | The calling workflow’s own history. |
LINEAGE | The caller’s history together with its inherited ancestor history. |
From a workflow or activity, call ctx.getPropagatedHistory(). It returns undefined when no history was
supplied, otherwise a read-only PropagatedHistoryView:
async function auditActivity(ctx, input) {
const history = ctx.getPropagatedHistory();
if (history) {
console.log(history.appIDs);
for (const ancestor of history.workflows) {
console.log(ancestor.workflowName, ancestor.instanceId, ancestor.eventCount);
}
}
return input;
}
| Member | Meaning |
|---|
scope, chunks | Requested scope and ancestor chunks, oldest ancestor first and immediate parent last. |
events, appIDs | Flattened events and distinct app IDs across chunks. |
workflows | Chunks with startEventIndex and eventCount in the flattened view. |
getWorkflowByName(name) | Oldest matching ancestor, or null. |
getEventsByAppID(id), getEventsByInstanceID(id) | Events matching an app or instance. |
getEventsByWorkflowName(name) | Events from every matching workflow instance, not just the first match. |
Each chunk contains appId, instanceId, workflowName, decoded events, and original rawEvents,
rawSignatures, and signingCertChains. The SDK preserves these bytes but does not verify signatures.
Do not treat this view as SDK-verified evidence. Payloads in history remain serialized; reference envelopes
are not automatically resolved in this view.
History propagation increases data transferred to callees and can expose ancestor inputs, outputs, and
events. Select the narrowest scope needed. This is distinct from the unsupported stateful workflow client.
Cross-app data references
With binary-store payload support, cross-app activity, child,
and detached workflow inputs are inline by default. Set allowPayloadReferences: true only when the
destination understands the reference contract and can access the same stored objects.
The JavaScript SDK shares the version-1 reference envelope with the .NET SDK. Interoperation requires
compatible serializers, contracts, and storage access; JavaScript does not implement .NET payload-stage
reversal. Inlining only applies to the top-level call input: results, events, nested references, and
propagated history are not made compatible by that default. See
cross-app reference handling
before enabling offloading in a multi-app topology.
4 - Workflow versioning in the JavaScript SDK
Evolve workflow code with replay-safe named patches
Workflow code must reproduce the task sequence recorded in history when replaying an existing instance.
Changing activity order, adding durable tasks, or changing branches without a migration strategy can cause
nondeterministic replay failures.
With @dapr/dapr 3.19.0 or later, use ctx.isPatched(patchName) for named code changes.
Patch evaluation is validated by the Dapr runtime; the examples in this documentation use runtime 1.19 or later.
See Workflow versioning for the runtime-level concepts.
Introduce a patch
Suppose an existing workflow calls chargePayment directly, and a new deployment must validate the order first:
function* orderWorkflow(ctx, order) {
if (ctx.isPatched("validate-order-before-charge")) {
yield ctx.callActivity("validateOrder", order);
}
return yield ctx.callActivity("chargePayment", order);
}
When an unrecorded patch is first encountered during replay, isPatched returns false so the old execution
path remains intact. Outside replay, or when the patch is already recorded, it returns true. The SDK
records evaluated patches for runtime version validation.
Names are case-sensitive and must not be empty or whitespace. Use a distinct name for each logical change;
never reuse a name for an unrelated change or compute it from nondeterministic values.
Keep old execution paths available
Keep the original branch and patch check until every instance that needs that history has finished and is
no longer eligible for replay or rerun. Do not reorder patch checks or remove them merely because a new
deployment has started. Account for retained histories, suspended instances, child workflows, and
administrative reruns when planning retirement.
For a larger contract change, register a new workflow name and send new instances to it while keeping the
old implementation available for existing instances. Patching does not convert persisted serialized data
or turn an incompatible payload schema into a compatible one.
Next steps
5 - Workflow serialization in the JavaScript SDK
Configure payload serialization consistently across JavaScript workflow clients and workers
The JavaScript SDK uses JSON by default. With @dapr/dapr 3.19.0 or later, clients and workers can
share a custom IDaprSerializer through their serializer option. Serializer selection is client-side;
it does not by itself require binary-store support.
Default JSON behavior
Workflow payloads use JSON.stringify and JSON.parse. Use JSON-compatible application contracts:
dates deserialize as strings, prototypes are not reconstructed, BigInt requires explicit handling,
and cyclic values cannot be serialized. TypeScript generics describe the expected result but do not
validate or instantiate that type at runtime.
WorkflowState.getInput<T>(), getOutput<T>(), and getCustomStatus<T>() use the client’s serializer.
Their serialized counterparts are strings, not parsed application values.
The serializer contract converts values to and from strings:
import {
DaprWorkflowClient,
JsonSerializer,
WorkflowRuntime,
type IDaprSerializer,
} from "@dapr/dapr";
const serializer: IDaprSerializer = new JsonSerializer();
const options = { daprHost: "127.0.0.1", daprPort: "50001", serializer };
const client = new DaprWorkflowClient(options);
const runtime = new WorkflowRuntime(options);
For a custom format, implement serialize<T>(value: T): string and
deserialize<T = unknown>(data: string): T. An optional contentType identifies the media type for
SDK APIs that use it; it is not a workflow wire-format negotiation mechanism. Binary formats can use
Base64 to satisfy the string contract, at the cost of added size. Serialize deterministically and throw
on invalid input rather than silently returning defaults.
Persisted-history compatibility
Deploy compatible serializers to every client, activity worker, and workflow worker that participates in
the workflow. Changing the encoding or schema can break existing histories, results, events, and reruns.
The SDK does not automatically migrate persisted data or negotiate formats across apps. Keep the old
decoder available or introduce a separate workflow contract and registration name.For cross-language workflows, agree on the wire format and property names. The default JavaScript JSON
behavior is not a replacement for the .NET SDK’s case-insensitive deserialization or other language-specific
conversion rules.
Combine serialization with data references
Workflow data references offload the serialized UTF-8 string to
a binary store. Offloading is separate from choosing a serializer and is evaluated after serialization.
The reference envelope itself is versioned JSON, independent of the application serializer.
Supply the same serializer to the BinaryStoreWorkflowPayloadResolver and all clients and workers.
When a workflow client or runtime has payloads but no explicit serializer, it uses payloads.serializer.
An explicit serializer overrides that choice; do not configure incompatible serializers in those two places.
Only top-level PayloadReference objects receive special envelope handling, not references nested within
an application object.
6 - Workflow data references in the JavaScript SDK
Offload large workflow payloads to a binary store and defer materialization with explicit references
Workflow data references keep large payloads out of inline workflow history. The JavaScript SDK stores the
serialized value in a Dapr binary store and records a small, versioned reference envelope instead. You can
keep ordinary workflow inputs and let the SDK retrieve data automatically, or forward explicit
PayloadReference values without downloading the document in the workflow.
Alpha feature
The binary-store building block and this SDK integration are alpha and subject to change.
These APIs require @dapr/dapr 3.19.0 or later, Dapr runtime 1.19 or later, and a binary-store
component in addition to the normal workflow infrastructure.Choose how to handle payloads
| Approach | Behavior | Best for |
|---|
| Inline (default) | Serialized values travel in workflow history. No binary-store access is required. | Small payloads. |
| Automatic offloading | PreferReference stores eligible large values. Ordinary input/result contracts trigger automatic download and deserialization. | Reducing history size while preserving existing application contracts. |
| Explicit references | Register reference inputs and request reference results; forward envelopes without downloading the data. Resolve or stream only where needed. | Workflows that coordinate large documents but do not inspect their contents. |
Offloading replaces repeated large history values with small references, reducing history storage and
workflow protocol traffic. It does not make storage I/O free:
- Automatic offloading still materializes values. An ordinary workflow input, activity result, or event
must be downloaded and deserialized when consumed. Replays repeat those reads; the cache is scoped to
a single orchestrator execution, not shared across replays.
- Explicit references avoid unnecessary reads. A workflow can pass the same reference through several
activities without loading the document itself. Only consumers that need the content resolve or stream it.
- Streaming avoids full buffering. It trades the convenience of a deserialized object for incremental
access to the raw serialized bytes.
For a document of size S forwarded through multiple steps, inline history repeatedly carries data on the
order of S; reference-based history repeatedly carries the much smaller envelope. This is a traffic model,
not a JavaScript latency or allocation benchmark. Actual performance depends on document size, replay
frequency, store latency, and how many consumers materialize the data. Small values may be faster inline.
Do not apply the .NET benchmark results
to JavaScript; measure your own workload.
Set up payload support
For a disposable local experiment, save this as components\workflow-payloads.yaml:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: workflow-payloads
spec:
type: binarystore.in-memory
version: v1
metadata:
- name: maxSize
value: "4Mi"
- name: maxTotalSize
value: "512Mi"
In-memory storage is for experiments only
In-memory payloads disappear when the sidecar stops and are not shared between sidecars. Durable workflow
history can outlive those objects and become unreadable. Use durable binary storage for workflows that must
survive restarts, and retain objects for as long as histories or downstream consumers reference them.See the binary-store guide for component and transport details.
Adding a component alone does not enable workflow offloading.
Register a resolver on clients and workers
import {
BinaryStoreWorkflowPayloadResolver,
CommunicationProtocolEnum,
DaprClient,
DaprWorkflowClient,
PayloadWritePreference,
WorkflowRuntime,
} from "@dapr/dapr";
const connection = { daprHost: "127.0.0.1", daprPort: "50001" };
const dapr = new DaprClient({
...connection,
communicationProtocol: CommunicationProtocolEnum.GRPC,
});
const payloads = new BinaryStoreWorkflowPayloadResolver(dapr.binaryStore, {
storeName: "workflow-payloads",
writePreference: PayloadWritePreference.PreferReference,
offloadThreshold: 64 * 1024,
});
const client = new DaprWorkflowClient({ ...connection, payloads });
const runtime = new WorkflowRuntime({ ...connection, payloads });
The resolver uses DaprClient.binaryStore, which supports HTTP or gRPC; the workflow transport itself
still uses gRPC. Configure endpoints and API tokens consistently if the clients connect to different endpoints.
storeName must match the component name. All readers need access to the referenced store/key and compatible
serialization. Merely constructing a resolver uses Preserve, not automatic offloading; the example
explicitly selects PreferReference.
Use automatic offloading
With the registration above, keep ordinary inputs:
function* documentWorkflow(ctx, document) {
return yield ctx.callActivity("measureDocument", document);
}
async function measureDocument(_ctx, document) {
return document.content.length;
}
runtime.registerWorkflow(documentWorkflow).registerActivity(measureDocument);
try {
await runtime.start();
const id = await client.scheduleNewWorkflow(documentWorkflow, {
content: "a".repeat(128 * 1024),
});
const state = await client.waitForWorkflowCompletion(id, true, 60);
console.log(state?.getOutput());
} finally {
await runtime.stop();
await client.stop();
await dapr.stop();
}
Combine this snippet with the imports and resolver setup above in app.mjs, then run:
dapr run --app-id document-workflow --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs
The document exceeds the threshold and is offloaded at eligible boundaries. The workflow still receives an
ordinary object and downloads it on replay; the small numeric result stays inline.
Pass explicit references
JavaScript types are erased at runtime. A TypeScript annotation such as PayloadReference<DocumentPayload>
alone does not change how inputs are read. Register each workflow and activity that accepts an envelope
with { inputAsReference: true }.
Use this alternative to the automatic example, with the same resolver setup:
function* referenceWorkflow(ctx, documentReference) {
return yield ctx.callActivity("measureReference", documentReference);
}
async function measureReference(ctx, documentReference) {
const document = await ctx.payloads.resolve(documentReference);
return document.content.length;
}
runtime
.registerWorkflow(referenceWorkflow, { inputAsReference: true })
.registerActivity(measureReference, { inputAsReference: true });
try {
await runtime.start();
const instanceId = "document-reference-1";
const reference = await payloads.store(
{ content: "a".repeat(128 * 1024) },
instanceId,
);
await client.scheduleNewWorkflow(referenceWorkflow, reference, instanceId);
const state = await client.waitForWorkflowCompletion(instanceId, true, 60);
console.log(state?.getOutput());
} finally {
await runtime.stop();
await client.stop();
await dapr.stop();
}
store(value, instanceId, signal?) explicitly stores the serialized value, independently of the automatic
threshold or write preference. The workflow only forwards its reference; the activity downloads the document.
A reference-input contract rejects an inline value instead of silently storing or converting it.
Keep activity and child results as references
Activities can return a reference created by ctx.payloads.store(value, ctx.getWorkflowInstanceId()).
Request resultAsReference: true on ctx.callActivity or ctx.callChildWorkflow to receive that reference
without automatically resolving it:
function* documentPipeline(ctx, input) {
const reference = yield ctx.callActivity("createDocument", input, {
resultAsReference: true,
});
return yield ctx.callActivity("measureReference", reference);
}
The producing activity must actually return a reference, or its result must be offloaded by policy.
An inline result fails a reference-result contract, including a small result below the offload threshold.
resultAsReference does not force a write. Forward PayloadReference objects directly, not a plain object
containing one: nested references are not recursively converted or resolved.
Resolve inside a workflow only when necessary
ctx.hasPayloadResolver() reports whether payload support is configured.
yield ctx.resolvePayload(reference) downloads and deserializes the data through an SDK task.
yield ctx.openPayload(reference) obtains a raw Node.js readable stream; consuming it asynchronously requires
an async generator. These read-only APIs are supported, but repeat during replay and sacrifice the principal
benefit of keeping the document out of the orchestrator. Prefer resolving or streaming in activities.
Do not perform arbitrary storage writes from workflow code.
Stream payloads in activities
ctx.payloads.open(reference, signal?) returns a Node.js Readable over the raw stored UTF-8 serialized bytes.
It does not deserialize the document:
async function countStoredBytes(ctx, reference) {
const stream = ctx.payloads.open(reference);
let bytes = 0;
try {
for await (const chunk of stream) {
bytes += chunk.length;
}
} finally {
stream.destroy();
}
return bytes;
}
runtime.registerActivity(countStoredBytes, { inputAsReference: true });
Streaming bypasses maxInMemoryRehydrationSize, but also bypasses buffered SHA-256 verification and
payload-stage reversal. If needed, compute and verify a hash while consuming the stream.
Destroy streams on early exit and handle stream errors. Creating the stream alone does not download all data.
The byte count includes JSON syntax and encoding, not just the length of an application string.
Configuration and policy reference
Offloading uses the serialized value’s UTF-8 byte count, not JavaScript string length. Keys have the form
<keyPrefix>/<instanceId>/<sha256>. References contain the store, key, size, SHA-256 hash, format version,
and optional stage metadata. The version-1 wire envelope uses the reserved $daprRef property.
| Resolver option | Default and behavior |
|---|
storeName | Required binary-store component name. Reads use the store recorded in each reference. |
keyPrefix | "workflow-payloads"; prefix for new content-addressed keys. |
writePreference | PayloadWritePreference.Preserve. See the policy table below. |
offloadSites | PayloadKind.All; bitwise combination of eligible boundary kinds. |
offloadThreshold | 64 * 1024 bytes, inclusive. Positive integer; null offloads every nonempty serialized snapshot eligible under PreferReference. |
skipWriteIfExists | true; tolerates an already-existing content-addressed object rather than overwriting it. Does not mean the SDK skips every upload attempt. |
maxInMemoryRehydrationSize | 16 * 1024 * 1024 bytes. Enforced against both the envelope size and actual buffered bytes. |
verifyIntegrityOnRead | true; buffered reads verify the SHA-256 hash. |
resolutionCacheSize | 128 entries per orchestrator execution. 0 disables caching. |
resolutionCacheBytes | 64 * 1024 * 1024 summed envelope bytes per execution. 0 disables caching; values exceeding the budget are read without being cached. |
manifest | Optional durable, idempotent IWorkflowPayloadManifest implementation for explicit cleanup. |
The cache retains serialized reads, not a durable cross-replay object cache. Both the entry and byte bounds apply.
| Write preference | Ordinary values | Existing top-level references |
|---|
Preserve | Stay inline. | Pass through, except where a cross-app input requires inlining. |
PreferReference | Offload if the site and threshold allow it. | Pass through, except where a cross-app input requires inlining. |
PreferInline | Stay inline. | Resolve before writing the outgoing value. |
offloadSites combines PayloadKind.WorkflowInput, WorkflowOutput, ActivityInput, ActivityOutput,
ExternalEvent, ContinueAsNewInput, CustomStatus, ChildWorkflowInput, and ChildWorkflowOutput.
For example, PayloadKind.ActivityInput | PayloadKind.ActivityOutput limits automatic offloading to activity
boundaries. PayloadKind.None disables new automatic offloading without disabling reads or forwarding.
Detached inputs use WorkflowInput, termination output uses WorkflowOutput, and rerun replacement input
uses WorkflowInput.
Read limits and errors
Buffered resolution rejects staged references because JavaScript does not implement stage reversal.
It also throws PayloadRehydrationLimitError above the configured limit, PayloadNotFoundError for a
missing object, and PayloadIntegrityError for a hash mismatch when verification is enabled.
PayloadNotResolvableError indicates missing resolver support. Invalid or unsupported reserved reference
envelopes throw rather than being treated as ordinary application data.
Changing the write threshold affects new writes only. Lowering the rehydration limit can break existing
ordinary-value consumers and PreferInline conversions. Plan that change against all retained payloads;
use explicit references and streaming for values that must not be fully buffered.
Read client results
getWorkflowState and the wait methods prepare referenced input, output, and custom status before returning.
With a compatible resolver, the ordinary getters use the resolved serialized data. This means fetching
payloads can trigger downloads even when you intend to inspect a reference.
state.getInputReference(), getOutputReference(), and getCustomStatusReference() expose the original
reference. If buffered resolution was refused because the resolver is absent or the size exceeds its limit,
the reference remains accessible, while the corresponding typed getter throws the preparation error:
const state = await client.getWorkflowState(instanceId, true);
const reference = state?.getOutputReference();
if (reference) {
const stream = payloads.open(reference);
try {
for await (const chunk of stream) {
process.stdout.write(chunk);
}
} finally {
stream.destroy();
}
}
Use an appropriate rehydration limit on clients intended to stream large results.
Missing objects and integrity failures reject the state request instead of being deferred.
Serialized getters can still contain the raw envelope when preparation could not resolve it; do not treat
that envelope as an application object. Metadata-only state reads omit input/output fields.
History retrieval and propagated-history views preserve their wire payloads and do not hydrate them.
Cross-app and cross-language calls
Cross-app activity, child, and detached workflow inputs are inline by default, even under
PreferReference. The sender resolves an existing top-level reference before submission; a failed read
prevents submission. The destination should accept an ordinary value for that input.
To forward references, explicitly allow them on the call:
const result = yield ctx.callActivity("remoteDocumentConsumer", reference, {
targetAppId: "document-service",
allowPayloadReferences: true,
});
This permits the resolver’s write policy; it does not force offloading or override PreferInline.
Every reader must resolve the recorded store and key to the same binary object through its own sidecar.
Separate in-memory components do not meet that requirement.
The JavaScript and .NET SDKs share a version-1 envelope, but cross-language use also requires compatible
serialization and contracts. JavaScript buffered reads do not reverse .NET payload stages. Use unstaged,
compatible payloads for such calls; other SDKs must not be assumed to support this scheme.
The inline default protects only top-level call inputs. It does not inline nested references, return
values, external events, or references in propagated ancestor history. Coordinate payload support across
the entire topology before enabling it; do not mix payload-aware and payload-unaware workers for one app ID.
Events and continuation
client.raiseEvent and workflow ctx.sendEvent apply the ExternalEvent write policy.
ctx.waitForExternalEvent(name) resolves an offloaded event as an ordinary value; use
ctx.waitForExternalEvent(name, { asReference: true }) to receive an envelope without downloading.
The sender must actually supply a reference or produce an offloaded event for that contract.
continueAsNew applies the ContinueAsNewInput policy. Unconsumed reference events preserved with
saveEvents: true are carried forward without being materialized. Ensure the next generation’s input
contract is compatible with the new input.
Adopt references in existing applications
First deploy resolver support with Preserve everywhere that may read the histories. This keeps ordinary
new values inline while allowing existing references to be read. Next, enable PreferReference at selected
sites after assessing storage durability, read limits, and every downstream consumer.
Moving from ordinary input to inputAsReference: true is a contract change: old inline histories cannot
satisfy it. Introduce a new registration name/version and route new instances to that contract, keeping the
old implementation for existing instances. Likewise, resultAsReference and asReference must not be
introduced into histories that still contain inline results or events.
To stop creating new automatic references, use Preserve; to inline outgoing references as well, use
PreferInline. Keep the resolver registered while any history, result, event, or consumer can still
contain a reference. Switching write policy does not rewrite retained history.
Retention and cleanup
Neither workflow completion nor purgeWorkflow deletes binary-store objects. A failed scheduling request can
also leave an uploaded object behind. Retain objects for suspended workflows, replay, reruns, descendants,
and forwarded references; deleting a source instance’s objects can break another instance that still uses them.
For explicit cleanup, supply a durable IWorkflowPayloadManifest with asynchronous
record(instanceId, reference, signal?), list(instanceId, signal?), and clear(instanceId, signal?)
methods, then call await payloads.deletePayloads(instanceId) only when no consumer needs those objects.
The resolver records writes in the manifest; it does not discover every forwarded-reference consumer.
Cleanup without a manifest throws. Missing objects are tolerated during deletion; other errors propagate,
and the manifest is cleared only after deletions succeed.
Use application-level retention and access control appropriate to the data. A reference is a locator,
not authorization or a substitute for protecting the binary-store component.