Release sdk-2.0.0-alpha.1
Pre-releaseChangelog
[2.0.0-alpha.1] — alpha — pre-release
Upgrade target for users on the
1.xline (latest:1.1.2).
Highlights
2.0.0 ships five categories of change:
withRetry— a new helper for retrying a chunk of durable logic
(multi-operation blocks containingwaitForCallback,invoke, etc.)
with a configurable backoff strategy.- Linear retry strategy —
createLinearRetryStrategyand the new
retryPresets.linearpreset for fixed-increment backoff alongside the
existing exponential strategies. - Serdes upgrades — three additions:
context.configureSerdes()sets default serdes once instead of
passingserdes:to every operation;createFileSystemSerdesoffloads large payloads to a durable mount
(Amazon S3 Files, EFS) so executions stay under the checkpoint size
limit;- inline previews keep PII out of
GetDurableExecutionHistoryand the
AWS console while the full value lives on disk.
- Cost / scale knob for batch operations —
NestingType.FLATon
mapandparallelskips the per-iterationCONTEXToperation for
up to 2x cost reduction and up to 2x more iterations per execution,
trading per-branch observability for throughput. - More precise error types — promise combinators and callbacks throw
specific error subclasses (PromiseCombinatorError,
CallbackTimeoutError,CallbackSubmitterError). This is the main
source of breaking changes for code that branches oninstanceof.
A handful of operational fixes are also included.
⚠ Upgrade guide (breaking changes)
1. Promise combinator failures now throw PromiseCombinatorError
context.Promise.all, Promise.allSettled, Promise.any, and
Promise.race previously rejected with StepError. They now reject with
PromiseCombinatorError, which extends DurableOperationError directly —
not StepError or ChildContextError.
// v1.x — no longer matches in v2
try {
await context.Promise.all([...]);
} catch (err) {
if (err instanceof StepError) { /* ... */ }
}
// v2.0
import { PromiseCombinatorError } from "@aws/durable-execution-sdk-js";
try {
await context.Promise.all([...]);
} catch (err) {
if (err instanceof PromiseCombinatorError) {
// err.cause is the original failure from the first rejecting branch
}
}If you don't care about distinguishing the operation type, catch the base
class:
import { DurableOperationError } from "@aws/durable-execution-sdk-js";
try { /* ... */ }
catch (err) {
if (err instanceof DurableOperationError) { /* ... */ }
}2. Callback failures split into specific error types
createCallback and waitForCallback previously threw CallbackError for
every failure mode. v2 introduces two new sibling classes that do not
extend CallbackError:
| Scenario | v1.x error | v2 error |
|---|---|---|
Callback completed with FAILED |
CallbackError |
CallbackError |
Callback timed out (TIMED_OUT) |
CallbackError |
CallbackTimeoutError |
waitForCallback submitter threw |
CallbackError |
CallbackSubmitterError |
instanceof CallbackError will silently stop matching for the timeout
and submitter cases.
// v1.x
try {
await context.waitForCallback("approval", submitter, { timeout: { hours: 1 } });
} catch (err) {
if (err instanceof CallbackError) {
// Catches timeout, submitter failure, AND callback-FAILED in v1.x
}
}
// v2.0
import {
CallbackError,
CallbackTimeoutError,
CallbackSubmitterError,
} from "@aws/durable-execution-sdk-js";
try {
await context.waitForCallback("approval", submitter, { timeout: { hours: 1 } });
} catch (err) {
if (err instanceof CallbackTimeoutError) {
// Approver missed the 1h deadline
} else if (err instanceof CallbackSubmitterError) {
// The submitter function (e.g. publishing the approval URL) threw
} else if (err instanceof CallbackError) {
// Callback was completed with status=FAILED
}
}Added
withRetry — retry a block of durable logic
A new helper for retrying chunks of logic that contain operations a
step cannot host (e.g. waitForCallback, invoke). Semantically a
runInChildContext with a retry policy wrapped around it.
import {
withRetry,
createRetryStrategy,
} from "@aws/durable-execution-sdk-js";
// Named (default: wraps in runInChildContext)
const decision = await withRetry(
context,
"approval",
(ctx, attempt) =>
ctx.waitForCallback(`approval-${attempt}`, submitter, {
timeout: { hours: 24 },
}),
{
retryStrategy: createRetryStrategy({
maxAttempts: 3,
initialDelay: { seconds: 2 },
backoffRate: 2,
}),
},
);
// Anonymous
const result = await withRetry(
context,
(ctx, attempt) => ctx.invoke(`charge-${attempt}`, fnArn, payload),
{ retryStrategy: (_err, n) => ({ shouldRetry: n < 3, delay: { seconds: 1 } }) },
);createLinearRetryStrategy + retryPresets.linear
Linear backoff with configurable initial delay and increment.
import {
createLinearRetryStrategy,
retryPresets,
} from "@aws/durable-execution-sdk-js";
// Custom: 8 attempts, starting at 2s, +3s each attempt → 2,5,8,11,14,17,20s
const strategy = createLinearRetryStrategy(8, 2, 3);
await context.step("flaky", fn, { retryStrategy: strategy });
// Or use the new preset (6 attempts: 1,2,3,4,5s)
await context.step("flaky", fn, { retryStrategy: retryPresets.linear });context.configureSerdes() + SerdesConfig
Set default serdes once on the context instead of passing serdes: to
every operation. Defaults flow into step, runInChildContext, invoke,
and waitForCondition. Callbacks (createCallback, waitForCallback)
require an explicit defaultCallbackDeserializer — they keep the
passthrough deserializer otherwise so customer-provided callback payloads
aren't accidentally JSON-parsed.
import {
createClassSerdes,
Serdes,
} from "@aws/durable-execution-sdk-js";
class Order {
id: string = "";
total: number = 0;
}
// Use a class-aware serdes for every step / runInChildContext /
// invoke / waitForCondition in this execution.
context.configureSerdes({
defaultSerdes: createClassSerdes(Order),
});
// Custom serdes work the same way.
const customSerdes: Serdes<unknown> = {
serialize: async (v) => (v === undefined ? undefined : myEncode(v)),
deserialize: async (s) => (s === undefined ? undefined : myDecode(s)),
};
context.configureSerdes({
defaultSerdes: customSerdes,
// Callbacks default to passthrough; opt-in if your callback payloads
// need the same encoding.
defaultCallbackDeserializer: customSerdes,
});Per-operation serdes: arguments still win over the default — configureSerdes
only changes the fallback.
createFileSystemSerdes(basePath, config?)
A built-in Serdes that writes each value to a file under basePath
and stores only a small file pointer in the checkpoint. Use it to keep
executions under the per-checkpoint size limit (~256KB) when individual
operations produce large results.
⚠ Use a durable, shared mount. S3 Files (Lambda S3 mount) and EFS
are supported. Do not point this at Lambda's/tmp—/tmpis
per-execution-environment and a replay running on a different sandbox
won't find the file, breaking deserialization.
import {
createFileSystemSerdes,
FileSystemSerdesMode,
} from "@aws/durable-execution-sdk-js";
// ALWAYS write to file (default) — predictable checkpoint sizes, every
// step result lives on disk.
const always = createFileSystemSerdes("/mnt/s3");
// OVERFLOW: store inline as JSON when it fits; spill to a file only when
// the inline payload would exceed the ~256KB threshold. Best for mixed
// workloads where most results are small.
const overflow = createFileSystemSerdes("/mnt/s3", {
storageMode: FileSystemSerdesMode.OVERFLOW,
});
// Wire it up as the default for the whole execution.
context.configureSerdes({ defaultSerdes: always });
// Or use it for a single operation.
const blob = await context.step("fetch-large-blob", fetchBlob, {
serdes: always,
});The checkpoint envelope is one of:
{ "data": "<inline JSON>" } // OVERFLOW, under threshold
{ "file": "/mnt/s3/<execArn>/<entityId>.json" } // ALWAYS, or OVERFLOW spilledFiles are organized as <basePath>/<urlEncodedExecutionArn>/<entityId>.json,
so each execution's data is namespaced and easy to clean up.
Hiding sensitive data with inline previews
When a step's result contains PII — customer name, address, phone, payment
details — storing it directly in the checkpoint exposes it through the
GetDurableExecutionHistory API and the durable-execution console UI to
anyone with read access. v2 makes it possible to keep the full value
on a durable filesystem (where replay can still read it) while storing
only a redacted preview in the checkpoint that operators see.
createFileSystemSerdes accepts an optional generatePreview function.
The returned object is stored inline in the envelope alongside the file
pointer:
{
"file": "/mnt/s3/<execArn>/1.json",
"preview": { "orderId": "ord-123", "status": "shipped", "total": 99.5 }
}GetDurableExecutionHistory and the AWS console render the preview
field; the file is read only by the SDK during replay.
Example: an order workflow with PII
import {
createFileSystemSerdes,
buildPreview,
PreviewMode,
} from "@aws/durable-execution-sdk-js";
interface OrderResult {
orderId: string;
status: "pending" | "shipped";
total: number;
customer: {
fullName: string; // PII
email: string; // PII
phone: string; // PII
address: { street: string; city: string; zip: string }; // PII
};
}
// Allow-list approach: start from EXCLUDE_ALL and only let the safe
// fields through. New PII fields added later are excluded automatically.
const piiSafeSerdes = createFileSystemSerdes("/mnt/s3", {
generatePreview: (value) =>
buildPreview(value, {
mode: PreviewMode.EXCLUDE_ALL,
include: [
{ name: "orderId" },
{ name: "status" },
{ name: "total" },
],
}),
});
context.configureSerdes({ defaultSerdes: piiSafeSerdes });
// Now any step returning an OrderResult writes the full record to
// /mnt/s3/<execArn>/<entityId>.json. The Lambda checkpoint only stores
// { file: "...", preview: { orderId, status, total } }.
// Replay reads the full file; operators see only the preview.Two complementary patterns
Allow-list (recommended for PII) — start with EXCLUDE_ALL, list the
fields safe to display:
buildPreview(value, {
mode: PreviewMode.EXCLUDE_ALL,
include: [{ name: "orderId" }, { name: "status" }],
});Deny-list with masking — start with INCLUDE_ALL, then exclude
fields that must never appear and mask fields whose presence is useful
for debugging but whose value is sensitive (e.g. show that email exists
without leaking it):
import { FieldMatchMode } from "@aws/durable-execution-sdk-js";
buildPreview(value, {
mode: PreviewMode.INCLUDE_ALL,
exclude: [
{ name: "address" }, // drop entire subtree
{ name: "ssn" },
],
mask: [
{ name: "email" }, // -> "***"
{ name: "phone" },
{ name: "fullName" },
],
maskString: "[REDACTED]", // optional, default "***"
});How mask actually behaves
Three rules govern interaction between include, exclude, and mask:
-
excludealways wins. A field listed in bothexcludeandmask
is excluded — its key never appears in the preview at all. Use this
when even the presence of the field is sensitive. -
maskimplies visibility. When a path matches amaskrule, the
field is emitted withmaskStringas its value regardless of the
mode. InEXCLUDE_ALLmode this means amaskentry alone makes
the field appear (redacted) — you don't also need to add it to
include. Use this to signal "this field exists in the result" without
leaking its value. -
Masking replaces the entire subtree. When the masked path is an
object or array, the whole value is replaced withmaskString;
buildPreviewdoes not recurse into it.// value = { customer: { name: "Alice", address: { city: "Seattle" } } } buildPreview(value, { mode: PreviewMode.EXCLUDE_ALL, mask: [{ name: "customer" }], }); // -> { customer: "***" } // (NOT { customer: { name: "***", address: "***" } })
To redact only the leaves, list each leaf in
maskinstead of the
parent.
Targeting a specific path
Field selectors default to FieldMatchMode.ANYWHERE, which matches the
field name at any depth. Use FieldMatchMode.PATH to pin a selector to
an exact dot-path — useful when the same field name appears in multiple
sub-trees but only some are sensitive:
mask: [
{ name: "customer.email", match: FieldMatchMode.PATH },
{ name: "billing.email", match: FieldMatchMode.PATH },
// Note: this will NOT match a generic top-level `email` field.
],Custom previews
If buildPreview doesn't fit, pass any function that returns the object
to embed:
createFileSystemSerdes("/mnt/s3", {
generatePreview: (value: any) => ({
orderId: value.orderId,
customerInitials: value.customer.fullName
.split(" ").map((n: string) => n[0]).join(""),
summary: `Order ${value.orderId} (${value.status})`,
}),
});NestingType for map, parallel
Trade observability for cost on batch operations. Default is
NestingType.NESTED (today's behavior). Switching to NestingType.FLAT
skips per-iteration CONTEXT operations for up to 2x cost reduction and
higher per-execution scale (up to 2x more iterations in map), at the price of less detailed history.
import { NestingType } from "@aws/durable-execution-sdk-js";
const results = await context.map("process", items, async (ctx, item) => {
return ctx.step("transform", () => transform(item));
}, {
maxConcurrency: 100,
nesting: NestingType.FLAT, // ← new
});Because the default is NESTED, existing code is unaffected unless
you opt in.
errorMapper on ChildConfig
runInChildContext, promise combinators, and withRetry accept a
function to remap the thrown error type. Useful when you want a domain
error class instead of ChildContextError.
class OrderError extends DurableOperationError {
readonly errorType = "OrderError";
}
await context.runInChildContext("place-order", placeOrder, {
errorMapper: (orig) =>
new OrderError(`Order failed: ${orig.message}`, orig.cause),
});Changed
- Promise combinators are now implemented on top of
runInChildContext
(wasstep). This enables Lambda termination during long idle waits in
combinators and is a prerequisite for the typedPromiseCombinatorError
above. (#435, #436, #441) FileSystemSerdesConfig.mode→storageMode. Only relevant if you
tracked the2.0.0-alpha.1line; v1.x users are unaffected. (#536)- UserAgent header uses
aws-durable-execution-sdk-js/<version>and
appends-bundledwhen running from the Lambda bundled runtime path.
(#522, internal commit5f951b1)
Fixed
- KMS exceptions during
CheckpointDurableExecutionand
GetDurableExecutionStateare now treated as non-retryable customer
errors. See upgrade guide §3. (#508) fix(eslint-plugin): removecontext.getSourceCode()so the plugin
works on ESLint v10. (#472)fix(sdk): resolve ESLint warnings surfaced during the build. (#483)fix(examples): prevent duplicateDeleteFunctioncalls during
cleanup in integration tests. (#486)