
Aws Lambda Durable Functions
- 71 installs
- 850 repo stars
- Updated August 3, 2026
- awslabs/agent-plugins
AWS Lambda durable functions is a Claude skill that helps developers build resilient long-running multi-step AWS Lambda applications using the Durable Execution SDK, covering the replay model, step operations, and saga e
About
AWS Lambda durable functions is a skill for building long-running, multi-step serverless applications on AWS Lambda with automatic state persistence and retry logic. A developer uses it when writing stateful Lambda handlers that must survive interruptions, coordinate steps, wait on callbacks, or implement saga-style compensation. It codifies the replay model rules and provides code patterns, IaC deployment guidance, and local testing setup.
- Builds resilient multi-step Lambda applications that run up to 1 year despite interruptions
- Covers the replay/determinism model, step operations, wait/callback and saga error patterns
- Ships getting-started, testing, deployment IaC and troubleshooting reference guides
Aws Lambda Durable Functions by the numbers
- 71 all-time installs (skills.sh)
- Ranked #648 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
aws-lambda-durable-functions capabilities & compatibility
Requires an AWS account; Lambda durable execution incurs AWS usage costs
- Capabilities
- aws step functions · workflow orchestration · serverless deployment
- Works with
- aws
- Use cases
- orchestration · api development · devops
- Pricing
- Bring your own API key
What aws-lambda-durable-functions says it does
Build resilient multi-step applications and AI workflows that can execute for up to 1 year while maintaining reliable progress despite interruptions.
npx skills add https://github.com/awslabs/agent-plugins --skill aws-lambda-durable-functionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 71 |
|---|---|
| repo stars | ★ 850 |
| Last updated | August 3, 2026 |
| Repository | awslabs/agent-plugins ↗ |
What it does
Build a stateful Lambda handler that orchestrates multiple steps with checkpoints, retries, and callbacks over a long-running execution.
Who is it for?
Long-running, stateful Lambda workflows needing retries, checkpoints, and callbacks
Skip if: Simple stateless Lambda functions with no orchestration needs
When should I use this skill?
You are writing Lambda handlers that must persist state, retry steps, or wait on external callbacks over long executions.
What you get
A durable Lambda handler with replay-safe steps, retry logic, wait/callback support, and IaC deployment.
- Durable Lambda handler code
- Step/wait/saga patterns
- IaC deployment config
By the numbers
- executes for up to 1 year
- 11 reference guides bundled
Files
AWS Lambda durable functions
Build resilient multi-step applications and AI workflows that can execute for up to 1 year while maintaining reliable progress despite interruptions.
Onboarding
Step 1: Validate Prerequisites
Before using AWS Lambda durable functions, verify:
1. AWS CLI is installed (2.33.22 or higher) and configured:
aws --version
aws sts get-caller-identity2. Runtime environment is ready:
- For TypeScript/JavaScript: Node.js 22+ (
node --version) - For Python: Python 3.11+ (
python --version. Note that currently only Lambda runtime environments 3.13+ come with the Durable Execution SDK pre-installed. 3.11 is the min supported Python version by the Durable SDK itself, however, you could use OCI to bring your own container image with your own Python runtime + Durable SDK.)
3. Deployment capability exists (one of):
- AWS SAM CLI (
sam --version) 1.153.1 or higher - AWS CDK (
cdk --version) v2.237.1 or higher - Direct Lambda deployment access
Step 2: Select language and IaC framework
Language Selection
Default: TypeScript
Override syntax:
- "use Python" → Generate Python code
- "use JavaScript" → Generate JavaScript code
When not specified, ALWAYS use TypeScript
IaC framework selection
Default: CDK
Override syntax:
- "use CloudFormation" → Generate YAML templates
- "use SAM" → Generate YAML templates
When not specified, ALWAYS use CDK
Error Scenarios
Unsupported Language
- List detected language
- State: "Durable Execution SDK is not yet available for [framework]"
- Suggest supported languages as alternatives
Unsupported IaC Framework
- List detected framework
- State: "[framework] might not support Lambda durable functions yet"
- Suggest supported frameworks as alternatives
Serverless MCP Server Unavailable
- Inform user: "AWS Serverless MCP not responding"
- Ask: "Proceed without MCP support?"
- DO NOT continue without user confirmation
Step 3: Install SDK
For TypeScript/JavaScript:
npm install @aws/durable-execution-sdk-js
npm install --save-dev @aws/durable-execution-sdk-js-testingFor Python:
pip install aws-durable-execution-sdk-python
pip install aws-durable-execution-sdk-python-testingWhen to Load Reference Files
Load the appropriate reference file based on what the user is working on:
- Getting started, basic setup, example, ESLint, or Jest setup -> see getting-started.md
- Understanding replay model, determinism, or non-deterministic errors -> see replay-model-rules.md
- Creating steps, atomic operations, or retry logic -> see step-operations.md
- Waiting, delays, callbacks, external systems, or polling -> see wait-operations.md
- Parallel execution, map operations, batch processing, or concurrency -> see concurrent-operations.md
- Error handling, retry strategies, saga pattern, or compensating transactions -> see error-handling.md
- Advanced error handling, timeout handling, circuit breakers, or conditional retries -> see advanced-error-handling.md
- Testing, local testing, cloud testing, test runner, or flaky tests -> see testing-patterns.md
- Deployment, CloudFormation, CDK, SAM, log groups, deploy, or infrastructure -> see deployment-iac.md
- Advanced patterns, GenAI agents, completion policies, step semantics, or custom serialization -> see advanced-patterns.md
- troubleshooting, stuck execution, failed execution, debug execution ID, execution history, execution error, why did my execution fail, execution timed out, callback not received, diagnose execution, or root cause execution -> see troubleshooting-executions.md
Quick Reference
Basic Handler Pattern
TypeScript:
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js';
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const result = await context.step('process', async () => processData(event));
return result;
});Python:
from aws_durable_execution_sdk_python import durable_execution, DurableContext
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
result = context.step(lambda _: process_data(event), name='process')
return resultCritical Rules
1. All non-deterministic code MUST be in steps (Date.now, Math.random, API calls) 2. Cannot nest durable operations - use runInChildContext to group operations 3. Closure mutations are lost on replay - return values from steps 4. Side effects outside steps repeat - use context.logger (replay-aware)
Python API Differences
The Python SDK differs from TypeScript in several key areas:
- Steps: Use
@durable_stepdecorator +context.step(my_step(args)), or inlinecontext.step(lambda _: ..., name='...'). Prefer the decorator for automatic step naming. - Wait:
context.wait(duration=Duration.from_seconds(n), name='...') - Exceptions:
ExecutionError(permanent),InvocationError(transient),CallbackError(callback failures) - Testing: Use
DurableFunctionTestRunnerclass directly - instantiate with handler, use context manager, callrun(input=...)
Invocation Requirements
Durable functions require qualified ARNs (version, alias, or $LATEST):
# Valid
aws lambda invoke --function-name my-function:1 output.json
aws lambda invoke --function-name my-function:prod output.json
# Invalid - will fail
aws lambda invoke --function-name my-function output.jsonIAM Permissions
Your Lambda execution role MUST have the AWSLambdaBasicDurableExecutionRolePolicy managed policy attached. This includes:
lambda:CheckpointDurableExecution- Persist execution statelambda:GetDurableExecutionState- Retrieve execution state- CloudWatch Logs permissions
Additional permissions needed for:
- Durable invokes:
lambda:InvokeFunctionon target function ARNs - External callbacks: Systems need
lambda:SendDurableExecutionCallbackSuccessandlambda:SendDurableExecutionCallbackFailure
Validation Guidelines
When writing or reviewing durable function code, ALWAYS check for these replay model violations:
1. Non-deterministic code outside steps: Date.now(), Math.random(), UUID generation, API calls, database queries must all be inside steps 2. Nested durable operations in step functions: Cannot call context.step(), context.wait(), or context.invoke() inside a step function — use context.runInChildContext() instead 3. Closure mutations that won't persist: Variables mutated inside steps are NOT preserved across replays — return values from steps instead 4. Side effects outside steps that repeat on replay: Use context.logger for logging (it is replay-aware and deduplicates automatically)
When implementing or modifying tests for durable functions, ALWAYS verify:
1. All operations have descriptive names 2. Tests get operations by NAME, never by index 3. Replay behavior is tested with multiple invocations 4. Use LocalDurableTestRunner for local testing
MCP Server Configuration
Write access is enabled by default. The plugin ships with --allow-write in .mcp.json, so the MCP server can create projects, generate IaC, and deploy on behalf of the user.
Access to sensitive data (like Lambda and API Gateway logs) is not enabled by default. To grant it, add --allow-sensitive-data-access to .mcp.json.
Resources
Advanced Error Handling
Advanced error handling patterns for durable functions, including timeout handling, circuit breakers, and conditional retry strategies.
Timeout Handling with Callbacks
Pattern: Wait for an external callback with a timeout, and implement fallback logic if the timeout is reached.
Implementation approach:
1. Use waitForCallback (TypeScript) or wait_for_callback (Python) with a timeout configuration set in the config argument 2. Wrap in try-catch to handle timeout errors 3. Check if the error is a timeout 4. Implement fallback logic in a step (e.g., escalate to manager, use default value, retry with different parameters) 5. Return appropriate status indicating timeout occurred
Key considerations:
- Timeout errors are thrown when the callback doesn't complete within the specified duration
- Fallback logic should be in a step to ensure it's checkpointed
- Log timeout events for monitoring and debugging
Local Timeout with Promise.race in Typescript SDK
Pattern: Implement a timeout for a step operation within a single Lambda invocation.
Implementation approach:
1. Use Promise.race() to race the step operation against a timeout promise 2. The timeout promise rejects after the specified duration 3. Catch the timeout error and implement fallback logic 4. Execute fallback operation in a separate step
Important limitation: In TypeScript, native setTimeout (and patterns like Promise.race using it) will fail during execution replays. To create a reliable timeout that persists across execution (expands over multi invocations), always use the timeout parameter provided by waitForCallback or waitForCondition
Conditional Retry Based on Error Type
Pattern: Retry operations selectively based on the type of error encountered.
Implementation approach:
1. Define a custom retry strategy function that examines the error 2. For client errors (4xx): Don't retry - these are permanent failures 3. For server errors (5xx): Retry with exponential backoff 4. For network errors: Retry with fixed delay 5. For unknown errors: Don't retry by default
Key considerations:
- Client errors (400-499) typically indicate bad input and shouldn't be retried
- Server errors (500-599) are often transient and benefit from retry
- Network errors (connection refused, timeout) should retry with reasonable limits
- Use exponential backoff for server errors to avoid overwhelming the service
- Set maximum retry attempts to prevent infinite loops
Circuit Breaker Pattern
Pattern: Temporarily stop making requests to a failing external service to prevent cascading failures.
Implementation approach:
1. Track failure count and last failure time (note: these reset on replay due to closure mutations) 2. Check if circuit is "open" (too many recent failures) 3. If open, throw a circuit breaker error and wait before retrying 4. If closed, attempt the operation 5. On success, reset failure count 6. On failure, increment failure count and record timestamp 7. Configure retry strategy to wait longer when circuit is open
Important caveat: The example implementations use closure variables (failureCount, lastFailureTime) which reset on replay. For production use, store circuit breaker state in:
- A step return value that persists across replays
- An external store like DynamoDB
- A durable variable pattern
Key considerations:
- Circuit breaker prevents cascading failures to downstream services
- The "open" duration should be long enough for the service to recover
- Reset the circuit on successful operations
- Log circuit state changes for monitoring
Error Handling Best Practices
1. Timeout Handling: Always implement fallback logic for callback timeouts - don't let executions fail silently 2. Conditional Retries: Classify errors as transient vs permanent, only retry transient errors 3. Circuit Breakers: Protect against cascading failures to external services, especially for high-volume operations 4. Structured Logging: Log error context (error type, attempt count, operation name) for debugging 5. Graceful Degradation: Return partial results when possible rather than failing completely 6. Error Classification: Distinguish between client errors (don't retry), server errors (retry with backoff), and network errors (retry with fixed delay)
Common Error Patterns
Transient Errors (Should Retry)
- Network timeouts
- Service unavailable (503)
- Rate limiting (429)
- Database connection failures
- Temporary infrastructure issues
Permanent Errors (Should Not Retry)
- Invalid input (400)
- Authentication failures (401, 403)
- Resource not found (404)
- Business logic violations
- Validation errors
Timeout Errors (Need Fallback)
- Callback timeouts - external system didn't respond in time
- External system delays - service is slow or unresponsive
- Long-running operations - operation exceeded expected duration
Advanced Patterns
Advanced techniques and patterns for sophisticated durable function workflows.
Advanced GenAI Agent Patterns
Agent with Reasoning and Dynamic Step Naming
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
context.logger.info('Starting AI agent', { prompt: event.prompt });
const messages = [{ role: 'user', content: event.prompt }];
while (true) {
// Invoke AI model with reasoning
const { response, reasoning, tool } = await context.step(
'invoke-model',
async (stepCtx) => {
stepCtx.logger.info('Invoking AI model', {
messageCount: messages.length
});
return await invokeAIModel(messages);
}
);
// Log AI's reasoning
if (reasoning) {
context.logger.debug('AI reasoning', { reasoning });
}
// If no tool needed, return response
if (tool == null) {
context.logger.info('AI agent completed - no tool needed');
return response;
}
// Execute tool with dynamic step naming
const toolResult = await context.step(
`execute-tool-${tool.name}`, // Dynamic step name
async (stepCtx) => {
stepCtx.logger.info('Executing tool', {
toolName: tool.name,
toolParams: tool.parameters
});
return await executeTool(tool, response);
}
);
// Add result to conversation
messages.push({
role: 'assistant',
content: toolResult,
});
context.logger.debug('Tool result added', {
toolName: tool.name,
resultLength: toolResult.length
});
}
});Python:
# Note: invoke_ai_model and execute_tool are decorated with @durable_step
@durable_execution
def handler(event: dict, context: DurableContext) -> str:
context.logger.info('Starting AI agent', extra={'prompt': event['prompt']})
messages = [{'role': 'user', 'content': event['prompt']}]
while True:
# Invoke AI model
result = context.step(invoke_ai_model(messages))
response = result['response']
reasoning = result.get('reasoning')
tool = result.get('tool')
if reasoning:
context.logger.debug('AI reasoning', extra={'reasoning': reasoning})
if tool is None:
context.logger.info('AI agent completed')
return response
# Execute tool with dynamic step naming
tool_result = context.step(
func=execute_tool(tool, response),
name=f"execute-tool-{tool['name']}"
)
messages.append({'role': 'assistant', 'content': tool_result})
context.logger.debug('Tool result added', extra={'tool': tool['name']})Step Semantics Deep Dive
AtMostOncePerRetry vs AtLeastOncePerRetry
TypeScript:
import { StepSemantics } from '@aws/durable-execution-sdk-js';
// AtMostOncePerRetry (DEFAULT) - For idempotent operations
// Step executes at most once per retry attempt
// If step fails partway through, it won't re-execute the same attempt
await context.step(
'update-database',
async () => {
// This is idempotent - safe to retry
return await updateUserRecord(userId, data);
},
{ semantics: StepSemantics.AtMostOncePerRetry }
);
// AtLeastOncePerRetry - For operations that can execute multiple times
// Step may execute multiple times per retry attempt
// Use when idempotency is handled externally
await context.step(
'send-notification',
async () => {
// External system handles deduplication
return await sendEmail(email, message);
},
{ semantics: StepSemantics.AtLeastOncePerRetry }
);When to use each:
| Semantic | Use When | Example Operations |
|---|---|---|
| AtMostOncePerRetry | Operation is idempotent | Database updates, API calls with idempotency keys |
| AtLeastOncePerRetry | External deduplication exists | Queuing systems, event streams |
Completion Policies - Interaction and Combination
Combining Multiple Constraints
Completion policies can be combined, and execution stops when the first constraint is met:
TypeScript:
const results = await context.map(
'process-items',
items,
processFunc,
{
completionConfig: {
minSuccessful: 8, // Need at least 8 successes
toleratedFailureCount: 2, // OR can tolerate 2 failures
toleratedFailurePercentage: 20, // OR can tolerate 20% failures
}
}
);
// Execution stops when ANY of these conditions is met:
// 1. 8 successful items (minSuccessful reached)
// 2. 2 failures occur (toleratedFailureCount reached)
// 3. 20% of items fail (toleratedFailurePercentage reached)Understanding Stop Conditions
Example with 10 items:
const items = Array.from({ length: 10 }, (_, i) => i);
const results = await context.map(
'process',
items,
processFunc,
{
maxConcurrency: 3,
completionConfig: {
minSuccessful: 7,
toleratedFailureCount: 3
}
}
);
// Scenario 1: 7 successes, 0 failures
// ✅ Stops after 7th success (minSuccessful reached)
// Remaining 3 items are not processed
// Scenario 2: 5 successes, 3 failures
// ❌ Stops after 3rd failure (toleratedFailureCount reached)
// Remaining 2 items are not processed
// results.throwIfError() will throw because minSuccessful not met
// Scenario 3: 7 successes, 2 failures
// ✅ Stops after 7th success (minSuccessful reached)
// 1 item not processed, but completion policy satisfiedEarly Termination Pattern
Use completion policies for early termination when searching:
TypeScript:
// Stop after finding first match
const results = await context.map(
'find-match',
candidates,
async (ctx, candidate) => {
return await ctx.step(async () => checkMatch(candidate));
},
{
completionConfig: {
minSuccessful: 1 // Stop after first success
}
}
);
// Only one item processed (assuming first succeeds)
if (results.successCount > 0) {
const match = results.getSucceeded()[0];
context.logger.info('Found match', { match });
}Advanced Error Handling
For timeout handling (waitForCallback, Promise.race), conditional retries, and circuit breaker patterns, see advanced-error-handling.md.
Advanced and Retry Strategies
For conditional retry strategies and circuit breaker patterns, see advanced-error-handling.md.
Custom Serialization Patterns
Class with Date Fields
TypeScript:
import {
createClassSerdesWithDates
} from '@aws/durable-execution-sdk-js';
class User {
constructor(
public name: string,
public email: string,
public createdAt: Date,
public updatedAt: Date
) {}
}
const result = await context.step(
'create-user',
async () => new User('Alice', 'alice@example.com', new Date(), new Date()),
{
serdes: createClassSerdesWithDates(User, ['createdAt', 'updatedAt'])
}
);
// result is properly deserialized User instance with Date objects
console.log(result.createdAt instanceof Date); // trueComplex Object Graphs
TypeScript:
import { createClassSerdes } from '@aws/durable-execution-sdk-js';
class Order {
constructor(
public id: string,
public items: OrderItem[],
public customer: Customer
) {}
}
class OrderItem {
constructor(public sku: string, public quantity: number) {}
}
class Customer {
constructor(public id: string, public name: string) {}
}
// Create serdes for each class
const orderSerdes = createClassSerdes(Order);
const itemSerdes = createClassSerdes(OrderItem);
const customerSerdes = createClassSerdes(Customer);
const result = await context.step(
'process-order',
async () => {
const customer = new Customer('CUST-123', 'Alice');
const items = [
new OrderItem('SKU-001', 2),
new OrderItem('SKU-002', 1)
];
return new Order('ORD-456', items, customer);
},
{ serdes: orderSerdes }
);Nested Workflows
Parent-Child Workflow Pattern
TypeScript:
// Parent orchestrator
export const orchestrator = withDurableExecution(
async (event, context: DurableContext) => {
const childFunctionArn = process.env.CHILD_FUNCTION_ARN!;
// Invoke child workflows in parallel
const results = await context.parallel(
'process-batches',
[
{
name: 'batch-1',
func: async (ctx) => ctx.invoke(
'process-batch-1',
childFunctionArn,
{ batch: event.batches[0] }
)
},
{
name: 'batch-2',
func: async (ctx) => ctx.invoke(
'process-batch-2',
childFunctionArn,
{ batch: event.batches[1] }
)
}
]
);
return results.getResults();
}
);
// Child worker
export const worker = withDurableExecution(
async (event, context: DurableContext) => {
const items = event.batch.items;
const results = await context.map(
'process-items',
items,
async (ctx, item) => {
return await ctx.step(async () => processItem(item));
}
);
return results.getResults();
}
);Best Practices Summary
1. Dynamic Step Naming: Use template literals for dynamic operation names 2. Structured Logging: Log reasoning and context with each operation 3. Error Handling: See advanced-error-handling.md for timeout, retry, and circuit breaker patterns 4. Completion Policies: Understand how combined constraints interact 5. Custom Serialization: Use proper serdes for complex objects 6. Nested Workflows: Use invoke for modular, composable architectures
Concurrent Operations
Process arrays and run operations in parallel with concurrency control.
Map Operations
Process arrays with automatic concurrency control and completion policies:
TypeScript:
const items = [1, 2, 3, 4, 5];
const results = await context.map(
'process-items',
items,
async (ctx, item, index) => {
return await ctx.step(`process-${index}`, async () =>
processItem(item)
);
},
{
maxConcurrency: 3,
completionConfig: {
minSuccessful: 4,
toleratedFailureCount: 1
}
}
);
results.throwIfError();
const allResults = results.getResults();Python:
# Note: process is decorated with @durable_step
from aws_durable_execution_sdk_python.config import MapConfig, CompletionConfig
items = [1, 2, 3, 4, 5]
def process_item(ctx: DurableContext, item: int, index: int, items: list):
return ctx.step(process(item), name=f'process-{index}')
results = context.map(
inputs=items,
func=process_item,
name='process-items',
config=MapConfig(
max_concurrency=3,
completion_config=CompletionConfig(
min_successful=4,
tolerated_failure_count=1
)
)
)
results.throw_if_error()
all_results = results.get_results()Parallel Operations
Run heterogeneous operations concurrently:
TypeScript:
const results = await context.parallel(
'parallel-ops',
[
{
name: 'fetch-user',
func: async (ctx) => ctx.step(async () => fetchUser(userId))
},
{
name: 'fetch-orders',
func: async (ctx) => ctx.step(async () => fetchOrders(userId))
},
{
name: 'fetch-preferences',
func: async (ctx) => ctx.step(async () => fetchPreferences(userId))
}
],
{ maxConcurrency: 3 }
);
const [user, orders, preferences] = results.getResults();Python:
# Note: fetch_user, fetch_orders, fetch_preferences are decorated with @durable_step
from aws_durable_execution_sdk_python.config import ParallelConfig
def fetch_user_data(ctx: DurableContext):
return ctx.step(fetch_user(user_id))
def fetch_orders_data(ctx: DurableContext):
return ctx.step(fetch_orders(user_id))
def fetch_prefs_data(ctx: DurableContext):
return ctx.step(fetch_preferences(user_id))
results = context.parallel(
[fetch_user_data, fetch_orders_data, fetch_prefs_data],
name='parallel-ops',
config=ParallelConfig(max_concurrency=3)
)
user, orders, preferences = results.get_results()Completion Policies
Minimum Successful
Require a minimum number of successful operations:
TypeScript:
const results = await context.map(
'process-batch',
items,
async (ctx, item, index) => ctx.step(async () => process(item)),
{
completionConfig: {
minSuccessful: 8 // Need at least 8 successes
}
}
);Tolerated Failures
Allow a specific number of failures:
TypeScript:
const results = await context.map(
'process-batch',
items,
async (ctx, item, index) => ctx.step(async () => process(item)),
{
completionConfig: {
toleratedFailureCount: 2 // Allow up to 2 failures
}
}
);Tolerated Failure Percentage
Allow a percentage of failures:
TypeScript:
const results = await context.map(
'process-batch',
items,
async (ctx, item, index) => ctx.step(async () => process(item)),
{
completionConfig: {
toleratedFailurePercentage: 10 // Allow up to 10% failures
}
}
);Python:
results = context.map(
inputs=items,
func=process_item,
config=MapConfig(
completion_config=CompletionConfig(
tolerated_failure_percentage=10
)
),
name='process-batch'
)Batch Result Handling
Check Status
TypeScript:
const results = await context.map('process', items, processFunc);
console.log(results.status); // 'COMPLETED' | 'FAILED'
console.log(results.totalCount); // Total items
console.log(results.startedCount); // Items started
console.log(results.successCount); // Successful items
console.log(results.failureCount); // Failed items
console.log(results.hasFailure()); // BooleanGet Results
TypeScript:
// Get all results (throws if any failed)
const allResults = results.getResults();
// Get successful results only
const successful = results.succeeded.map(item => item.result);
// Get failed items
const failed = results.failed.map(item => ({
index: item.index,
error: item.error
}));
// Get all items with status
const all = results.all.map(item => ({
index: item.index,
status: item.status,
result: item.result,
error: item.error
}));Error Handling
TypeScript:
const results = await context.map('process', items, processFunc);
if (results.hasFailure()) {
context.logger.error('Some items failed', {
failureCount: results.failureCount,
failures: results.failed.map(f => f.index)
});
// Retry failed items
const failedItems = results.failed.map(f => items[f.index]);
await context.map('retry-failed', failedItems, processFunc);
}Concurrency Control
Fixed Concurrency
TypeScript:
const results = await context.map(
'process',
items,
processFunc,
{ maxConcurrency: 5 } // Process 5 items at a time
);Dynamic Concurrency
Adjust based on item characteristics:
TypeScript:
const results = await context.map(
'process',
items,
async (ctx, item, index) => {
// Heavy items get their own processing
if (item.size > 1000) {
return await ctx.step(`heavy-${index}`, async () =>
processHeavy(item)
);
}
// Light items can be batched
return await ctx.step(`light-${index}`, async () =>
processLight(item)
);
},
{ maxConcurrency: 10 }
);Advanced Patterns
Map with Callbacks
TypeScript:
const results = await context.map(
'process-with-approval',
items,
async (ctx, item, index) => {
const processed = await ctx.step('process', async () =>
process(item)
);
const approved = await ctx.waitForCallback(
'approval',
async (callbackId) => sendApproval(item, callbackId),
{ timeout: { hours: 24 } }
);
return { processed, approved };
},
{ maxConcurrency: 3 }
);Nested Map Operations
TypeScript:
const results = await context.map(
'process-batches',
batches,
async (ctx, batch, batchIndex) => {
return await ctx.map(
`batch-${batchIndex}`,
batch.items,
async (itemCtx, item, itemIndex) => {
return await itemCtx.step(async () => process(item));
}
);
}
);Map with Child Contexts
TypeScript:
const results = await context.map(
'complex-process',
items,
async (ctx, item, index) => {
return await ctx.runInChildContext(`item-${index}`, async (childCtx) => {
const validated = await childCtx.step('validate', async () =>
validate(item)
);
await childCtx.wait({ seconds: 1 });
const processed = await childCtx.step('process', async () =>
process(validated)
);
return processed;
});
},
{ maxConcurrency: 5 }
);Performance Optimization
Batch Size Selection
// Small items: Higher concurrency
const results = await context.map(
'small-items',
smallItems,
processFunc,
{ maxConcurrency: 20 }
);
// Large items: Lower concurrency
const results = await context.map(
'large-items',
largeItems,
processFunc,
{ maxConcurrency: 3 }
);Early Termination
Use completion policies to stop early:
const results = await context.map(
'find-match',
candidates,
async (ctx, candidate) => {
return await ctx.step(async () => checkMatch(candidate));
},
{
completionConfig: {
minSuccessful: 1 // Stop after first success
}
}
);Best Practices
1. Set appropriate maxConcurrency based on downstream system capacity 2. Use completion policies to handle partial failures gracefully 3. Name all operations for debugging 4. Handle batch results explicitly - check for failures 5. Consider retry strategies for failed items 6. Monitor concurrency limits to avoid overwhelming systems 7. Use child contexts for complex per-item workflows 8. Implement circuit breakers for external service calls
Deployment with Infrastructure as Code
Deploy durable functions using CloudFormation, CDK, or SAM.
IaC framework selection
Default: CDK
Override syntax:
- "use CloudFormation" → Generate YAML templates
- "use SAM" → Generate YAML templates
When not specified, ALWAYS use CDK
Error Scenario: Unsupported IaC Framework
- List detected framework
- State: "[framework] might not support Lambda durable functions yet"
- Suggest supported frameworks as alternatives
Requirements
All durable functions require:
1. DurableConfig property on the function 2. AWSLambdaBasicDurableExecutionRolePolicy attached to execution role 3. Qualified ARN (version or alias) for invocation
AWS CloudFormation
template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Resources:
DurableFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy
DurableFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: myDurableFunction
Runtime: nodejs24.x # or python3.14
Handler: index.handler
Role: !GetAtt DurableFunctionRole.Arn
Code:
ZipFile: |
// Your durable function code
DurableConfig:
ExecutionTimeout: 3600 # Max execution time (seconds)
RetentionPeriodInDays: 7 # How long to keep execution state
Environment:
Variables:
LOG_LEVEL: INFO
DurableFunctionVersion:
Type: AWS::Lambda::Version
Properties:
FunctionName: !Ref DurableFunction
DurableFunctionAlias:
Type: AWS::Lambda::Alias
Properties:
FunctionName: !Ref DurableFunction
FunctionVersion: !GetAtt DurableFunctionVersion.Version
Name: prod
Outputs:
FunctionArn:
Value: !GetAtt DurableFunction.Arn
AliasArn:
Value: !Ref DurableFunctionAliasDeploy:
aws cloudformation deploy \
--template-file template.yaml \
--stack-name my-durable-function \
--capabilities CAPABILITY_IAMAWS CDK
TypeScript:
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
export class DurableFunctionStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const durableFunction = new lambda.Function(this, 'DurableFunction', {
runtime: lambda.Runtime.NODEJS_24_X, // or PYTHON_3_14
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
durableConfig: {
executionTimeout: cdk.Duration.hours(1),
retentionPeriod: cdk.Duration.days(7)
},
environment: {
LOG_LEVEL: 'INFO'
}
});
// CDK automatically adds checkpoint permissions when durableConfig is set
// Create version and alias
const version = durableFunction.currentVersion;
const alias = new lambda.Alias(this, 'ProdAlias', {
aliasName: 'prod',
version: version
});
// Output the qualified ARN
new cdk.CfnOutput(this, 'FunctionAliasArn', {
value: alias.functionArn
});
}
}Deploy:
cdk deployCDK Custom Log Group Management
Best Practice: Explicitly create and manage CloudWatch Log Groups for better control over retention, cleanup, and costs.
import * as logs from 'aws-cdk-lib/aws-logs';
import * as iam from 'aws-cdk-lib/aws-iam';
// 1. Create explicit log group
const functionLogGroup = new logs.LogGroup(this, 'DurableFunctionLogGroup', {
logGroupName: '/aws/lambda/myDurableFunction',
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Delete on stack destroy
});
// 2. Link to function
const durableFunction = new lambda.Function(this, 'DurableFunction', {
runtime: lambda.Runtime.NODEJS_24_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
logGroup: functionLogGroup, // Link to managed log group
durableConfig: {
executionTimeout: cdk.Duration.hours(1),
retentionPeriod: cdk.Duration.days(7)
}
});
// 3. Add durable execution policy (required with explicit log groups)
durableFunction.role?.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName(
'service-role/AWSLambdaBasicDurableExecutionRolePolicy'
)
);Benefits:
- Explicit Cleanup:
removalPolicy: cdk.RemovalPolicy.DESTROYensures log groups are deleted when stack is destroyed - Custom Retention: Set retention periods matching compliance/debugging needs
- Predictable Naming: Control exact log group name
- Cost Control: Avoid accumulating costs from orphaned log groups
When to use:
- ✅ Production environments where log retention policies must be enforced
- ✅ Development/test environments where automatic cleanup saves costs
- ✅ Multi-function stacks where consistent log management is needed
Important: Don't forget to add AWSLambdaBasicDurableExecutionRolePolicy when using explicit log groups.
AWS SAM
template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 900
MemorySize: 512
Resources:
DurableFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: myDurableFunction
Runtime: nodejs24.x # or python3.14
Handler: index.handler
CodeUri: ./src
DurableConfig:
ExecutionTimeout: 3600
RetentionPeriodInDays: 7
Policies:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy
AutoPublishAlias: prod
Environment:
Variables:
LOG_LEVEL: INFO
Outputs:
FunctionArn:
Value: !GetAtt DurableFunction.Arn
AliasArn:
Value: !Ref DurableFunction.AliasDeploy:
sam build
sam deploy --guidedDurable Invokes
For functions that invoke other durable functions:
CloudFormation:
DurableFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy
Policies:
- PolicyName: InvokeOtherFunctions
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource:
- !GetAtt TargetFunction.Arn
- !Sub '${TargetFunction.Arn}:*' # For versions/aliasesCDK:
const targetFunction = new lambda.Function(this, 'TargetFunction', {
// ... configuration
});
const orchestratorFunction = new lambda.Function(this, 'OrchestratorFunction', {
// ... configuration with durableConfig
});
// Grant invoke permission
targetFunction.grantInvoke(orchestratorFunction);External Callbacks
For external systems to send callbacks:
IAM Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:SendDurableExecutionCallbackSuccess",
"lambda:SendDurableExecutionCallbackFailure",
"lambda:SendDurableExecutionCallbackHeartbeat"
],
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:myDurableFunction:*"
}
]
}Environment Configuration
Development:
DurableFunction:
Type: AWS::Lambda::Function
Properties:
DurableConfig:
ExecutionTimeout: 900 # 15 minutes
RetentionPeriodInDays: 1 # Short retention
Environment:
Variables:
LOG_LEVEL: DEBUG
ENVIRONMENT: developmentProduction:
DurableFunction:
Type: AWS::Lambda::Function
Properties:
DurableConfig:
ExecutionTimeout: 86400 # 24 hours
RetentionPeriodInDays: 30 # Long retention
Environment:
Variables:
LOG_LEVEL: INFO
ENVIRONMENT: productionMulti-Environment Deployment
CDK with Stages:
const app = new cdk.App();
new DurableFunctionStack(app, 'DurableFunction-Dev', {
env: { account: '123456789012', region: 'us-east-1' },
stage: 'dev',
durableConfig: {
executionTimeout: cdk.Duration.minutes(15),
retentionPeriod: cdk.Duration.days(1)
}
});
new DurableFunctionStack(app, 'DurableFunction-Prod', {
env: { account: '123456789012', region: 'us-east-1' },
stage: 'prod',
durableConfig: {
executionTimeout: cdk.Duration.hours(24),
retentionPeriod: cdk.Duration.days(30)
}
});Invocation Examples
Critical Requirements
⚠️ Important Invocation Rules:
1. Qualified Function Name Required: You MUST provide a qualified function name with version, alias, or :$LATEST 2. Idempotency with durable-execution-name: Use this parameter to ensure the same execution name always refers to the same execution 3. Binary Format: Use --cli-binary-format raw-in-base64-out to avoid base64 encoding issues
Synchronous Invocation (RequestResponse)
Synchronous invocation waits for the function to complete and returns the result immediately. Suitable for short workflows.
aws lambda invoke \
--function-name 'myDurableFunction:$LATEST' \
--invocation-type RequestResponse \
--durable-execution-name "execution-123" \
--payload '{"userId":"12345","action":"process"}' \
--cli-binary-format raw-in-base64-out \
--output json \
response.json
# View the response
cat response.jsonWhen to use RequestResponse:
- Short-running workflows (under 15 minutes total)
- When you need the result immediately
- Interactive applications requiring synchronous responses
Asynchronous Invocation (Event)
Asynchronous invocation returns immediately with the execution ID. Ideal for long-running workflows.
aws lambda invoke \
--function-name 'myDurableFunction:$LATEST' \
--invocation-type Event \
--durable-execution-name "background-task-456" \
--payload '{"orderId":"ORD-789","amount":99.99}' \
--cli-binary-format raw-in-base64-out \
--output json \
response.json
# Response contains execution ID, not the result
cat response.jsonWhen to use Event:
- Long-running workflows (hours, days, or longer)
- Background processing tasks
- Workflows with wait operations or human-in-the-loop steps
Idempotency with durable-execution-name
The --durable-execution-name parameter ensures that the same execution is never created twice:
# First invocation - creates new execution
aws lambda invoke \
--function-name 'myDurableFunction:$LATEST' \
--invocation-type RequestResponse \
--durable-execution-name "order-processing-ORD-123" \
--payload '{"orderId":"ORD-123"}' \
--cli-binary-format raw-in-base64-out \
response.json
# Second invocation with same execution name - returns existing execution result
aws lambda invoke \
--function-name 'myDurableFunction:$LATEST' \
--invocation-type RequestResponse \
--durable-execution-name "order-processing-ORD-123" \
--payload '{"orderId":"ORD-123"}' \
--cli-binary-format raw-in-base64-out \
response.jsonUsing Specific Function Versions
Durable functions require qualified ARNs (version, alias, or $LATEST):
# ✅ Invoke specific version
aws lambda invoke \
--function-name 'myDurableFunction:1' \
--invocation-type RequestResponse \
--durable-execution-name "versioned-exec-1" \
--payload '{"test":"data"}' \
--cli-binary-format raw-in-base64-out \
response.json
# ✅ Invoke using alias
aws lambda invoke \
--function-name 'myDurableFunction:production' \
--invocation-type RequestResponse \
--durable-execution-name "prod-exec-1" \
--payload '{"test":"data"}' \
--cli-binary-format raw-in-base64-out \
response.json
# ❌ Unqualified - will fail!
aws lambda invoke \
--function-name 'myDurableFunction' \
--payload '{"test":"data"}' \
response.json
# Error: Durable execution requires qualified function identifierMonitoring and Observability
CloudWatch Logs:
DurableFunction:
Type: AWS::Lambda::Function
Properties:
# ... other properties
LoggingConfig:
LogFormat: JSON
LogGroup: !Ref DurableFunctionLogGroup
DurableFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/lambda/myDurableFunction
RetentionInDays: 7CloudWatch Alarms:
DurableFunctionErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: DurableFunction-Errors
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 5
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: FunctionName
Value: !Ref DurableFunctionBest Practices
1. Always use qualified ARNs (versions or aliases) for invocation 2. Set appropriate execution timeouts based on workflow duration 3. Configure retention periods to balance cost and debugging needs 4. Use aliases for production deployments 5. Grant minimal IAM permissions - only what's needed 6. Enable structured logging (JSON format) 7. Set up CloudWatch alarms for errors and throttles 8. Use environment variables for configuration 9. Deploy to multiple environments (dev, staging, prod) 10. Version your infrastructure code alongside function code
Common issues
Function Not Durable
Issue: Function executes but doesn't checkpoint.
Solution: Verify DurableConfig is set and role has checkpoint permissions.
Invocation Fails with "Unqualified ARN"
Issue: InvalidParameterValueException: Durable execution requires qualified function identifier
Solution: Use version, alias, or $LATEST:
# ✅ Correct
aws lambda invoke --function-name myFunction:prod ...
aws lambda invoke --function-name myFunction:1 ...
# ❌ Wrong
aws lambda invoke --function-name myFunction ...Checkpoint Permission Denied
Issue: AccessDeniedException: User is not authorized to perform: lambda:CheckpointDurableExecution
Solution: Add AWSLambdaBasicDurableExecutionRolePolicy to execution role.
Error Handling and Retry Strategies
Comprehensive error handling patterns for durable functions.
TypeScript:
import { createRetryStrategy, JitterStrategy } from '@aws/durable-execution-sdk-js';
// Exponential backoff with jitter
const result = await context.step(
'api-call',
async () => callAPI(),
{
retryStrategy: createRetryStrategy({
maxAttempts: 5,
initialDelay: { seconds: 1 },
maxDelay: { seconds: 60 },
backoffRate: 2.0,
jitter: JitterStrategy.FULL
})
}
);
// Fixed delay
const result = await context.step(
'simple-retry',
async () => operation(),
{
retryStrategy: createRetryStrategy({
maxAttempts: 3,
delay: { seconds: 5 },
backoffRate: 1
})
}
);Python:
from aws_durable_execution_sdk_python.retries import RetryStrategyConfig, create_retry_strategy, JitterStrategy
retry_config = RetryStrategyConfig(
max_attempts=5,
initial_delay=Duration.from_seconds(1),
max_delay=Duration.from_seconds(60),
backoff_rate=2.0,
jitter_strategy=JitterStrategy.FULL
)
result = context.step(
func=api_call(),
config=StepConfig(retry_strategy=create_retry_strategy(retry_config))
)Custom Retry Logic
TypeScript:
const result = await context.step(
'custom-retry',
async () => riskyOperation(),
{
retryStrategy: (error, attemptCount) => {
// Don't retry client errors
if (error.statusCode >= 400 && error.statusCode < 500) {
return { shouldRetry: false };
}
// Retry server errors with exponential backoff
if (attemptCount < 5) {
return {
shouldRetry: true,
delay: { seconds: Math.pow(2, attemptCount) }
};
}
return { shouldRetry: false };
}
}
);Python:
def custom_retry(error: Exception, attempt: int) -> RetryDecision:
if hasattr(error, 'status_code') and 400 <= error.status_code < 500:
return RetryDecision(should_retry=False)
if attempt < 5:
return RetryDecision(
should_retry=True,
delay=Duration.from_seconds(2 ** attempt)
)
return RetryDecision(should_retry=False)Error Classification
Retryable vs Non-Retryable
TypeScript:
class ValidationError extends Error {
name = 'ValidationError';
}
class NetworkError extends Error {
name = 'NetworkError';
}
const result = await context.step(
'selective-retry',
async () => operation(),
{
retryStrategy: createRetryStrategy({
maxAttempts: 3,
retryableErrorTypes: ['NetworkError', 'TimeoutError'],
// ValidationError won't be retried
})
}
);Python:
retry_config = RetryStrategyConfig(
max_attempts=3,
retryable_error_types=[NetworkError, TimeoutError]
)Saga Pattern
Implement compensating transactions for distributed workflows:
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const compensations: Array<{
name: string;
fn: () => Promise<void>;
}> = [];
try {
// Step 1: Reserve inventory
const reservation = await context.step('reserve-inventory', async () =>
inventoryService.reserve(event.items)
);
compensations.push({
name: 'cancel-reservation',
fn: () => inventoryService.cancelReservation(reservation.id)
});
// Step 2: Charge payment
const payment = await context.step('charge-payment', async () =>
paymentService.charge(event.paymentMethod, event.amount)
);
compensations.push({
name: 'refund-payment',
fn: () => paymentService.refund(payment.id)
});
// Step 3: Create shipment
const shipment = await context.step('create-shipment', async () =>
shippingService.createShipment(event.address, event.items)
);
compensations.push({
name: 'cancel-shipment',
fn: () => shippingService.cancelShipment(shipment.id)
});
return { success: true, orderId: shipment.orderId };
} catch (error) {
context.logger.error('Order failed, executing compensations', error);
// Execute compensations in reverse order
for (const comp of compensations.reverse()) {
try {
await context.step(comp.name, async () => comp.fn());
} catch (compError) {
context.logger.error(`Compensation ${comp.name} failed`, compError);
// Continue with other compensations
}
}
throw error;
}
});Python:
# Note: All service methods are decorated with @durable_step
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
compensations = []
try:
# Step 1: Reserve inventory
reservation = context.step(reserve_inventory(event['items']))
compensations.append(('cancel-reservation', cancel_reservation, reservation['id']))
# Step 2: Charge payment
payment = context.step(charge_payment(event['payment_method'], event['amount']))
compensations.append(('refund-payment', refund_payment, payment['id']))
# Step 3: Create shipment
shipment = context.step(create_shipment(event['address'], event['items']))
return {'success': True, 'order_id': shipment['order_id']}
except Exception as error:
context.logger.error('Order failed, executing compensations', error)
for name, comp_step, resource_id in reversed(compensations):
try:
context.step(comp_step(resource_id))
except Exception as comp_error:
context.logger.error(f'Compensation {name} failed', comp_error)
raise errorUnrecoverable Errors
Mark errors as unrecoverable to stop execution immediately:
TypeScript:
import { UnrecoverableInvocationError } from '@aws/durable-execution-sdk-js';
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const user = await context.step('fetch-user', async () => {
const user = await fetchUser(event.userId);
if (!user) {
// Stop execution immediately - no retry
throw new UnrecoverableInvocationError('User not found');
}
return user;
});
// Continue processing...
});Python:
from aws_durable_execution_sdk_python.exceptions import ExecutionError
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
@durable_step
def fetch_user_step(step_ctx: StepContext):
user = fetch_user(event['user_id'])
if not user:
# Stop execution immediately — permanent failure, no retry
raise ExecutionError('User not found')
return user
user = context.step(fetch_user_step())
# Continue processing...The SDK provides these exception types for different failure scenarios:
| Exception | Retryable | Use case |
|---|---|---|
ExecutionError | No | Permanent business logic failures (returns FAILED status) |
InvocationError | Yes (by Lambda) | Transient infrastructure issues (Lambda retries invocation) |
CallbackError | No | Callback handling failures |
DurableExecutionsError | — | Base class for all SDK exceptions |
Error Determinism
Ensure errors are deterministic across replays:
TypeScript:
class CustomBusinessError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly details: any
) {
super(message);
this.name = 'CustomBusinessError';
}
}
const result = await context.step('validate', async () => {
if (!isValid(data)) {
// ✅ Deterministic error
throw new CustomBusinessError(
'Validation failed',
'INVALID_DATA',
{ field: 'email', reason: 'invalid format' }
);
}
return processData(data);
});Circuit Breaker Pattern
TypeScript:
class CircuitBreaker {
private failures = 0;
private lastFailureTime = 0;
private readonly threshold = 5;
private readonly timeout = 60000; // 1 minute
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.isOpen()) {
throw new Error('Circuit breaker is open');
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private isOpen(): boolean {
if (this.failures >= this.threshold) {
const elapsed = Date.now() - this.lastFailureTime;
return elapsed < this.timeout;
}
return false;
}
private onSuccess() {
this.failures = 0;
}
private onFailure() {
this.failures++;
this.lastFailureTime = Date.now();
}
}
// Use in handler
const breaker = new CircuitBreaker();
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const result = await context.step('api-call', async () => {
return await breaker.execute(() => callExternalAPI());
});
return result;
});Partial Failure Handling
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const results = await context.map(
'process-items',
event.items,
async (ctx, item, index) => {
return await ctx.step(async () => processItem(item));
},
{
completionConfig: {
toleratedFailurePercentage: 10 // Allow 10% failures
}
}
);
if (results.hasFailure()) {
// Log failures but continue
context.logger.warn('Some items failed', {
failureCount: results.failureCount,
failures: results.failed.map(f => ({
index: f.index,
error: f.error?.message
}))
});
// Store failed items for later retry
await context.step('store-failures', async () => {
const failedItems = results.failed.map(f => event.items[f.index]);
return await storeFailedItems(failedItems);
});
}
return {
totalProcessed: results.successCount,
failed: results.failureCount
};
});Best Practices
1. Use appropriate retry strategies - exponential backoff for most cases 2. Classify errors correctly - distinguish retryable from non-retryable 3. Implement compensating transactions for distributed workflows 4. Make errors deterministic - same input produces same error 5. Use unrecoverable errors to stop execution early when appropriate 6. Log errors with context using context.logger 7. Handle partial failures gracefully in batch operations 8. Implement circuit breakers for external service calls 9. Test error scenarios thoroughly with test runners 10. Monitor error rates and adjust retry strategies accordingly
Getting Started with AWS Lambda durable functions
Quick start guide for building your first durable function.
Language selection
Default: TypeScript
Override syntax:
- "use Python" → Generate Python code
- "use JavaScript" → Generate JavaScript code
When not specified, ALWAYS use TypeScript
Error Scenarios: Unsupported Language
- List detected language
- State: "Durable Execution SDK is not yet available for [framework]"
- Suggest supported languages as alternatives
Basic Handler
TypeScript:
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js';
export const handler = withDurableExecution(async (event, context: DurableContext) => {
// Execute a step with automatic retry
const userData = await context.step('fetch-user', async () =>
fetchUserFromDB(event.userId)
);
// Wait without compute charges
await context.wait({ seconds: 5 });
// Process in another step
const result = await context.step('process', async () =>
processUser(userData)
);
return { success: true, data: result };
});Python:
from aws_durable_execution_sdk_python import durable_execution, DurableContext, durable_step, StepContext
from aws_durable_execution_sdk_python.config import Duration
@durable_step
def fetch_user(step_ctx: StepContext, user_id: str):
return fetch_user_from_db(user_id)
@durable_step
def process_user_data(step_ctx: StepContext, user_data: dict):
return process_user(user_data)
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
user_data = context.step(fetch_user(event['userId']))
context.wait(duration=Duration.from_seconds(5))
result = context.step(process_user_data(user_data))
return {'success': True, 'data': result}Common Patterns
Multi-Step Workflow
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const validated = await context.step('validate', async () =>
validateInput(event)
);
const processed = await context.step('process', async () =>
processData(validated)
);
await context.wait('cooldown', { seconds: 30 });
await context.step('notify', async () =>
sendNotification(processed)
);
return { success: true };
});GenAI Agent (Agentic Loop)
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const messages = [{ role: 'user', content: event.prompt }];
while (true) {
const { response, tool } = await context.step('invoke-model', async () =>
invokeAIModel(messages)
);
if (tool == null) return response;
const toolResult = await context.step(`tool-${tool.name}`, async () =>
executeTool(tool, response)
);
messages.push({ role: 'assistant', content: toolResult });
}
});Python:
# Note: invoke_ai_model and execute_tool are decorated with @durable_step
@durable_execution
def handler(event: dict, context: DurableContext) -> str:
messages = [{"role": "user", "content": event["prompt"]}]
while True:
result = context.step(invoke_ai_model(messages))
if result.get("tool") is None:
return result["response"]
tool = result["tool"]
tool_result = context.step(execute_tool(tool, result["response"]))
messages.append({"role": "assistant", "content": tool_result})Human-in-the-Loop Approval
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const plan = await context.step('generate-plan', async () =>
generatePlan(event)
);
const answer = await context.waitForCallback(
'wait-for-approval',
async (callbackId) => sendApprovalEmail(event.approverEmail, plan, callbackId),
{ timeout: { hours: 24 } }
);
if (answer === 'APPROVED') {
await context.step('execute', async () => performAction(plan));
return { status: 'completed' };
}
return { status: 'rejected' };
});Python:
from aws_durable_execution_sdk_python.config import WaitForCallbackConfig
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
# Note: generate_plan and perform_action are decorated with @durable_step
plan = context.step(generate_plan(event))
# Wait for external approval
def submit_approval(callback_id: str, ctx):
send_approval_email(event['approver_email'], plan, callback_id)
answer = context.wait_for_callback(
submitter=submit_approval,
name='wait-for-approval',
config=WaitForCallbackConfig(timeout=Duration.from_hours(24))
)
if answer == 'APPROVED':
context.step(perform_action(plan))
return {'status': 'completed'}
return {'status': 'rejected'}Saga Pattern (Compensating Transactions)
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const compensations: Array<{ name: string; fn: () => Promise<void> }> = [];
try {
await context.step('book-flight', async () => flightClient.book(event));
compensations.push({
name: 'cancel-flight',
fn: () => flightClient.cancel(event)
});
await context.step('book-hotel', async () => hotelClient.book(event));
compensations.push({
name: 'cancel-hotel',
fn: () => hotelClient.cancel(event)
});
return { success: true };
} catch (error) {
for (const comp of compensations.reverse()) {
await context.step(comp.name, async () => comp.fn());
}
throw error;
}
});Project Structure
TypeScript
my-durable-function/
├── src/
│ ├── handler.ts # Main handler
│ ├── steps/ # Step functions
│ │ ├── validate.ts
│ │ └── process.ts
│ └── utils/ # Utilities
│ └── retry-strategies.ts
├── tests/
│ └── handler.test.ts # Tests with LocalDurableTestRunner
├── infrastructure/
│ └── template.yaml # SAM/CloudFormation
├── eslint.config.js # ESLint configuration
├── jest.config.js # Jest configuration
├── tsconfig.json # TypeScript configuration
└── package.jsonPython
my-durable-function/
├── src/
│ ├── handler.py # Main handler
│ ├── steps/ # Step functions
│ │ ├── __init__.py
│ │ ├── validate.py
│ │ └── process.py
│ └── utils/
│ └── retry_strategies.py
├── tests/
│ └── test_handler.py # Tests with DurableFunctionTestRunner
├── infrastructure/
│ └── template.yaml # SAM/CloudFormation
└── pyproject.toml # Project configurationESLint Plugin Setup
Install the ESLint plugin to catch common durable function mistakes at development time:
npm install --save-dev @aws/durable-execution-sdk-js-eslint-pluginOption A: Flat Config (eslint.config.js)
import durableExecutionPlugin from '@aws/durable-execution-sdk-js-eslint-plugin';
export default [
{
plugins: {
'@aws/durable-execution-sdk-js': durableExecutionPlugin,
},
rules: {
'@aws/durable-execution-sdk-js/no-nested-durable-operations': 'error',
},
},
];Option B: Recommended Config
import durableExecutionPlugin from '@aws/durable-execution-sdk-js-eslint-plugin';
export default [
durableExecutionPlugin.configs.recommended,
// Your other configs...
];Option C: Legacy .eslintrc.json
{
"plugins": ["@aws/durable-execution-sdk-js-eslint-plugin"],
"extends": ["plugin:@aws/durable-execution-sdk-js-eslint-plugin/recommended"],
"rules": {
"@aws/durable-execution-sdk-js-eslint-plugin/no-nested-durable-operations": "error"
}
}What the plugin catches:
- Nested durable operations inside step functions
- Incorrect usage of durable context outside handler
- Common replay model violations
Jest Configuration
jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
};Key Configuration:
preset: 'ts-jest'- Essential for TypeScript supporttransform- Maps .ts files to ts-jest transformertestMatch- Specifies test file patterns
Python Project Setup
Add aws-durable-execution-sdk-python-testing to your dev/test dependencies in pyproject.toml.
Development Workflow
TypeScript
1. Write handler with durable operations 2. Test locally with LocalDurableTestRunner 3. Validate replay rules (no non-deterministic code outside steps) 4. Deploy with qualified ARN (version or alias) 5. Monitor execution state and logs
Python
1. Write handler with @durable_execution decorator 2. Test locally with DurableFunctionTestRunner and pytest 3. Validate replay rules (no non-deterministic code outside steps) 4. Deploy with qualified ARN (version or alias) 5. Monitor execution state and logs
Key Concepts
- Steps: Atomic operations with automatic retry and checkpointing
- Waits: Suspend execution without compute charges (up to 1 year)
- Child Contexts: Group multiple durable operations
- Callbacks: Wait for external systems to respond
- Map/Parallel: Process arrays or run operations concurrently
Setup Checklist
When starting a new durable function project:
TypeScript
- [ ] Install dependencies (
@aws/durable-execution-sdk-js, testing & eslint packages) - [ ] Create
jest.config.jswith ts-jest preset - [ ] Configure
tsconfig.jsonwith proper module resolution - [ ] Set up ESLint with durable execution plugin
- [ ] Create handler with
withDurableExecutionwrapper - [ ] Write tests using
LocalDurableTestRunner - [ ] Use
skipTime: truefor fast test execution - [ ] Verify TypeScript compilation:
npx tsc --noEmit - [ ] Run tests to confirm setup:
npm test - [ ] Review replay model rules (no non-deterministic code outside steps)
Python
- [ ] Install
aws-durable-execution-sdk-python - [ ] Install
aws-durable-execution-sdk-python-testingandpytestfor testing - [ ] Create handler with
@durable_executiondecorator - [ ] Define step functions with
@durable_stepdecorator - [ ] Write tests using
DurableFunctionTestRunnerclass - [ ] Run tests:
pytest - [ ] Review replay model rules (no non-deterministic code outside steps)
Error Scenarios
Unsupported Language
- List detected language
- State: "Durable Execution SDK is not yet available for [language]"
- List supported languages as alternatives
Next Steps
- Review replay-model-rules.md to avoid common pitfalls
- Explore step-operations.md for retry strategies
- Learn wait-operations.md for external integrations
- Check testing-patterns.md for comprehensive testing
Replay Model Rules - CRITICAL
The replay model is the foundation of durable functions. Violations cause subtle, hard-to-debug issues. Read this carefully.
How Replay Works
Durable functions use a "checkpoint and replay" execution model:
1. Code runs from the beginning on every invocation 2. Steps that already completed return their checkpointed results WITHOUT re-executing 3. Code OUTSIDE steps executes again on every replay 4. New steps execute when reached
Example:
// First execution: Runs lines 1-5
// After wait: Runs lines 1-5 again (line 2 returns cached result)
const data = await context.step('fetch', async () => fetchAPI()); // Line 2: Executes once, cached
await context.wait({ seconds: 60 }); // Line 3: Waits
const result = await context.step('process', async () => process(data)); // Line 5: Executes after waitRule 1: Deterministic Code Outside Steps
ALL code outside steps MUST produce the same result on every replay.
❌ WRONG - Non-Deterministic Outside Steps
TypeScript:
// These values change on each replay!
const id = uuid.v4(); // Different UUID each time
const timestamp = Date.now(); // Different timestamp each time
const random = Math.random(); // Different random number
const now = new Date(); // Different date each time
await context.step('save', async () => saveData({ id, timestamp }));Python:
# These values change on each replay!
id = str(uuid.uuid4()) # Different UUID each time
timestamp = time.time() # Different timestamp each time
random_val = random.random() # Different random number
now = datetime.now() # Different datetime each time
context.step(lambda _: save_data({"id": id}), name='save')✅ CORRECT - Non-Deterministic Inside Steps
TypeScript:
const id = await context.step('generate-id', async () => uuid.v4());
const timestamp = await context.step('get-time', async () => Date.now());
const random = await context.step('random', async () => Math.random());
const now = await context.step('get-date', async () => new Date());
await context.step('save', async () => saveData({ id, timestamp }));Python:
id = context.step(lambda _: str(uuid.uuid4()), name='generate-id')
timestamp = context.step(lambda _: time.time(), name='get-time')
random_val = context.step(lambda _: random.random(), name='random')
now = context.step(lambda _: datetime.now(), name='get-date')
context.step(lambda _: save_data({"id": id}), name='save')Must Be In Steps
Date.now(),new Date(),time.time(),datetime.now()Math.random(),random.random()- UUID generation (
uuid.v4(),uuid.uuid4()) - API calls, HTTP requests
- Database queries
- File system operations
- Environment variable reads (if they can change)
- Any external system interaction
Rule 2: No Nested Durable Operations
You CANNOT call durable operations inside a step function.
❌ WRONG - Nested Operations
TypeScript:
await context.step('process', async () => {
await context.wait({ seconds: 1 }); // ERROR!
await context.step(async () => ...); // ERROR!
await context.invoke('other-fn', ...); // ERROR!
return result;
});Python:
@durable_step
def process(step_ctx: StepContext):
context.wait(duration=Duration.from_seconds(1)) # ERROR!
context.step(lambda _: ..., name='nested') # ERROR!
return result
context.step(process())✅ CORRECT - Use Child Context
TypeScript:
await context.runInChildContext('process', async (childCtx) => {
await childCtx.wait({ seconds: 1 });
const step1 = await childCtx.step('validate', async () => validate());
const step2 = await childCtx.step('process', async () => process(step1));
return step2;
});Python:
# Note: validate and process are decorated with @durable_step
def process_child(child_ctx: DurableContext):
child_ctx.wait(duration=Duration.from_seconds(1))
step1 = child_ctx.step(validate())
step2 = child_ctx.step(process(step1))
return step2
context.run_in_child_context(func=process_child, name='process')Rule 3: Closure Mutations Are Lost
Variables mutated inside steps are NOT preserved across replays.
❌ WRONG - Lost Mutations
TypeScript:
let counter = 0;
await context.step('increment', async () => {
counter++; // This mutation is lost!
});
console.log(counter); // Always 0 on replay!Python:
counter = 0
@durable_step
def increment(step_ctx: StepContext):
nonlocal counter
counter += 1 # This mutation is lost!
context.step(increment())
print(counter) # Always 0 on replay!✅ CORRECT - Return Values
TypeScript:
let counter = 0;
counter = await context.step('increment', async () => counter + 1);
console.log(counter); // Correct valuePython:
counter = 0
counter = context.step(lambda _: counter + 1, name='increment')
print(counter) # Correct valueRule 4: Side Effects Outside Steps Repeat
Side effects outside steps happen on EVERY replay.
❌ WRONG - Repeated Side Effects
TypeScript:
console.log('Starting process'); // Logs multiple times!
await sendEmail(user.email); // Sends multiple emails!
await updateDatabase(data); // Updates multiple times!
await context.step('process', async () => process());Python:
print('Starting process') # Prints multiple times!
send_email(user.email) # Sends multiple emails!
update_database(data) # Updates multiple times!
context.step(lambda _: process(), name='process')✅ CORRECT - Side Effects In Steps
TypeScript:
context.logger.info('Starting process'); // Deduplicated automatically
await context.step('send-email', async () => sendEmail(user.email));
await context.step('update-db', async () => updateDatabase(data));
await context.step('process', async () => process());Python:
# Note: Functions are decorated with @durable_step
context.logger.info('Starting process') # Deduplicated automatically
context.step(send_email(user.email))
context.step(update_database(data))
context.step(process())Exception: context.logger
context.logger is replay-aware and safe to use anywhere. It automatically deduplicates logs across replays.
Common Pitfalls
Pitfall 1: Reading Environment Variables
// ❌ WRONG if env vars can change
const apiKey = process.env.API_KEY;
await context.step('call-api', async () => callAPI(apiKey));
// ✅ CORRECT
const apiKey = await context.step('get-key', async () => process.env.API_KEY);
await context.step('call-api', async () => callAPI(apiKey));Pitfall 2: Array/Object Mutations
// ❌ WRONG
const items = [];
await context.step('add-item', async () => {
items.push(newItem); // Lost on replay
});
// ✅ CORRECT
let items = [];
items = await context.step('add-item', async () => [...items, newItem]);Pitfall 3: Conditional Logic with Non-Deterministic Values
// ❌ WRONG
if (Math.random() > 0.5) { // Different on each replay!
await context.step('path-a', async () => ...);
} else {
await context.step('path-b', async () => ...);
}
// ✅ CORRECT
const shouldTakePathA = await context.step('decide', async () => Math.random() > 0.5);
if (shouldTakePathA) {
await context.step('path-a', async () => ...);
} else {
await context.step('path-b', async () => ...);
}Debugging Replay Issues
If you see inconsistent behavior:
1. Check for non-deterministic code outside steps 2. Verify no nested durable operations 3. Look for closure mutations 4. Search for side effects outside steps 5. Use `context.logger` to trace execution flow
Testing Replay Behavior
Always test with multiple invocations to simulate replay:
const runner = new LocalDurableTestRunner({ handlerFunction: handler });
const execution = await runner.run({ payload: { test: true } });
// Verify operations executed correctly
const step1 = runner.getOperation('step-name');
expect(step1.getStatus()).toBe(OperationStatus.SUCCEEDED);Step Operations
Steps are atomic operations with automatic retry and state persistence.
Basic Step Patterns
Python: Two Ways to Define Steps
Recommended: `@durable_step` Decorator
from aws_durable_execution_sdk_python import durable_step, StepContext
@durable_step
def fetch_user(step_ctx: StepContext, user_id: str):
"""Fetch user from database - reusable step function."""
return fetch_user_from_api(user_id)
# Call it - name is automatically inferred from function name
result = context.step(fetch_user(user_id))Alternative: Inline Lambda
# For simple one-off operations
result = context.step(
func=lambda step_ctx: fetch_user_from_api(user_id),
name='fetch-user'
)Use `@durable_step` for:
- Reusable step functions
- Complex logic
- Better readability and testing
Use lambda for:
- Simple inline operations
- One-off transformations
TypeScript: Named Steps
TypeScript:
const result = await context.step('fetch-user', async () => {
return await fetchUserFromAPI(userId);
});Best Practice: Always name steps for easier debugging and testing.
Retry Configuration
Exponential Backoff
TypeScript:
import { createRetryStrategy, JitterStrategy } from '@aws/durable-execution-sdk-js';
const result = await context.step(
'api-call',
async () => callExternalAPI(),
{
retryStrategy: createRetryStrategy({
maxAttempts: 5,
initialDelay: { seconds: 1 },
maxDelay: { seconds: 60 },
backoffRate: 2.0,
jitter: JitterStrategy.FULL
})
}
);Python:
# Note: api_call is decorated with @durable_step
from aws_durable_execution_sdk_python.config import StepConfig, Duration
from aws_durable_execution_sdk_python.retries import RetryStrategyConfig, create_retry_strategy, JitterStrategy
retry_config = RetryStrategyConfig(
max_attempts=5,
initial_delay=Duration.from_seconds(5),
max_delay=Duration.from_seconds(60),
backoff_rate=2.0,
jitter_strategy=JitterStrategy.FULL
)
result = context.step(
func=api_call(),
config=StepConfig(retry_strategy=create_retry_strategy(retry_config))
)Custom Retry Strategy
TypeScript:
const result = await context.step(
'custom-retry',
async () => riskyOperation(),
{
retryStrategy: (error, attemptCount) => {
// Don't retry validation errors
if (error.name === 'ValidationError') {
return { shouldRetry: false };
}
// Retry up to 3 times with exponential backoff
if (attemptCount < 3) {
return {
shouldRetry: true,
delay: { seconds: Math.pow(2, attemptCount) }
};
}
return { shouldRetry: false };
}
}
);Python:
from aws_durable_execution_sdk_python.retries import RetryDecision
def custom_retry(error: Exception, attempt: int) -> RetryDecision:
if isinstance(error, ValidationError):
return RetryDecision(should_retry=False)
if attempt < 3:
return RetryDecision(
should_retry=True,
delay=Duration.from_seconds(2 ** attempt)
)
return RetryDecision(should_retry=False)
result = context.step(
risky_operation(),
config=StepConfig(retry_strategy=custom_retry)
)Retryable Error Types
TypeScript:
const result = await context.step(
'selective-retry',
async () => operation(),
{
retryStrategy: createRetryStrategy({
maxAttempts: 3,
retryableErrorTypes: ['NetworkError', 'TimeoutError']
})
}
);Python:
retry_config = RetryStrategyConfig(
max_attempts=3,
retryable_error_types=[NetworkError, TimeoutError]
)Step Semantics
AT_LEAST_ONCE (Default)
Step executes at least once, may execute multiple times on failure/retry.
TypeScript:
const result = await context.step(
'idempotent-operation',
async () => idempotentAPI(),
{ semantics: 'AT_LEAST_ONCE' }
);AT_MOST_ONCE
Step executes at most once, never retries. Use for non-idempotent operations.
TypeScript:
const result = await context.step(
'charge-payment',
async () => chargeCard(amount),
{ semantics: 'AT_MOST_ONCE' }
);Python:
from aws_durable_execution_sdk_python.config import StepSemantics
result = context.step(
charge_card(amount),
config=StepConfig(step_semantics=StepSemantics.AT_MOST_ONCE_PER_RETRY)
)Custom Serialization
For complex types, provide custom serialization:
TypeScript:
import { createClassSerdesWithDates } from '@aws/durable-execution-sdk-js';
class User {
constructor(
public id: string,
public name: string,
public createdAt: Date
) {}
}
const userSerdes = createClassSerdesWithDates(User, ['createdAt']);
const user = await context.step(
'fetch-user',
async () => new User('123', 'Alice', new Date()),
{ serdes: userSerdes }
);Python:
from dataclasses import dataclass
from datetime import datetime
@dataclass
class User:
id: str
name: str
created_at: datetime
# Python SDK handles dataclass serialization automatically
user = context.step(
lambda _: User('123', 'Alice', datetime.now()),
name='fetch-user'
)When to Use Steps vs Child Contexts
Use Steps For:
- Single atomic operations
- API calls
- Database queries
- Data transformations
- Operations that should retry as a unit
Use Child Contexts For:
- Grouping multiple durable operations
- Complex workflows with steps, waits, and invokes
- Isolating state tracking
- Organizing related operations
Example:
// ❌ WRONG: Cannot nest durable operations in step
await context.step('process', async () => {
await context.wait({ seconds: 1 }); // ERROR!
});
// ✅ CORRECT: Use child context
await context.runInChildContext('process', async (childCtx) => {
const data = await childCtx.step('fetch', async () => fetch());
await childCtx.wait({ seconds: 1 });
return await childCtx.step('save', async () => save(data));
});Error Handling
Steps throw errors after all retry attempts are exhausted:
TypeScript:
try {
const result = await context.step('risky', async () => riskyOperation());
} catch (error) {
if (error instanceof StepError) {
context.logger.error('Step failed', error.cause);
// Handle or rethrow
}
}Python:
try:
# Note: risky_operation is decorated with @durable_step
result = context.step(risky_operation())
except Exception as error:
context.logger.error('Step failed: %s', str(error))
# Handle or rethrowFor SDK-specific exceptions, use the base class or specific types:
from aws_durable_execution_sdk_python import DurableExecutionsError
try:
result = context.step(risky_operation())
except DurableExecutionsError as error:
context.logger.error('SDK error: %s', str(error))
except Exception as error:
context.logger.error('Application error: %s', str(error))Best Practices
1. Always name steps for debugging and testing 2. Keep steps atomic - one logical operation per step 3. Make steps idempotent when possible 4. Use appropriate retry strategies based on operation type 5. Handle errors explicitly - don't let them propagate unexpectedly 6. Use custom serialization for complex types 7. Choose correct semantics (AT_LEAST_ONCE vs AT_MOST_ONCE)
Testing Patterns
Test durable functions locally and in the cloud with comprehensive test runners.
Critical Testing Patterns
ALWAYS follow these patterns to avoid flaky tests:
DO:
- ✅ Name all operations for test reliability
- ✅ TypeScript: Use
runner.getOperation("name")to find operations by name - ✅ TypeScript: Use
WaitingOperationStatus.STARTEDwhen waiting for callback operations - ✅ TypeScript: JSON.stringify callback parameters:
sendCallbackSuccess(JSON.stringify(data)) - ✅ TypeScript: Use
skipTime: truein setupTestEnvironment for fast tests - ✅ TypeScript: Wrap event data in
payloadobject:runner.run({ payload: { ... } }) - ✅ TypeScript: Cast
getResult()to appropriate type:execution.getResult() as ResultType - ✅ Python: Use
result.get_step("name")to find step operations by name - ✅ Python: Use
result.operationsto iterate and filter operations by type - ✅ Python: Instantiate
DurableFunctionTestRunner(handler=my_handler)directly - ✅ Python: Use
runner.run(input={...}, timeout=10)— noteinput=notpayload - ✅ Python: The value of result.result is serialized. Deserialize using the appropriate SerDes or default json deserializer.
DON'T:
- ❌ Use
getOperationByIndex()unless absolutely necessary - ❌ Assume operation indices are stable (parallel creates nested operations)
- ❌ TypeScript: Send objects to sendCallbackSuccess — stringify first
- ❌ TypeScript: Forget that callback results are JSON strings — parse them
- ❌ TypeScript: Test callbacks without proper synchronization (leads to race conditions)
- ❌ Python: Confuse
DurableFunctionTestRunner(local) withDurableFunctionCloudTestRunner(cloud) - ❌ Python: Forget the
with runner:context manager — it manages execution lifecycle
Local Testing Setup
TypeScript:
import {
LocalDurableTestRunner,
OperationType,
OperationStatus
} from '@aws/durable-execution-sdk-js-testing';
describe('My Durable Function', () => {
beforeAll(() =>
LocalDurableTestRunner.setupTestEnvironment({ skipTime: true })
);
afterAll(() =>
LocalDurableTestRunner.teardownTestEnvironment()
);
it('should execute workflow', async () => {
const runner = new LocalDurableTestRunner({
handlerFunction: handler
});
const execution = await runner.run({
payload: { userId: '123' }
});
expect(execution.getStatus()).toBe('SUCCEEDED');
expect(execution.getResult()).toEqual({ success: true });
});
});Python:
The Python testing SDK provides DurableFunctionTestRunner for local testing and DurableFunctionCloudTestRunner for cloud testing.
Install the testing SDK:
pip install aws-durable-execution-sdk-python-testing pytestExample test:
from aws_durable_execution_sdk_python_testing import DurableFunctionTestRunner
from aws_durable_execution_sdk_python.execution import InvocationStatus
from src.my_function import handler
def test_workflow():
"""Test durable function locally."""
runner = DurableFunctionTestRunner(handler=handler)
with runner:
result = runner.run(input={'user_id': '123'}, timeout=10)
assert result.status is InvocationStatus.SUCCEEDEDGetting Operations
CRITICAL: Always get operations by NAME, not by index.
TypeScript:
it('should execute steps in order', async () => {
const runner = new LocalDurableTestRunner({ handlerFunction: handler });
await runner.run({ payload: { test: true } });
// ✅ CORRECT: Get by name
const fetchStep = runner.getOperation('fetch-user');
expect(fetchStep.getType()).toBe(OperationType.STEP);
expect(fetchStep.getStatus()).toBe(OperationStatus.SUCCEEDED);
const processStep = runner.getOperation('process-data');
expect(processStep.getStatus()).toBe(OperationStatus.SUCCEEDED);
// ❌ WRONG: Get by index (brittle, breaks easily)
// const step1 = runner.getOperationByIndex(0);
});Python:
from aws_durable_execution_sdk_python.lambda_service import OperationType
def test_steps_execute():
"""Test step execution."""
runner = DurableFunctionTestRunner(handler=handler)
with runner:
result = runner.run(input={'test': True}, timeout=10)
# ✅ CORRECT: Get step by name
fetch_step = result.get_step('fetch-user')
assert fetch_step is not None
# ✅ Also valid: filter result.operations by type
step_names = {op.name for op in result.operations if op.operation_type == OperationType.STEP}
assert step_names >= {'fetch-user', 'process-data'}
assert 'process-data' in step_namesTesting Replay Behavior
TypeScript:
it('should handle replay correctly', async () => {
const runner = new LocalDurableTestRunner({ handlerFunction: handler });
// First execution
const execution1 = await runner.run({ payload: { value: 42 } });
expect(execution1.getStatus()).toBe('SUCCEEDED');
// Simulate replay
const execution2 = await runner.run({ payload: { value: 42 } });
expect(execution2.getStatus()).toBe('SUCCEEDED');
// Results should be identical
expect(execution1.getResult()).toEqual(execution2.getResult());
});Testing with Fake Clock
TypeScript:
it('should wait for specified duration', async () => {
const runner = new LocalDurableTestRunner({
handlerFunction: handler
});
const executionPromise = runner.run({ payload: {} });
// Advance time by 60 seconds
await runner.skipTime({ seconds: 60 });
const execution = await executionPromise;
expect(execution.getStatus()).toBe('SUCCEEDED');
const waitOp = runner.getOperation('delay');
expect(waitOp.getType()).toBe(OperationType.WAIT);
expect(waitOp.getWaitDetails()?.waitSeconds).toBe(60);
});Test Runner API Patterns
CRITICAL: Always wrap event data in payload and cast results appropriately.
TypeScript:
it('should use correct test runner API', async () => {
const runner = new LocalDurableTestRunner({
handlerFunction: handler,
});
// ✅ CORRECT: Wrap event in payload
const execution = await runner.run({
payload: { name: 'Alice', userId: '123' }
});
// ✅ CORRECT: Type cast result
const result = execution.getResult() as {
greeting: string;
message: string;
};
expect(result.greeting).toBe('Hello, Alice!');
// ✅ CORRECT: Get operations by name
const greetingStep = runner.getOperation('generate-greeting');
expect(greetingStep.getStepDetails()?.result).toBe('Hello, Alice!');
});
// ❌ WRONG: Missing payload wrapper and type casting
it('incorrect api usage', async () => {
const runner = new LocalDurableTestRunner({ handlerFunction: handler });
// ❌ Missing payload wrapper
const execution = await runner.run({ name: 'Alice' });
// ❌ No type casting - result is 'unknown'
const result = execution.getResult();
// expect(result.greeting).toBe('...'); // Type error!
});Testing Callbacks
CRITICAL: Use waitForData() with WaitingOperationStatus.STARTED to avoid flaky tests caused by promise races.
TypeScript:
import { WaitingOperationStatus } from '@aws/durable-execution-sdk-js-testing';
it('should handle callback success', async () => {
const runner = new LocalDurableTestRunner({ handlerFunction: handler });
// Start execution (will pause at callback)
const executionPromise = runner.run({
payload: { approver: 'alice@example.com' }
});
// ✅ CRITICAL: Get operation by NAME
const callbackOp = runner.getOperation('wait-for-approval');
// ✅ CRITICAL: Wait for operation to reach STARTED status
await callbackOp.waitForData(WaitingOperationStatus.STARTED);
// ✅ CRITICAL: Must JSON.stringify callback data!
await callbackOp.sendCallbackSuccess(
JSON.stringify({ approved: true, comments: 'Looks good' })
);
const execution = await executionPromise;
expect(execution.getStatus()).toBe('SUCCEEDED');
// ✅ CRITICAL: Parse JSON string result
const result: any = execution.getResult();
const approval = typeof result.approval === 'string'
? JSON.parse(result.approval)
: result.approval;
expect(approval.approved).toBe(true);
expect(approval.comments).toBe('Looks good');
});
it('should handle callback failure', async () => {
const runner = new LocalDurableTestRunner({ handlerFunction: handler });
const executionPromise = runner.run({ payload: {} });
await new Promise(resolve => setTimeout(resolve, 100));
const callbackOp = runner.getOperation('wait-for-approval');
// Send callback failure
await callbackOp.sendCallbackFailure(
'ApprovalDenied',
'Request was rejected'
);
const execution = await executionPromise;
expect(execution.getStatus()).toBe('FAILED');
});Python:
Testing callbacks in Python follows the same marker pattern. The callback operation appears in result.operations with operation_type == OperationType.CALLBACK:
def test_callback_creation():
"""Test that callback is created correctly."""
runner = DurableFunctionTestRunner(handler=handler)
with runner:
result = runner.run(input={'approver': '[email]'}, timeout=10)
# Find callback operations in the result
callback_ops = [
op for op in result.operations
if op.operation_type == OperationType.CALLBACK
]
assert len(callback_ops) == 1
assert callback_ops[0].name == 'wait-for-approval'
assert callback_ops[0].callback_id is not NoneTesting Callback Heartbeats
TypeScript:
it('should handle callback heartbeats', async () => {
const runner = new LocalDurableTestRunner({ handlerFunction: handler });
const executionPromise = runner.run({ payload: {} });
await new Promise(resolve => setTimeout(resolve, 100));
const callbackOp = runner.getOperation('long-running-process');
// Send heartbeats
await callbackOp.sendCallbackHeartbeat();
await runner.skipTime({ minutes: 2 });
await callbackOp.sendCallbackHeartbeat();
await runner.skipTime({ minutes: 2 });
// Complete callback
await callbackOp.sendCallbackSuccess(JSON.stringify({ status: 'completed' }));
const execution = await executionPromise;
expect(execution.getStatus()).toBe('SUCCEEDED');
});Testing Error Scenarios
TypeScript:
it('should retry on failure', async () => {
let attemptCount = 0;
const testHandler = withDurableExecution(async (event, context: DurableContext) => {
return await context.step('flaky-operation', async () => {
attemptCount++;
if (attemptCount < 3) {
throw new Error('Temporary failure');
}
return { success: true };
});
});
const runner = new LocalDurableTestRunner({ handlerFunction: testHandler });
const execution = await runner.run({ payload: {} });
expect(execution.getStatus()).toBe('SUCCEEDED');
expect(attemptCount).toBe(3);
const step = runner.getOperation('flaky-operation');
expect(step.getStatus()).toBe(OperationStatus.SUCCEEDED);
});
it('should fail after max retries', async () => {
const testHandler = withDurableExecution(async (event, context: DurableContext) => {
return await context.step(
'always-fails',
async () => {
throw new Error('Permanent failure');
},
{
retryStrategy: createRetryStrategy({ maxAttempts: 3 })
}
);
});
const runner = new LocalDurableTestRunner({ handlerFunction: testHandler });
const execution = await runner.run({ payload: {} });
expect(execution.getStatus()).toBe('FAILED');
expect(execution.getError()?.errorMessage).toContain('Permanent failure');
});Testing Concurrent Operations
TypeScript:
it('should process items concurrently', async () => {
const runner = new LocalDurableTestRunner({ handlerFunction: handler });
const execution = await runner.run({
payload: { items: [1, 2, 3, 4, 5] }
});
expect(execution.getStatus()).toBe('SUCCEEDED');
const mapOp = runner.getOperation('process-items');
expect(mapOp.getType()).toBe(OperationType.MAP);
// Check individual item operations
const item0 = runner.getOperation('process-0');
expect(item0.getStatus()).toBe(OperationStatus.SUCCEEDED);
});Cloud Testing
For integration tests against real Lambda:
TypeScript:
import { CloudDurableTestRunner } from '@aws/durable-execution-sdk-js-testing';
describe('Integration Tests', () => {
it('should execute in real Lambda', async () => {
const runner = new CloudDurableTestRunner({
functionName: 'my-durable-function:1', // Qualified ARN required
client: new LambdaClient({ region: 'us-east-1' })
});
const execution = await runner.run({
payload: { userId: '123' },
config: { pollInterval: 1000 }
});
expect(execution.getStatus()).toBe('SUCCEEDED');
const step = runner.getOperation('fetch-user');
expect(step.getStatus()).toBe(OperationStatus.SUCCEEDED);
});
});Python:
Cloud mode uses DurableFunctionCloudTestRunner with the same API:
# Set environment variables for cloud mode
export AWS_REGION=us-west-2
export QUALIFIED_FUNCTION_NAME="my-durable-function:$LATEST"
export LAMBDA_FUNCTION_TEST_NAME="my_function"
# Run in cloud mode
pytest --runner-mode=cloud -k test_workflowThe same test works in both modes:
def test_workflow_cloud():
"""Test against deployed Lambda function."""
runner = DurableFunctionCloudTestRunner(
function_name='my-function:$LATEST',
region='us-west-2'
)
with runner:
result = runner.run(input={'user_id': '123'}, timeout=60)
assert result.status is InvocationStatus.SUCCEEDEDTest Assertions
TypeScript:
it('should validate operation details', async () => {
const runner = new LocalDurableTestRunner({ handlerFunction: handler });
await runner.run({ payload: {} });
const step = runner.getOperation('process-data');
// Check operation type
expect(step.getType()).toBe(OperationType.STEP);
// Check status
expect(step.getStatus()).toBe(OperationStatus.SUCCEEDED);
// Check timing
expect(step.getStartTimestamp()).toBeDefined();
expect(step.getEndTimestamp()).toBeDefined();
// Check result
const stepDetails = step.getStepDetails();
expect(stepDetails?.result).toEqual({ processed: true });
});Best Practices
1. Always name operations for reliable test assertions 2. Get operations by name, never by index 3. Test replay behavior with multiple invocations 4. Use fake clock for time-dependent tests 5. Test error scenarios including retries and failures 6. Test callbacks with success, failure, and timeout cases 7. Validate operation details (type, status, timing, results) 8. Use cloud tests for integration testing 9. Mock external dependencies in unit tests 10. Test concurrent operations individually and as a group
Common Pitfalls
❌ Getting Operations by Index
// Brittle - breaks when operations change
const step = runner.getOperationByIndex(0);✅ Getting Operations by Name
// Robust - works even if operation order changes
const step = runner.getOperation('fetch-user');❌ Not Waiting for Callbacks
// Race condition - callback might not exist yet
const callbackOp = runner.getOperation('wait-approval');
await callbackOp.sendCallbackSuccess('{}');✅ Waiting for Callbacks
// Use waitForData with proper status
import { WaitingOperationStatus } from '@aws/durable-execution-sdk-js-testing';
const callbackOp = runner.getOperation('wait-approval');
await callbackOp.waitForData(WaitingOperationStatus.STARTED);
await callbackOp.sendCallbackSuccess(JSON.stringify({}));Common Testing Errors
TypeScript
| Error | Cause | Solution |
|---|---|---|
'result' is of type 'unknown' | Missing type casting in tests | Cast result: as any or specific type |
'payload' does not exist in type | Wrong test runner API | Wrap event in payload: {} object |
Cannot find operation at index | Using index for unstable operations | Use getOperation("name") instead |
| Flaky callback tests | Race condition with callback creation | Use waitForData(WaitingOperationStatus.STARTED) |
Unexpected token in callback result | Forgot to JSON.stringify | Always stringify: JSON.stringify(data) |
| Callback result parsing error | Result is JSON string | Parse result: JSON.parse(result.value) |
| Operation not found by name | Missing operation name | Always name operations in handler |
Jest Configuration
jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
};Key points:
preset: 'ts-jest'is essential for TypeScript supporttransformmaps .ts files to ts-jest transformertestMatchspecifies test file patterns- Use
skipTime: truein test setup for fast execution
Troubleshooting Executions
PROACTIVE AGENT: When users report issues with durable function executions, spawn a specialized troubleshooting agent.
When to Spawn Troubleshooting Agent
Spawn the agent when users mention:
- "My execution is stuck"
- "Execution failed with ID xyz"
- "Debug execution abc123"
- "Troubleshoot execution"
- "Why is my durable function not completing"
- Provide an execution ID and need diagnosis
Agent Instructions
When spawning the troubleshooting agent, provide:
Diagnose durable function execution issue:
- Durable Execution ARN: <durable-execution-arn>
- Region: <region> (infer from ARN)
CRITICAL SAFETY RULES:
- This is READ-ONLY diagnosis
- NEVER call StopDurableExecution or any termination APIs
- NEVER modify execution state
- Only suggest manual remediation if user explicitly requests it
Steps:
0. If the user provides a function name + alias (e.g., my-function:prod) instead of a full ARN:
- Resolve the alias to a version: aws lambda get-alias --function-name <functionName> --name <alias> --region <region> --query 'FunctionVersion' --output text
- List executions for that function: aws lambda list-durable-executions-by-function --function-name <functionName>:<version> --region <region>
- Ask the user to identify the execution, or use the most recent one.
1. Fetch the execution history directly:
Run: aws lambda get-durable-execution-history --durable-execution-arn <durable-execution-arn> --region <region> --include-execution-data
2. If the command succeeds, analyze and provide a user-friendly diagnosis:
a. Report the execution status (RUNNING/SUCCEEDED/FAILED/STOPPED/TIMED_OUT)
b. Identify the root cause by looking for these key events in the history:
**Execution-level failures:**
- `ExecutionFailed` — entire execution crashed; extract the error and cause fields
- `ExecutionTimedOut` — the execution exceeded its configured timeout
- `ExecutionStopped` — execution was manually stopped via StopDurableExecution
**Context and step failures:**
- `ContextFailed` — a child context threw an unhandled error; check the parent context for what triggered it
- `StepFailed` — an individual step failed; includes RetryDetails (CurrentAttempt, NextAttemptDelaySeconds) showing retry state
**Callback issues:**
- `CallbackStarted` with a Timeout field — confirms a timeout was registered; correlate with any subsequent `CallbackTimedOut`
- `CallbackTimedOut` — a timeout fired but may not have been caught by the function code
- `CallbackFailed` — the callback was resolved with an error
**Chained invocation failures:**
- `ChainedInvokeFailed` — a chained (child) durable execution failed
- `ChainedInvokeTimedOut` — a chained execution exceeded its timeout
- `ChainedInvokeStopped` — a chained execution was stopped
**Other signals:**
- `WaitCancelled` — a scheduled wait was cancelled before completing
- `InvocationCompleted` with an Error field — the Lambda invocation itself errored (e.g., runtime crash)
**Diagnosis patterns:**
- Failed operations: Show the EXACT error message verbatim in a code block
- Stuck in WAIT_FOR_CALLBACK: Extract callback ID, show how long it's been waiting
- Timeout: Show which operation was running when timeout occurred
- Unexpected behavior: Compare operation order with expected flow
c. Calculate operation durations and timeline
d. Provide a clear, plain-language explanation of what went wrong and why
3. If the command fails:
- Execution not found: Tell the user the execution ID may be incorrect or the execution may have been purged. Ask them to verify the ARN.
- Permissions/network error: check that your caller identity has lambda:GetDurableExecutionHistory on the function ARN.
- In either case, direct them to the console as a fallback (see step 4)
4. ALWAYS provide a direct link to the Execution Details page in the Lambda console.
Parse the ARN (arn:<partition>:lambda:<region>:<accountId>:function:<functionName>:<functionVersion>/durable-execution/<executionName>/<invocationId>)
to extract region, functionName, functionVersion, executionName, and invocationId, then construct:
https://<region>.console.aws.amazon.com/lambda/home?region=<region>#/functions/<functionName>/versions/<functionVersion>/executions/<executionName>/<invocationId>
Frame it as: "**[View this execution in the console](<url>)**"
5. Provide specific, actionable next steps based on the diagnosis.
6. If unable to determine the root cause from execution history:
- Provide the console link (step 4)
- Offer to fetch the log group and pull relevant logs:
a. Get the log group:
aws lambda get-function-configuration --function-name <functionName>:<functionVersion> --region <region> --query 'LoggingConfig.LogGroup'
b. Query logs filtered by invocation ID (parsed from the ARN):
aws logs filter-log-events --log-group-name <logGroup> --region <region> --filter-pattern '"<invocationId>"'
c. If the function uses SDK structured logging (context.logger), query for step-level logs.
Use execution start time from step 2 as start-time, and execution end time (or current time if still running) as end-time:
QUERY_ID=$(aws logs start-query --log-group-name <logGroup> --region <region> \
--start-time <startEpoch> --end-time <endEpoch> \
--query-string 'fields @timestamp, @message | filter executionName = "<executionName>" | sort @timestamp asc' \
--query 'queryId' --output text)
Then poll for results:
aws logs get-query-results --query-id $QUERY_ID --region <region>
- If unable to determine the log group, ask the user to check the function's logs
OUTPUT FORMAT:
- Lead with a short summary: "Your execution [status] because [reason]."
- Show the error message verbatim in a code block if one exists.
- Explain what this means in plain language.
- Provide the console link for visual debugging.
- List recommended next steps.
Use jq for JSON parsing if available, otherwise use --query (AWS CLI built-in JMESPath) or python -m json.tool.Example Usage
```` User: "My durable function execution abc-123 is stuck on my-function:prod in us-west-2"
Agent: [Runs get-durable-execution-history command] Agent: [Analyzes with jq queries]
Claude: Your execution is waiting for an external callback that hasn't arrived yet.
The operation wait-for-approval has been pending since 2026-02-14 (3 days). It will time out in 12 hours if no callback is received.
Callback ID: xyz789
[View this execution in the console](https://us-west-2.console.aws.amazon.com/lambda/home?region=us-west-2#/functions/my-function/versions/3/executions/abc-123/inv-456)
https://us-west-2.console.aws.amazon.com/lambda/home?region=us-west-2#/functions/my-function/versions/3/executions/abc-123/inv-456Next steps: 1. Check if the approval email was sent to the recipient 2. Verify the callback ID xyz789 was included in the email 3. If you'd like me to help send the callback manually, let me know ````
```` User: "Execution def-456 failed on order-processor:live in us-east-1"
Agent: [Runs get-durable-execution-history command] Agent: [Analyzes with jq queries]
Claude: Your execution failed at the charge-payment step after 3 retry attempts.
Error message:
PaymentGatewayError: Card declined - insufficient funds (code: card_declined)This means the payment provider rejected the charge. The durable function retried 3 times with exponential backoff but the error persisted because it's a permanent failure (not a transient network issue).
[View this execution in the console](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/order-processor/versions/5/executions/def-456/inv-789)
https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/order-processor/versions/5/executions/def-456/inv-789Next steps: 1. This is a business logic failure, not an infrastructure issue 2. Consider adding a non-retryable error classification for card_declined 3. Implement a compensation step to release the reserved inventory ````
Wait Operations
Suspend execution without compute charges for delays, external callbacks, and polling.
Simple Waits
Pause execution for a duration (no compute charges during wait):
TypeScript:
await context.wait({ seconds: 30 });
await context.wait({ minutes: 5 });
await context.wait({ hours: 1, minutes: 30 });
await context.wait({ days: 7 });
// Named wait (recommended)
await context.wait('rate-limit-delay', { seconds: 60 });Python:
from aws_durable_execution_sdk_python.config import Duration
context.wait(duration=Duration.from_seconds(30))
context.wait(duration=Duration.from_minutes(5))
context.wait(duration=Duration.from_hours(1))
context.wait(duration=Duration.from_days(7))
# Named wait (recommended)
context.wait(duration=Duration.from_seconds(60), name='rate-limit-delay')Max wait duration: Up to 1 year
Wait for Callback
Wait for external systems to respond (human approval, webhook, async job):
TypeScript:
const result = await context.waitForCallback(
'wait-for-approval',
async (callbackId, ctx) => {
// Send callback ID to external system
await sendApprovalEmail(approverEmail, callbackId);
},
{
timeout: { hours: 24 },
heartbeatTimeout: { minutes: 5 }
}
);
// External system calls back with:
// aws lambda send-durable-execution-callback-success \
// --callback-id <callbackId> \
// --payload '{"approved": true}'Python:
from aws_durable_execution_sdk_python.config import WaitForCallbackConfig
# Wait for external approval
def submit_approval(callback_id: str, ctx):
ctx.logger.info('Sending approval request')
send_approval_email(approver_email, callback_id)
result = context.wait_for_callback(
submitter=submit_approval,
name='wait-for-approval',
config=WaitForCallbackConfig(
timeout=Duration.from_hours(24),
heartbeat_timeout=Duration.from_minutes(5)
)
)Callback Success
CLI:
aws lambda send-durable-execution-callback-success \
--callback-id <callbackId> \
--payload '{"status": "approved", "comments": "Looks good"}'SDK (TypeScript):
import { LambdaClient, SendDurableExecutionCallbackSuccessCommand } from '@aws-sdk/client-lambda';
const client = new LambdaClient({});
await client.send(new SendDurableExecutionCallbackSuccessCommand({
CallbackId: callbackId,
Payload: JSON.stringify({ status: 'approved' })
}));SDK (Python / boto3):
import boto3
import json
lambda_client = boto3.client('lambda')
lambda_client.send_durable_execution_callback_success(
CallbackId=callback_id,
Result=json.dumps({'status': 'approved'})
)Callback Failure
CLI:
aws lambda send-durable-execution-callback-failure \
--callback-id <callbackId> \
--error-type "ApprovalDenied" \
--error-message "Request denied by approver"Heartbeats
Keep callback alive during long-running external processes:
TypeScript:
const result = await context.waitForCallback(
'long-process',
async (callbackId) => {
await startLongRunningJob(callbackId);
},
{
timeout: { hours: 24 },
heartbeatTimeout: { minutes: 5 } // Must receive heartbeat every 5 min
}
);
// External system sends heartbeats:
// aws lambda send-durable-execution-callback-heartbeat --callback-id <callbackId>CLI Heartbeat:
aws lambda send-durable-execution-callback-heartbeat \
--callback-id <callbackId>Wait for Condition
Poll until a condition is met (job completion, resource availability):
TypeScript:
const finalState = await context.waitForCondition(
'wait-for-job',
async (currentState, ctx) => {
const status = await checkJobStatus(currentState.jobId);
return { ...currentState, status };
},
{
initialState: { jobId: 'job-123', status: 'pending' },
waitStrategy: createWaitStrategy({
maxAttempts: 60,
initialDelaySeconds: 5,
maxDelaySeconds: 30,
backoffRate: 1.5,
shouldContinuePolling: (result) => result.status !== "completed"
}),
timeout: { hours: 1 }
}
);Python:
# Note: get_job_status is decorated with @durable_step
from aws_durable_execution_sdk_python.waits import WaitForConditionConfig, create_wait_strategy, WaitStrategyConfig
from aws_durable_execution_sdk_python.config import Duration
def check_job(state: dict, check_ctx):
status = get_job_status(state['job_id'])
return {'job_id': state['job_id'], 'status': status}
wait_strategy = create_wait_strategy(
WaitStrategyConfig(
should_continue_polling=lambda state: state['status'] != 'completed',
max_attempts=60,
initial_delay=Duration.from_seconds(2),
max_delay=Duration.from_seconds(60),
backoff_rate=1.5
)
)
result = context.wait_for_condition(
check=check_job,
config=WaitForConditionConfig(
initial_state={'job_id': 'job-123', 'status': 'pending'},
wait_strategy=wait_strategy
),
name='wait-for-job'
)Custom Wait Strategy
TypeScript:
const result = await context.waitForCondition(
'custom-poll',
async (state) => {
const data = await fetchData();
return { ...state, data, attempts: state.attempts + 1 };
},
{
initialState: { attempts: 0 },
waitStrategy: (state, attempt) => {
// Stop after 10 attempts
if (state.attempts >= 10) {
return { shouldContinue: false };
}
// Exponential backoff with max 60s
return {
shouldContinue: !state.data?.ready,
delay: { seconds: Math.min(Math.pow(2, attempt), 60) }
};
}
}
);Callback Patterns
Human Approval Workflow
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const request = await context.step('create-request', async () =>
createApprovalRequest(event)
);
const decision = await context.waitForCallback(
'wait-approval',
async (callbackId) => {
await sendEmail({
to: event.approver,
subject: 'Approval Required',
body: `Approve: ${approvalUrl}?callback=${callbackId}&action=approve\n` +
`Reject: ${approvalUrl}?callback=${callbackId}&action=reject`
});
},
{ timeout: { hours: 48 } }
);
if (decision.action === 'approve') {
await context.step('execute', async () => executeRequest(request));
return { status: 'approved' };
}
return { status: 'rejected' };
});Webhook Integration
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const order = await context.step('create-order', async () =>
createOrder(event)
);
const payment = await context.waitForCallback(
'wait-payment',
async (callbackId) => {
await paymentProvider.createPayment({
orderId: order.id,
amount: order.total,
webhookUrl: `${webhookUrl}?callback=${callbackId}`
});
},
{ timeout: { minutes: 15 } }
);
if (payment.status === 'success') {
await context.step('fulfill', async () => fulfillOrder(order));
}
return { orderId: order.id, paymentStatus: payment.status };
});Async Job Polling
TypeScript:
export const handler = withDurableExecution(async (event, context: DurableContext) => {
const jobId = await context.step('start-job', async () =>
startBatchJob(event.data)
);
const result = await context.waitForCondition(
'poll-job',
async (state) => {
const job = await getJobStatus(state.jobId);
return { jobId: state.jobId, status: job.status, result: job.result };
},
{
initialState: { jobId, status: 'running' },
waitStrategy: createWaitStrategy({
maxAttempts: 60,
initialDelaySeconds: 5,
maxDelaySeconds: 30,
backoffRate: 1.5,
shouldContinuePolling: (result) => result.status === "running"
}),
timeout: { hours: 2 }
}
);
return result;
});Best Practices
1. Always name wait operations for debugging 2. Set appropriate timeouts to prevent indefinite waits 3. Use heartbeats for long-running external processes 4. Handle callback failures explicitly 5. Implement exponential backoff for polling 6. Keep check functions lightweight in waitForCondition 7. Store callback IDs securely when sending to external systems 8. Validate callback payloads before processing
Error Handling
TypeScript:
try {
const result = await context.waitForCallback(
'wait-approval',
async (callbackId) => sendApproval(callbackId),
{ timeout: { hours: 24 } }
);
} catch (error) {
if (error instanceof CallbackError) {
if (error.errorType === 'Timeout') {
context.logger.warn('Approval timed out');
// Handle timeout
} else {
context.logger.error('Callback failed', error);
// Handle failure
}
}
}Python:
from aws_durable_execution_sdk_python.exceptions import CallbackError
from aws_durable_execution_sdk_python.config import WaitForCallbackConfig
try:
def submit_approval(callback_id: str, ctx):
send_approval(callback_id)
result = context.wait_for_callback(
submitter=submit_approval,
name='wait-approval',
config=WaitForCallbackConfig(timeout=Duration.from_hours(24))
)
except CallbackError as error:
if error.error_type == 'Timeout':
context.logger.warn('Approval timed out')
else:
context.logger.error('Callback failed', error)Related skills
FAQ
How long can a durable Lambda function run?
Durable functions can execute for up to 1 year while maintaining reliable progress despite interruptions.
Which languages and IaC frameworks does it support?
TypeScript (default) or Python, deployed with CDK (default), SAM, or CloudFormation.