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. Works best with the AWS MCP server but is not required. All AWS interactions in this skill use standard AWS CLI commands that work in any environment with configured AWS credentials. Critical Rules Read these before writing any code. Each one is a constraint that will silently break a function if violated. 1. Durable execution must be enabled at function creation time — it cannot be retrofitted. A new Lambda function…

: 'ts-jest',\n },\n collectCoverageFrom: [\n 'src/**/*.ts',\n '!src/**/*.d.ts',\n ],\n};\n```\n\n**Key Configuration:**\n\n- `preset: 'ts-jest'` - Essential for TypeScript support\n- `transform` - Maps .ts files to ts-jest transformer\n- `testMatch` - Specifies test file patterns\n\n## Python Project Setup\n\nAdd `aws-durable-execution-sdk-python-testing` to your dev/test dependencies in pyproject.toml.\n\n## Development Workflow\n\n### TypeScript\n\n1. **Write handler** with durable operations\n2. **Test locally** with `LocalDurableTestRunner`\n3. **Validate replay rules** (no non-deterministic code outside steps)\n4. **Deploy** with qualified ARN (version or alias)\n5. **Monitor** execution state and logs\n\n### Python\n\n1. **Write handler** with `@durable_execution` decorator\n2. **Test locally** with `DurableFunctionTestRunner` and pytest\n3. **Validate replay rules** (no non-deterministic code outside steps)\n4. **Deploy** with qualified ARN (version or alias)\n5. **Monitor** execution state and logs\n\n## Key Concepts\n\n- **Steps**: Atomic operations with automatic retry and checkpointing\n- **Waits**: Suspend execution without compute charges (up to 1 year)\n- **Child Contexts**: Group multiple durable operations\n- **Callbacks**: Wait for external systems to respond\n- **Map/Parallel**: Process arrays or run operations concurrently\n\n## Setup Checklist\n\nWhen starting a new durable function project:\n\n### TypeScript\n\n- [ ] Install dependencies (`@aws/durable-execution-sdk-js`, testing & eslint packages)\n- [ ] Create `jest.config.js` with ts-jest preset\n- [ ] Configure `tsconfig.json` with proper module resolution\n- [ ] Set up ESLint with durable execution plugin\n- [ ] Create handler with `withDurableExecution` wrapper\n- [ ] Write tests using `LocalDurableTestRunner`\n- [ ] Use `skipTime: true` for fast test execution\n- [ ] Verify TypeScript compilation: `npx tsc --noEmit`\n- [ ] Run tests to confirm setup: `npm test`\n- [ ] Review replay model rules (no non-deterministic code outside steps)\n\n### Python\n\n- [ ] Install `aws-durable-execution-sdk-python`\n- [ ] Install `aws-durable-execution-sdk-python-testing` and `pytest` for testing\n- [ ] Create handler with `@durable_execution` decorator\n- [ ] Define step functions with `@durable_step` decorator\n- [ ] Write tests using `DurableFunctionTestRunner` class\n- [ ] Run tests: `pytest`\n- [ ] Review replay model rules (no non-deterministic code outside steps)\n\n## Error Scenarios\n\n### Unsupported Language\n\n- List detected language\n- State: \"Durable Execution SDK is not yet available for [language]\"\n- List supported languages as alternatives\n\n## Next Steps\n\n- Review **replay-model-rules.md** to avoid common pitfalls\n- Explore **step-operations.md** for retry strategies\n- Learn **wait-operations.md** for external integrations\n- Check **testing-patterns.md** for comprehensive testing\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12932,"content_sha256":"1828428cd6429f5f9c1bf50f8a6b596856a4e010a83b3260f40f20a5781bfefd"},{"filename":"references/replay-model-rules.md","content":"# Replay Model Rules - CRITICAL\n\nThe replay model is the foundation of durable functions. Violations cause subtle, hard-to-debug issues. **Read this carefully.**\n\n## How Replay Works\n\nDurable functions use a \"checkpoint and replay\" execution model:\n\n1. Code runs from the beginning on every invocation\n2. Steps that already completed return their checkpointed results WITHOUT re-executing\n3. Code OUTSIDE steps executes again on every replay\n4. New steps execute when reached\n\n**Example:**\n\n```typescript\n// First execution: Runs lines 1-5\n// After wait: Runs lines 1-5 again (line 2 returns cached result)\nconst data = await context.step('fetch', async () => fetchAPI()); // Line 2: Executes once, cached\nawait context.wait({ seconds: 60 }); // Line 3: Waits\nconst result = await context.step('process', async () => process(data)); // Line 5: Executes after wait\n```\n\n## Rule 1: Deterministic Code Outside Steps\n\n**ALL code outside steps MUST produce the same result on every replay.**\n\n### ❌ WRONG - Non-Deterministic Outside Steps\n\n**TypeScript:**\n\n```typescript\n// These values change on each replay!\nconst id = uuid.v4(); // Different UUID each time\nconst timestamp = Date.now(); // Different timestamp each time\nconst random = Math.random(); // Different random number\nconst now = new Date(); // Different date each time\n\nawait context.step('save', async () => saveData({ id, timestamp }));\n```\n\n**Python:**\n\n```python\n# These values change on each replay!\nid = str(uuid.uuid4()) # Different UUID each time\ntimestamp = time.time() # Different timestamp each time\nrandom_val = random.random() # Different random number\nnow = datetime.now() # Different datetime each time\n\ncontext.step(lambda _: save_data({\"id\": id}), name='save')\n```\n\n### ✅ CORRECT - Non-Deterministic Inside Steps\n\n**TypeScript:**\n\n```typescript\nconst id = await context.step('generate-id', async () => uuid.v4());\nconst timestamp = await context.step('get-time', async () => Date.now());\nconst random = await context.step('random', async () => Math.random());\nconst now = await context.step('get-date', async () => new Date());\n\nawait context.step('save', async () => saveData({ id, timestamp }));\n```\n\n**Python:**\n\n```python\nid = context.step(lambda _: str(uuid.uuid4()), name='generate-id')\ntimestamp = context.step(lambda _: time.time(), name='get-time')\nrandom_val = context.step(lambda _: random.random(), name='random')\nnow = context.step(lambda _: datetime.now(), name='get-date')\n\ncontext.step(lambda _: save_data({\"id\": id}), name='save')\n```\n\n### Must Be In Steps\n\n- `Date.now()`, `new Date()`, `time.time()`, `datetime.now()`\n- `Math.random()`, `random.random()`\n- UUID generation (`uuid.v4()`, `uuid.uuid4()`)\n- API calls, HTTP requests\n- Database queries\n- File system operations\n- Environment variable reads (if they can change)\n- Any external system interaction\n\n## Rule 2: No Nested Durable Operations\n\n**You CANNOT call durable operations inside a step function.**\n\n### ❌ WRONG - Nested Operations\n\n**TypeScript:**\n\n```typescript\nawait context.step('process', async () => {\n await context.wait({ seconds: 1 }); // ERROR!\n await context.step(async () => ...); // ERROR!\n await context.invoke('other-fn', ...); // ERROR!\n return result;\n});\n```\n\n**Python:**\n\n```python\n@durable_step\ndef process(step_ctx: StepContext):\n context.wait(duration=Duration.from_seconds(1)) # ERROR!\n context.step(lambda _: ..., name='nested') # ERROR!\n return result\n\ncontext.step(process())\n```\n\n### ✅ CORRECT - Use Child Context\n\n**TypeScript:**\n\n```typescript\nawait context.runInChildContext('process', async (childCtx) => {\n await childCtx.wait({ seconds: 1 });\n const step1 = await childCtx.step('validate', async () => validate());\n const step2 = await childCtx.step('process', async () => process(step1));\n return step2;\n});\n```\n\n**Python:**\n\n```python\n# Note: validate and process are decorated with @durable_step\ndef process_child(child_ctx: DurableContext):\n child_ctx.wait(duration=Duration.from_seconds(1))\n step1 = child_ctx.step(validate())\n step2 = child_ctx.step(process(step1))\n return step2\n\ncontext.run_in_child_context(func=process_child, name='process')\n```\n\n## Rule 3: Closure Mutations Are Lost\n\n**Variables mutated inside steps are NOT preserved across replays.**\n\n### ❌ WRONG - Lost Mutations\n\n**TypeScript:**\n\n```typescript\nlet counter = 0;\nawait context.step('increment', async () => {\n counter++; // This mutation is lost!\n});\nconsole.log(counter); // Always 0 on replay!\n```\n\n**Python:**\n\n```python\ncounter = 0\n@durable_step\ndef increment(step_ctx: StepContext):\n nonlocal counter\n counter += 1 # This mutation is lost!\n\ncontext.step(increment())\nprint(counter) # Always 0 on replay!\n```\n\n### ✅ CORRECT - Return Values\n\n**TypeScript:**\n\n```typescript\nlet counter = 0;\ncounter = await context.step('increment', async () => counter + 1);\nconsole.log(counter); // Correct value\n```\n\n**Python:**\n\n```python\ncounter = 0\ncounter = context.step(lambda _: counter + 1, name='increment')\nprint(counter) # Correct value\n```\n\n## Rule 4: Side Effects Outside Steps Repeat\n\n**Side effects outside steps happen on EVERY replay.**\n\n### ❌ WRONG - Repeated Side Effects\n\n**TypeScript:**\n\n```typescript\nconsole.log('Starting process'); // Logs multiple times!\nawait sendEmail(user.email); // Sends multiple emails!\nawait updateDatabase(data); // Updates multiple times!\n\nawait context.step('process', async () => process());\n```\n\n**Python:**\n\n```python\nprint('Starting process') # Prints multiple times!\nsend_email(user.email) # Sends multiple emails!\nupdate_database(data) # Updates multiple times!\n\ncontext.step(lambda _: process(), name='process')\n```\n\n### ✅ CORRECT - Side Effects In Steps\n\n**TypeScript:**\n\n```typescript\ncontext.logger.info('Starting process'); // Deduplicated automatically\nawait context.step('send-email', async () => sendEmail(user.email));\nawait context.step('update-db', async () => updateDatabase(data));\nawait context.step('process', async () => process());\n```\n\n**Python:**\n\n```python\n# Note: Functions are decorated with @durable_step\ncontext.logger.info('Starting process') # Deduplicated automatically\ncontext.step(send_email(user.email))\ncontext.step(update_database(data))\ncontext.step(process())\n```\n\n### Exception: context.logger\n\n`context.logger` is replay-aware and safe to use anywhere. It automatically deduplicates logs across replays.\n\n## Common Pitfalls\n\n### Pitfall 1: Reading Environment Variables\n\n```typescript\n// ❌ WRONG if env vars can change\nconst apiKey = process.env.API_KEY;\nawait context.step('call-api', async () => callAPI(apiKey));\n\n// ✅ CORRECT\nconst apiKey = await context.step('get-key', async () => process.env.API_KEY);\nawait context.step('call-api', async () => callAPI(apiKey));\n```\n\n### Pitfall 2: Array/Object Mutations\n\n```typescript\n// ❌ WRONG\nconst items = [];\nawait context.step('add-item', async () => {\n items.push(newItem); // Lost on replay\n});\n\n// ✅ CORRECT\nlet items = [];\nitems = await context.step('add-item', async () => [...items, newItem]);\n```\n\n### Pitfall 3: Conditional Logic with Non-Deterministic Values\n\n```typescript\n// ❌ WRONG\nif (Math.random() > 0.5) { // Different on each replay!\n await context.step('path-a', async () => ...);\n} else {\n await context.step('path-b', async () => ...);\n}\n\n// ✅ CORRECT\nconst shouldTakePathA = await context.step('decide', async () => Math.random() > 0.5);\nif (shouldTakePathA) {\n await context.step('path-a', async () => ...);\n} else {\n await context.step('path-b', async () => ...);\n}\n```\n\n## Debugging Replay Issues\n\nIf you see inconsistent behavior:\n\n1. **Check for non-deterministic code outside steps**\n2. **Verify no nested durable operations**\n3. **Look for closure mutations**\n4. **Search for side effects outside steps**\n5. **Use `context.logger` to trace execution flow**\n\n## Testing Replay Behavior\n\nAlways test with multiple invocations to simulate replay:\n\n```typescript\nconst runner = new LocalDurableTestRunner({ handlerFunction: handler });\nconst execution = await runner.run({ payload: { test: true } });\n\n// Verify operations executed correctly\nconst step1 = runner.getOperation('step-name');\nexpect(step1.getStatus()).toBe(OperationStatus.SUCCEEDED);\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8493,"content_sha256":"9606ff4878b86744a736f6fb360b8df82ec692ed58da185b82a847391014bd42"},{"filename":"references/step-operations.md","content":"# Step Operations\n\nSteps are atomic operations with automatic retry and state persistence.\n\n## Basic Step Patterns\n\n### Python: Two Ways to Define Steps\n\n**Recommended: `@durable_step` Decorator**\n\n```python\nfrom aws_durable_execution_sdk_python import durable_step, StepContext\n\n@durable_step\ndef fetch_user(step_ctx: StepContext, user_id: str):\n \"\"\"Fetch user from database - reusable step function.\"\"\"\n return fetch_user_from_api(user_id)\n\n# Call it - name is automatically inferred from function name\nresult = context.step(fetch_user(user_id))\n```\n\nAlternative: **Inline Lambda**\n\n```python\n# For simple one-off operations\nresult = context.step(\n func=lambda step_ctx: fetch_user_from_api(user_id),\n name='fetch-user'\n)\n```\n\n**Use `@durable_step` for:**\n\n- Reusable step functions\n- Complex logic\n- Better readability and testing\n\n**Use lambda for:**\n\n- Simple inline operations\n- One-off transformations\n\n### TypeScript: Named Steps\n\n**TypeScript:**\n\n```typescript\nconst result = await context.step('fetch-user', async () => {\n return await fetchUserFromAPI(userId);\n});\n```\n\n**Best Practice:** Always name steps for easier debugging and testing.\n\n## Retry Configuration\n\n### Exponential Backoff\n\n**TypeScript:**\n\n```typescript\nimport { createRetryStrategy, JitterStrategy } from '@aws/durable-execution-sdk-js';\n\nconst result = await context.step(\n 'api-call',\n async () => callExternalAPI(),\n {\n retryStrategy: createRetryStrategy({\n maxAttempts: 5,\n initialDelay: { seconds: 1 },\n maxDelay: { seconds: 60 },\n backoffRate: 2.0,\n jitter: JitterStrategy.FULL\n })\n }\n);\n```\n\n**Python:**\n\n```python\n# Note: api_call is decorated with @durable_step\nfrom aws_durable_execution_sdk_python.config import StepConfig, Duration\nfrom aws_durable_execution_sdk_python.retries import RetryStrategyConfig, create_retry_strategy, JitterStrategy\n\nretry_config = RetryStrategyConfig(\n max_attempts=5,\n initial_delay=Duration.from_seconds(5),\n max_delay=Duration.from_seconds(60),\n backoff_rate=2.0,\n jitter_strategy=JitterStrategy.FULL\n)\n\nresult = context.step(\n func=api_call(),\n config=StepConfig(retry_strategy=create_retry_strategy(retry_config))\n)\n```\n\n### Custom Retry Strategy\n\n**TypeScript:**\n\n```typescript\nconst result = await context.step(\n 'custom-retry',\n async () => riskyOperation(),\n {\n retryStrategy: (error, attemptCount) => {\n // Don't retry validation errors\n if (error.name === 'ValidationError') {\n return { shouldRetry: false };\n }\n \n // Retry up to 3 times with exponential backoff\n if (attemptCount \u003c 3) {\n return {\n shouldRetry: true,\n delay: { seconds: Math.pow(2, attemptCount) }\n };\n }\n \n return { shouldRetry: false };\n }\n }\n);\n```\n\n**Python:**\n\n```python\nfrom aws_durable_execution_sdk_python.retries import RetryDecision\n\ndef custom_retry(error: Exception, attempt: int) -> RetryDecision:\n if isinstance(error, ValidationError):\n return RetryDecision.no_retry()\n \n if attempt \u003c 3:\n return RetryDecision(\n should_retry=True,\n delay=Duration.from_seconds(2 ** attempt)\n )\n \n return RetryDecision.no_retry()\n\nresult = context.step(\n risky_operation(),\n config=StepConfig(retry_strategy=custom_retry)\n)\n```\n\n### Retryable Error Types\n\n**TypeScript:**\n\n```typescript\nclass NetworkError extends Error {\n name = 'NetworkError';\n}\n\nclass TimeoutError extends Error {\n name = 'TimeoutError';\n}\n\nconst result = await context.step(\n 'selective-retry',\n async () => operation(),\n {\n retryStrategy: createRetryStrategy({\n maxAttempts: 3,\n retryableErrorTypes: [NetworkError, TimeoutError]\n })\n }\n);\n```\n\n**Python:**\n\n```python\nretry_config = RetryStrategyConfig(\n max_attempts=3,\n retryable_error_types=[NetworkError, TimeoutError]\n)\n```\n\n## Step Semantics\n\n### AtLeastOncePerRetry (default)\n\nStep executes at least once on each retry attempt. If the step succeeds but the checkpoint fails (e.g. due to a sandbox crash), the step will re-execute on replay. Use for idempotent operations that can tolerate duplicate execution.\n\n**TypeScript:**\n\n```typescript\nimport { StepSemantics } from '@aws/durable-execution-sdk-js';\n\nconst result = await context.step(\n 'idempotent-operation',\n async () => idempotentAPI(),\n { semantics: StepSemantics.AtLeastOncePerRetry }\n);\n```\n\n### AtMostOncePerRetry\n\nStep executes at most once per retry attempt. If a crash happens between the pre-step checkpoint and step completion, the step is skipped on replay rather than re-executed. The step can still run across multiple retry attempts. To guarantee at-most-once overall, pair with `retryStrategy: () => ({ shouldRetry: false })`.\n\n**TypeScript:**\n\n```typescript\nimport { StepSemantics } from '@aws/durable-execution-sdk-js';\n\nconst result = await context.step(\n 'charge-payment',\n async () => chargeCard(amount),\n {\n semantics: StepSemantics.AtMostOncePerRetry,\n retryStrategy: () => ({ shouldRetry: false })\n }\n);\n```\n\n**Python:**\n\n```python\nfrom aws_durable_execution_sdk_python.config import StepSemantics, StepConfig\n\nresult = context.step(\n charge_card(amount),\n config=StepConfig(\n step_semantics=StepSemantics.AT_MOST_ONCE_PER_RETRY,\n retry_strategy=lambda error, attempt: RetryDecision.no_retry()\n )\n)\n```\n\n## Custom Serialization\n\nFor complex types, provide custom serialization:\n\n**TypeScript:**\n\n```typescript\nimport { createClassSerdesWithDates } from '@aws/durable-execution-sdk-js';\n\nclass User {\n id: string = '';\n name: string = '';\n createdAt: Date = new Date();\n}\n\nconst userSerdes = createClassSerdesWithDates(User, ['createdAt']);\n\nconst user = await context.step(\n 'fetch-user',\n async () => {\n const user = new User();\n user.id = '123';\n user.name = 'Alice';\n user.createdAt = new Date();\n return user;\n },\n { serdes: userSerdes }\n);\n```\n\n**Python:**\n\n```python\nfrom dataclasses import dataclass\nfrom datetime import datetime\n\n@dataclass\nclass User:\n id: str\n name: str\n created_at: datetime\n\n# Python SDK handles dataclass serialization automatically\nuser = context.step(\n lambda _: User('123', 'Alice', datetime.now()),\n name='fetch-user'\n)\n```\n\n## When to Use Steps vs Child Contexts\n\n### Use Steps For:\n\n- Single atomic operations\n- API calls\n- Database queries\n- Data transformations\n- Operations that should retry as a unit\n\n### Use Child Contexts For:\n\n- Grouping multiple durable operations\n- Complex workflows with steps, waits, and invokes\n- Isolating state tracking\n- Organizing related operations\n\n**Example:**\n\n```typescript\n// ❌ WRONG: Cannot nest durable operations in step\nawait context.step('process', async () => {\n await context.wait({ seconds: 1 }); // ERROR!\n});\n\n// ✅ CORRECT: Use child context\nawait context.runInChildContext('process', async (childCtx) => {\n const data = await childCtx.step('fetch', async () => fetch());\n await childCtx.wait({ seconds: 1 });\n return await childCtx.step('save', async () => save(data));\n});\n```\n\n## Error Handling\n\nSteps throw errors after all retry attempts are exhausted:\n\n**TypeScript:**\n\n```typescript\ntry {\n const result = await context.step('risky', async () => riskyOperation());\n} catch (error) {\n if (error instanceof StepError) {\n context.logger.error('Step failed', error.cause);\n // Handle or rethrow\n }\n}\n```\n\n**Python:**\n\n```python\ntry:\n # Note: risky_operation is decorated with @durable_step\n result = context.step(risky_operation())\nexcept Exception as error:\n context.logger.error('Step failed: %s', str(error))\n # Handle or rethrow\n```\n\nFor SDK-specific exceptions, use the base class or specific types:\n\n```python\nfrom aws_durable_execution_sdk_python import DurableExecutionsError\n\ntry:\n result = context.step(risky_operation())\nexcept DurableExecutionsError as error:\n context.logger.error('SDK error: %s', str(error))\nexcept Exception as error:\n context.logger.error('Application error: %s', str(error))\n```\n\n## Best Practices\n\n1. **Always name steps** for debugging and testing\n2. **Keep steps atomic** - one logical operation per step\n3. **Make steps idempotent** when possible\n4. **Use appropriate retry strategies** based on operation type\n5. **Handle errors explicitly** - don't let them propagate unexpectedly\n6. **Use custom serialization** for complex types\n7. **Choose correct semantics** (`AtLeastOncePerRetry` vs `AtMostOncePerRetry`)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8511,"content_sha256":"d486496fc5b9ba8fcf27f7a4dd8a7626dafdf6b3931a81b6294d734e644de98b"},{"filename":"references/testing-patterns.md","content":"# Testing Patterns\n\nTest durable functions locally and in the cloud with comprehensive test runners.\n\n## Critical Testing Patterns\n\n**ALWAYS follow these patterns to avoid flaky tests:**\n\n### DO:\n\n- ✅ Name all operations for test reliability\n- ✅ TypeScript: Use `runner.getOperation(\"name\")` to find operations by name\n- ✅ TypeScript: Use `WaitingOperationStatus.STARTED` when waiting for callback operations\n- ✅ TypeScript: JSON.stringify callback parameters: `sendCallbackSuccess(JSON.stringify(data))`\n- ✅ TypeScript: Use `skipTime: true` in setupTestEnvironment for fast tests\n- ✅ TypeScript: Wrap event data in `payload` object: `runner.run({ payload: { ... } })`\n- ✅ TypeScript: Cast `getResult()` to appropriate type: `execution.getResult() as ResultType`\n- ✅ Python: Use `result.get_step(\"name\")` to find step operations by name\n- ✅ Python: Use `result.operations` to iterate and filter operations by type\n- ✅ Python: Instantiate `DurableFunctionTestRunner(handler=my_handler)` directly\n- ✅ Python: Use `runner.run(input={...}, timeout=10)` — note `input=` not `payload`\n- ✅ Python: The value of result.result is serialized. Deserialize using the appropriate SerDes or default json deserializer.\n\n### DON'T:\n\n- ❌ Use `getOperationByIndex()` unless absolutely necessary\n- ❌ Assume operation indices are stable (parallel creates nested operations)\n- ❌ TypeScript: Send objects to sendCallbackSuccess — stringify first\n- ❌ TypeScript: Forget that callback results are JSON strings — parse them\n- ❌ TypeScript: Test callbacks without proper synchronization (leads to race conditions)\n- ❌ Python: Confuse `DurableFunctionTestRunner` (local) with `DurableFunctionCloudTestRunner` (cloud)\n- ❌ Python: Forget the `with runner:` context manager — it manages execution lifecycle\n\n## Local Testing Setup\n\n**TypeScript:**\n\n```typescript\nimport {\n LocalDurableTestRunner,\n OperationType,\n OperationStatus\n} from '@aws/durable-execution-sdk-js-testing';\n\ndescribe('My Durable Function', () => {\n beforeAll(() => \n LocalDurableTestRunner.setupTestEnvironment({ skipTime: true })\n );\n \n afterAll(() => \n LocalDurableTestRunner.teardownTestEnvironment()\n );\n\n it('should execute workflow', async () => {\n const runner = new LocalDurableTestRunner({ \n handlerFunction: handler \n });\n \n const execution = await runner.run({ \n payload: { userId: '123' } \n });\n\n expect(execution.getStatus()).toBe('SUCCEEDED');\n expect(execution.getResult()).toEqual({ success: true });\n });\n});\n```\n\n**Python:**\n\nThe Python testing SDK provides `DurableFunctionTestRunner` for local testing and `DurableFunctionCloudTestRunner` for cloud testing.\n\nInstall the testing SDK:\n\n```bash\npip install aws-durable-execution-sdk-python-testing pytest\n```\n\nExample test:\n\n```python\nfrom aws_durable_execution_sdk_python_testing import DurableFunctionTestRunner\nfrom aws_durable_execution_sdk_python.execution import InvocationStatus\nfrom src.my_function import handler\n\ndef test_workflow():\n \"\"\"Test durable function locally.\"\"\"\n runner = DurableFunctionTestRunner(handler=handler)\n \n with runner:\n result = runner.run(input='{\"user_id\": \"123\"}', timeout=10)\n\n assert result.status is InvocationStatus.SUCCEEDED\n```\n\n## Getting Operations\n\n**CRITICAL: Always get operations by NAME, not by index.**\n\n**TypeScript:**\n\n```typescript\nit('should execute steps in order', async () => {\n const runner = new LocalDurableTestRunner({ handlerFunction: handler });\n await runner.run({ payload: { test: true } });\n\n // ✅ CORRECT: Get by name\n const fetchStep = runner.getOperation('fetch-user');\n expect(fetchStep.getType()).toBe(OperationType.STEP);\n expect(fetchStep.getStatus()).toBe(OperationStatus.SUCCEEDED);\n\n const processStep = runner.getOperation('process-data');\n expect(processStep.getStatus()).toBe(OperationStatus.SUCCEEDED);\n\n // ❌ WRONG: Get by index (brittle, breaks easily)\n // const step1 = runner.getOperationByIndex(0);\n});\n```\n\n**Python:**\n\n```python\nfrom aws_durable_execution_sdk_python.lambda_service import OperationType\ndef test_steps_execute():\n \"\"\"Test step execution.\"\"\"\n runner = DurableFunctionTestRunner(handler=handler)\n \n with runner:\n result = runner.run(input={'test': True}, timeout=10)\n\n # ✅ CORRECT: Get step by name\n fetch_step = result.get_step('fetch-user')\n assert fetch_step is not None\n\n # ✅ Also valid: filter result.operations by type\n step_names = {op.name for op in result.operations if op.operation_type == OperationType.STEP}\n assert step_names >= {'fetch-user', 'process-data'}\n assert 'process-data' in step_names\n```\n\n## Testing Replay Behavior\n\n**TypeScript:**\n\n```typescript\nit('should handle replay correctly', async () => {\n const runner = new LocalDurableTestRunner({ handlerFunction: handler });\n \n // First execution\n const execution1 = await runner.run({ payload: { value: 42 } });\n expect(execution1.getStatus()).toBe('SUCCEEDED');\n\n // Simulate replay\n const execution2 = await runner.run({ payload: { value: 42 } });\n expect(execution2.getStatus()).toBe('SUCCEEDED');\n \n // Results should be identical\n expect(execution1.getResult()).toEqual(execution2.getResult());\n});\n```\n\n## Testing with Fake Clock\n\n**TypeScript:**\n\n```typescript\nit('should wait for specified duration', async () => {\n const runner = new LocalDurableTestRunner({ \n handlerFunction: handler \n });\n\n const executionPromise = runner.run({ payload: {} });\n\n // Advance time by 60 seconds\n await runner.skipTime({ seconds: 60 });\n\n const execution = await executionPromise;\n expect(execution.getStatus()).toBe('SUCCEEDED');\n\n const waitOp = runner.getOperation('delay');\n expect(waitOp.getType()).toBe(OperationType.WAIT);\n expect(waitOp.getWaitDetails()?.waitSeconds).toBe(60);\n});\n```\n\n## Test Runner API Patterns\n\n**CRITICAL:** Always wrap event data in `payload` and cast results appropriately.\n\n**TypeScript:**\n\n```typescript\nit('should use correct test runner API', async () => {\n const runner = new LocalDurableTestRunner({\n handlerFunction: handler,\n });\n\n // ✅ CORRECT: Wrap event in payload\n const execution = await runner.run({\n payload: { name: 'Alice', userId: '123' }\n });\n\n // ✅ CORRECT: Type cast result\n const result = execution.getResult() as {\n greeting: string;\n message: string;\n };\n\n expect(result.greeting).toBe('Hello, Alice!');\n\n // ✅ CORRECT: Get operations by name\n const greetingStep = runner.getOperation('generate-greeting');\n expect(greetingStep.getStepDetails()?.result).toBe('Hello, Alice!');\n});\n\n// ❌ WRONG: Missing payload wrapper and type casting\nit('incorrect api usage', async () => {\n const runner = new LocalDurableTestRunner({ handlerFunction: handler });\n\n // ❌ Missing payload wrapper\n const execution = await runner.run({ name: 'Alice' });\n\n // ❌ No type casting - result is 'unknown'\n const result = execution.getResult();\n // expect(result.greeting).toBe('...'); // Type error!\n});\n```\n\n## Testing Callbacks\n\n**CRITICAL:** Use `waitForData()` with `WaitingOperationStatus.STARTED` to avoid flaky tests caused by promise races.\n\n**TypeScript:**\n\n```typescript\nimport { WaitingOperationStatus } from '@aws/durable-execution-sdk-js-testing';\n\nit('should handle callback success', async () => {\n const runner = new LocalDurableTestRunner({ handlerFunction: handler });\n\n // Start execution (will pause at callback)\n const executionPromise = runner.run({\n payload: { approver: '[email protected]' }\n });\n\n // ✅ CRITICAL: Get operation by NAME\n const callbackOp = runner.getOperation('wait-for-approval');\n\n // ✅ CRITICAL: Wait for operation to reach STARTED status\n await callbackOp.waitForData(WaitingOperationStatus.STARTED);\n\n // ✅ CRITICAL: Must JSON.stringify callback data!\n await callbackOp.sendCallbackSuccess(\n JSON.stringify({ approved: true, comments: 'Looks good' })\n );\n\n const execution = await executionPromise;\n expect(execution.getStatus()).toBe('SUCCEEDED');\n\n // ✅ CRITICAL: Parse JSON string result\n const result: any = execution.getResult();\n const approval = typeof result.approval === 'string'\n ? JSON.parse(result.approval)\n : result.approval;\n\n expect(approval.approved).toBe(true);\n expect(approval.comments).toBe('Looks good');\n});\n\nit('should handle callback failure', async () => {\n const runner = new LocalDurableTestRunner({ handlerFunction: handler });\n\n const executionPromise = runner.run({ payload: {} });\n\n const callbackOp = runner.getOperation('wait-for-approval');\n await callbackOp.waitForData(WaitingOperationStatus.STARTED);\n\n await callbackOp.sendCallbackFailure(\n 'ApprovalDenied',\n 'Request was rejected'\n );\n\n const execution = await executionPromise;\n expect(execution.getStatus()).toBe('FAILED');\n});\n```\n\n**Python:**\n\nTesting callbacks in Python follows the same marker pattern. The callback operation\nappears in `result.operations` with `operation_type == OperationType.CALLBACK`:\n\n```python\ndef test_callback_creation():\n \"\"\"Test that callback is created correctly.\"\"\"\n runner = DurableFunctionTestRunner(handler=handler)\n \n with runner:\n result = runner.run(input={'approver': '[email]'}, timeout=10)\n\n # Find callback operations in the result\n callback_ops = [\n op for op in result.operations\n if op.operation_type == OperationType.CALLBACK\n ]\n assert len(callback_ops) == 1\n assert callback_ops[0].name == 'wait-for-approval'\n assert callback_ops[0].callback_id is not None\n```\n\n## Testing Callback Heartbeats\n\n**TypeScript:**\n\n```typescript\nit('should handle callback heartbeats', async () => {\n const runner = new LocalDurableTestRunner({ handlerFunction: handler });\n\n const executionPromise = runner.run({ payload: {} });\n\n const callbackOp = runner.getOperation('long-running-process');\n await callbackOp.waitForData(WaitingOperationStatus.STARTED);\n\n // Send heartbeats\n await callbackOp.sendCallbackHeartbeat();\n await runner.skipTime({ minutes: 2 });\n await callbackOp.sendCallbackHeartbeat();\n await runner.skipTime({ minutes: 2 });\n \n // Complete callback\n await callbackOp.sendCallbackSuccess(JSON.stringify({ status: 'completed' }));\n\n const execution = await executionPromise;\n expect(execution.getStatus()).toBe('SUCCEEDED');\n});\n```\n\n## Testing Error Scenarios\n\n**TypeScript:**\n\n```typescript\nit('should retry on failure', async () => {\n let attemptCount = 0;\n \n const testHandler = withDurableExecution(async (event, context: DurableContext) => {\n return await context.step('flaky-operation', async () => {\n attemptCount++;\n if (attemptCount \u003c 3) {\n throw new Error('Temporary failure');\n }\n return { success: true };\n });\n });\n\n const runner = new LocalDurableTestRunner({ handlerFunction: testHandler });\n const execution = await runner.run({ payload: {} });\n\n expect(execution.getStatus()).toBe('SUCCEEDED');\n expect(attemptCount).toBe(3);\n\n const step = runner.getOperation('flaky-operation');\n expect(step.getStatus()).toBe(OperationStatus.SUCCEEDED);\n});\n\nit('should fail after max retries', async () => {\n const testHandler = withDurableExecution(async (event, context: DurableContext) => {\n return await context.step(\n 'always-fails',\n async () => {\n throw new Error('Permanent failure');\n },\n {\n retryStrategy: createRetryStrategy({ maxAttempts: 3 })\n }\n );\n });\n\n const runner = new LocalDurableTestRunner({ handlerFunction: testHandler });\n const execution = await runner.run({ payload: {} });\n\n expect(execution.getStatus()).toBe('FAILED');\n expect(execution.getError()?.errorMessage).toContain('Permanent failure');\n});\n```\n\n## Testing Concurrent Operations\n\n**TypeScript:**\n\n```typescript\nit('should process items concurrently', async () => {\n const runner = new LocalDurableTestRunner({ handlerFunction: handler });\n \n const execution = await runner.run({ \n payload: { items: [1, 2, 3, 4, 5] } \n });\n\n expect(execution.getStatus()).toBe('SUCCEEDED');\n\n const mapOp = runner.getOperation('process-items');\n expect(mapOp.getType()).toBe(OperationType.MAP);\n\n // Check individual item operations\n const item0 = runner.getOperation('process-0');\n expect(item0.getStatus()).toBe(OperationStatus.SUCCEEDED);\n});\n```\n\n## Cloud Testing\n\nFor integration tests against real Lambda:\n\n**TypeScript:**\n\n```typescript\nimport { CloudDurableTestRunner } from '@aws/durable-execution-sdk-js-testing';\n\ndescribe('Integration Tests', () => {\n it('should execute in real Lambda', async () => {\n const runner = new CloudDurableTestRunner({\n functionName: 'my-durable-function:1', // Qualified ARN required\n client: new LambdaClient({ region: 'us-east-1' })\n });\n\n const execution = await runner.run({\n payload: { userId: '123' },\n config: { pollInterval: 1000 }\n });\n\n expect(execution.getStatus()).toBe('SUCCEEDED');\n \n const step = runner.getOperation('fetch-user');\n expect(step.getStatus()).toBe(OperationStatus.SUCCEEDED);\n });\n});\n```\n\n**Python:**\n\nCloud mode uses `DurableFunctionCloudTestRunner` with the same API:\n\n```bash\n# Set environment variables for cloud mode\nexport AWS_REGION=us-west-2\nexport QUALIFIED_FUNCTION_NAME=\"my-durable-function:$LATEST\"\nexport LAMBDA_FUNCTION_TEST_NAME=\"my_function\"\n\n# Run in cloud mode\npytest --runner-mode=cloud -k test_workflow\n```\n\nThe same test works in both modes:\n\n```python\ndef test_workflow_cloud():\n \"\"\"Test against deployed Lambda function.\"\"\"\n runner = DurableFunctionCloudTestRunner(\n function_name='my-function:$LATEST',\n region='us-west-2'\n )\n \n with runner:\n result = runner.run(input={'user_id': '123'}, timeout=60)\n\n assert result.status is InvocationStatus.SUCCEEDED\n```\n\n## Test Assertions\n\n**TypeScript:**\n\n```typescript\nit('should validate operation details', async () => {\n const runner = new LocalDurableTestRunner({ handlerFunction: handler });\n await runner.run({ payload: {} });\n\n const step = runner.getOperation('process-data');\n \n // Check operation type\n expect(step.getType()).toBe(OperationType.STEP);\n \n // Check status\n expect(step.getStatus()).toBe(OperationStatus.SUCCEEDED);\n \n // Check timing\n expect(step.getStartTimestamp()).toBeDefined();\n expect(step.getEndTimestamp()).toBeDefined();\n \n // Check result\n const stepDetails = step.getStepDetails();\n expect(stepDetails?.result).toEqual({ processed: true });\n});\n```\n\n## Best Practices\n\n1. **Always name operations** for reliable test assertions\n2. **Get operations by name**, never by index\n3. **Test replay behavior** with multiple invocations\n4. **Use fake clock** for time-dependent tests\n5. **Test error scenarios** including retries and failures\n6. **Test callbacks** with success, failure, and timeout cases\n7. **Validate operation details** (type, status, timing, results)\n8. **Use cloud tests** for integration testing\n9. **Mock external dependencies** in unit tests\n10. **Test concurrent operations** individually and as a group\n\n## Common Pitfalls\n\n### ❌ Getting Operations by Index\n\n```typescript\n// Brittle - breaks when operations change\nconst step = runner.getOperationByIndex(0);\n```\n\n### ✅ Getting Operations by Name\n\n```typescript\n// Robust - works even if operation order changes\nconst step = runner.getOperation('fetch-user');\n```\n\n### ❌ Not Waiting for Callbacks\n\n```typescript\n// Race condition - callback might not exist yet\nconst callbackOp = runner.getOperation('wait-approval');\nawait callbackOp.sendCallbackSuccess('{}');\n```\n\n### ✅ Waiting for Callbacks\n\n```typescript\n// Use waitForData with proper status\nimport { WaitingOperationStatus } from '@aws/durable-execution-sdk-js-testing';\n\nconst callbackOp = runner.getOperation('wait-approval');\nawait callbackOp.waitForData(WaitingOperationStatus.STARTED);\nawait callbackOp.sendCallbackSuccess(JSON.stringify({}));\n```\n\n## Common Testing Errors\n\n### TypeScript\n\n| Error | Cause | Solution |\n| ------------------------------------- | ------------------------------------- | ------------------------------------------------- |\n| `'result' is of type 'unknown'` | Missing type casting in tests | Cast result: `as any` or specific type |\n| `'payload' does not exist in type` | Wrong test runner API | Wrap event in `payload: {}` object |\n| `Cannot find operation at index` | Using index for unstable operations | Use `getOperation(\"name\")` instead |\n| Flaky callback tests | Race condition with callback creation | Use `waitForData(WaitingOperationStatus.STARTED)` |\n| `Unexpected token` in callback result | Forgot to JSON.stringify | Always stringify: `JSON.stringify(data)` |\n| Callback result parsing error | Result is JSON string | Parse result: `JSON.parse(result.value)` |\n| Operation not found by name | Missing operation name | Always name operations in handler |\n\n## Jest Configuration\n\n**jest.config.js:**\n\n```javascript\nmodule.exports = {\n preset: 'ts-jest',\n testEnvironment: 'node',\n roots: ['\u003crootDir>/src'],\n testMatch: ['**/*.test.ts'],\n transform: {\n '^.+\\\\.ts

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. Works best with the AWS MCP server but is not required. All AWS interactions in this skill use standard AWS CLI commands that work in any environment with configured AWS credentials. Critical Rules Read these before writing any code. Each one is a constraint that will silently break a function if violated. 1. Durable execution must be enabled at function creation time — it cannot be retrofitted. A new Lambda function…

: 'ts-jest',\n },\n collectCoverageFrom: [\n 'src/**/*.ts',\n '!src/**/*.d.ts',\n ],\n};\n```\n\n**Key points:**\n\n- `preset: 'ts-jest'` is essential for TypeScript support\n- `transform` maps .ts files to ts-jest transformer\n- `testMatch` specifies test file patterns\n- Use `skipTime: true` in test setup for fast execution\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":17784,"content_sha256":"44ddb1a9c2c93650ade80e8d530429ae04848e0d602d8d19d6356c190219f7ed"},{"filename":"references/troubleshooting-executions.md","content":"# Troubleshooting Executions\n\n**PROACTIVE AGENT**: When users report issues with durable function executions, spawn a specialized troubleshooting agent.\n\n## When to Spawn Troubleshooting Agent\n\nSpawn the agent when users mention:\n\n- \"My execution is stuck\"\n- \"Execution failed with ID xyz\"\n- \"Debug execution abc123\"\n- \"Troubleshoot execution\"\n- \"Why is my durable function not completing\"\n- Provide an execution ID and need diagnosis\n\n## Agent Instructions\n\nWhen spawning the troubleshooting agent, provide:\n\n```\nDiagnose durable function execution issue:\n- Durable Execution ARN: \u003cdurable-execution-arn>\n- Region: \u003cregion> (infer from ARN)\n\nCRITICAL SAFETY RULES:\n- This is READ-ONLY diagnosis\n- NEVER call StopDurableExecution or any termination APIs\n- NEVER modify execution state\n- Only suggest manual remediation if user explicitly requests it\n\nSteps:\n0. If the user provides a function name + alias (e.g., my-function:live) instead of a full ARN:\n - Resolve the alias to a version: aws lambda get-alias --function-name \u003cfunctionName> --name \u003calias> --region \u003cregion> --query 'FunctionVersion' --output text\n - List executions for that function: aws lambda list-durable-executions-by-function --function-name \u003cfunctionName>:\u003cversion> --region \u003cregion>\n - Ask the user to identify the execution, or use the most recent one.\n\n1. Fetch the execution history directly:\n Run: aws lambda get-durable-execution-history --durable-execution-arn \u003cdurable-execution-arn> --region \u003cregion> --include-execution-data\n Note: execution data may contain sensitive information (PII, credentials, business data). Do not display raw step results to users without reviewing content first.\n\n2. If the command succeeds, analyze and provide a user-friendly diagnosis:\n a. Report the execution status (RUNNING/SUCCEEDED/FAILED/STOPPED/TIMED_OUT)\n b. Identify the root cause by looking for these key events in the history:\n\n **Execution-level failures:**\n - `ExecutionFailed` — entire execution crashed; extract the error and cause fields\n - `ExecutionTimedOut` — the execution exceeded its configured timeout\n - `ExecutionStopped` — execution was manually stopped via StopDurableExecution\n\n **Context and step failures:**\n - `ContextFailed` — a child context threw an unhandled error; check the parent context for what triggered it\n - `StepFailed` — an individual step failed; includes RetryDetails (CurrentAttempt, NextAttemptDelaySeconds) showing retry state\n\n **Callback issues:**\n - `CallbackStarted` with a Timeout field — confirms a timeout was registered; correlate with any subsequent `CallbackTimedOut`\n - `CallbackTimedOut` — a timeout fired but may not have been caught by the function code\n - `CallbackFailed` — the callback was resolved with an error\n\n **Chained invocation failures:**\n - `ChainedInvokeFailed` — a chained (child) durable execution failed\n - `ChainedInvokeTimedOut` — a chained execution exceeded its timeout\n - `ChainedInvokeStopped` — a chained execution was stopped\n\n **Other signals:**\n - `WaitCancelled` — a scheduled wait was cancelled before completing\n - `InvocationCompleted` with an Error field — the Lambda invocation itself errored (e.g., runtime crash)\n\n **Diagnosis patterns:**\n - Failed operations: Show the EXACT error message verbatim in a code block\n - Stuck in WAIT_FOR_CALLBACK: Extract callback ID, show how long it's been waiting\n - Timeout: Show which operation was running when timeout occurred\n - Unexpected behavior: Compare operation order with expected flow\n c. Calculate operation durations and timeline\n d. Provide a clear, plain-language explanation of what went wrong and why\n\n3. If the command fails:\n - 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.\n - Permissions/network error: check that your caller identity has lambda:GetDurableExecutionHistory on the function ARN.\n - In either case, direct them to the console as a fallback (see step 4)\n\n4. ALWAYS provide a direct link to the Execution Details page in the Lambda console.\n Parse the ARN (arn:\u003cpartition>:lambda:\u003cregion>:\u003caccountId>:function:\u003cfunctionName>:\u003cfunctionVersion>/durable-execution/\u003cexecutionName>/\u003cinvocationId>)\n to extract region, functionName, functionVersion, executionName, and invocationId, then construct:\n https://\u003cregion>.console.aws.amazon.com/lambda/home?region=\u003cregion>#/functions/\u003cfunctionName>/versions/\u003cfunctionVersion>/executions/\u003cexecutionName>/\u003cinvocationId>\n\n Frame it as: \"**[View this execution in the console](\u003curl>)**\"\n\n5. Provide specific, actionable next steps based on the diagnosis.\n\n6. If unable to determine the root cause from execution history:\n - Provide the console link (step 4)\n - Offer to fetch the log group and pull relevant logs:\n a. Get the log group:\n aws lambda get-function-configuration --function-name \u003cfunctionName>:\u003cfunctionVersion> --region \u003cregion> --query 'LoggingConfig.LogGroup'\n b. Query logs filtered by invocation ID (parsed from the ARN):\n aws logs filter-log-events --log-group-name \u003clogGroup> --region \u003cregion> --filter-pattern '\"\u003cinvocationId>\"'\n c. If the function uses SDK structured logging (context.logger), query for step-level logs.\n Use execution start time from step 2 as start-time, and execution end time (or current time if still running) as end-time:\n QUERY_ID=$(aws logs start-query --log-group-name \u003clogGroup> --region \u003cregion> \\\n --start-time \u003cstartEpoch> --end-time \u003cendEpoch> \\\n --query-string 'fields @timestamp, @message | filter executionName = \"\u003cexecutionName>\" | sort @timestamp asc' \\\n --query 'queryId' --output text)\n Then poll for results:\n aws logs get-query-results --query-id $QUERY_ID --region \u003cregion>\n - If unable to determine the log group, ask the user to check the function's logs\n\nOUTPUT FORMAT:\n- Lead with a short summary: \"Your execution [status] because [reason].\"\n- Show the error message verbatim in a code block if one exists.\n- Explain what this means in plain language.\n- Provide the console link for visual debugging.\n- List recommended next steps.\n\nUse jq for JSON parsing if available, otherwise use --query (AWS CLI built-in JMESPath) or python -m json.tool.\n```\n\n## Example Usage\n\n```\nUser: \"My durable function execution abc-123 is stuck on my-function:live in us-west-2\"\n\nAgent: [Runs get-durable-execution-history command]\nAgent: [Analyzes with jq queries]\n\nClaude: Your execution is **waiting for an external callback** that hasn't arrived yet.\n\n The operation `wait-for-approval` has been pending since 2026-02-14 (3 days).\n It will time out in 12 hours if no callback is received.\n\n Callback ID: `xyz789`\n\n **[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)**\n ```\n https://us-west-2.console.aws.amazon.com/lambda/home?region=us-west-2#/functions/my-function/versions/3/executions/abc-123/inv-456\n ```\n\n **Next steps:**\n 1. Check if the approval email was sent to the recipient\n 2. Verify the callback ID `xyz789` was included in the email\n 3. If you'd like me to help send the callback manually, let me know\n```\n\n```\nUser: \"Execution def-456 failed on order-processor:live in us-east-1\"\n\nAgent: [Runs get-durable-execution-history command]\nAgent: [Analyzes with jq queries]\n\nClaude: Your execution **failed** at the `charge-payment` step after 3 retry attempts.\n\n Error message:\n ```\n PaymentGatewayError: Card declined - insufficient funds (code: card_declined)\n ```\n\n This means the payment provider rejected the charge. The durable function\n retried 3 times with exponential backoff but the error persisted because\n it's a permanent failure (not a transient network issue).\n\n **[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)**\n ```\n https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/order-processor/versions/5/executions/def-456/inv-789\n ```\n\n **Next steps:**\n 1. This is a business logic failure, not an infrastructure issue\n 2. Consider adding a non-retryable error classification for `card_declined`\n 3. Implement a compensation step to release the reserved inventory\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8716,"content_sha256":"a15d78eba1ca6850a63551c0e0dd00ee2c8e438f05b2e4acd2caa226ad4ae3fd"},{"filename":"references/wait-operations.md","content":"# Wait Operations\n\nSuspend execution without compute charges for delays, external callbacks, and polling.\n\n## Simple Waits\n\nPause execution for a duration (no compute charges during wait):\n\n**TypeScript:**\n\n```typescript\nawait context.wait({ seconds: 30 });\nawait context.wait({ minutes: 5 });\nawait context.wait({ hours: 1, minutes: 30 });\nawait context.wait({ days: 7 });\n\n// Named wait (recommended)\nawait context.wait('rate-limit-delay', { seconds: 60 });\n```\n\n**Python:**\n\n```python\nfrom aws_durable_execution_sdk_python.config import Duration\n\ncontext.wait(duration=Duration.from_seconds(30))\ncontext.wait(duration=Duration.from_minutes(5))\ncontext.wait(duration=Duration.from_hours(1))\ncontext.wait(duration=Duration.from_days(7))\n\n# Named wait (recommended)\ncontext.wait(duration=Duration.from_seconds(60), name='rate-limit-delay')\n```\n\n**Max wait duration:** Up to 1 year\n\n## Wait for Callback\n\nWait for external systems to respond (human approval, webhook, async job):\n\n**TypeScript:**\n\n```typescript\nconst result = await context.waitForCallback(\n 'wait-for-approval',\n async (callbackId, ctx) => {\n // Send callback ID to external system\n await sendApprovalEmail(approverEmail, callbackId);\n },\n {\n timeout: { hours: 24 },\n heartbeatTimeout: { minutes: 5 }\n }\n);\n\n// External system calls back with:\n// aws lambda send-durable-execution-callback-success \\\n// --callback-id \u003ccallbackId> \\\n// --result '{\"approved\": true}'\n```\n\n**Python:**\n\n```python\nfrom aws_durable_execution_sdk_python.config import WaitForCallbackConfig\n\n# Wait for external approval\ndef submit_approval(callback_id: str, ctx):\n ctx.logger.info('Sending approval request')\n send_approval_email(approver_email, callback_id)\n\nresult = context.wait_for_callback(\n submitter=submit_approval,\n name='wait-for-approval',\n config=WaitForCallbackConfig(\n timeout=Duration.from_hours(24),\n heartbeat_timeout=Duration.from_minutes(5)\n )\n)\n```\n\n### Callback Success\n\n**CLI:**\n\n```bash\naws lambda send-durable-execution-callback-success \\\n --callback-id \u003ccallbackId> \\\n --result '{\"status\": \"approved\", \"comments\": \"Looks good\"}'\n```\n\n**SDK (TypeScript):**\n\n```typescript\nimport { LambdaClient, SendDurableExecutionCallbackSuccessCommand } from '@aws-sdk/client-lambda';\n\nconst client = new LambdaClient({});\nawait client.send(new SendDurableExecutionCallbackSuccessCommand({\n CallbackId: callbackId,\n Result: JSON.stringify({ status: 'approved' })\n}));\n```\n\n**SDK (Python / boto3):**\n\n```python\nimport boto3\nimport json\n\nlambda_client = boto3.client('lambda')\nlambda_client.send_durable_execution_callback_success(\n CallbackId=callback_id,\n Result=json.dumps({'status': 'approved'})\n)\n```\n\n### Callback Failure\n\n**CLI:**\n\n```bash\naws lambda send-durable-execution-callback-failure \\\n --callback-id \u003ccallbackId> \\\n --error-type \"ApprovalDenied\" \\\n --error-message \"Request denied by approver\"\n```\n\n### Heartbeats\n\nKeep callback alive during long-running external processes:\n\n**TypeScript:**\n\n```typescript\nconst result = await context.waitForCallback(\n 'long-process',\n async (callbackId) => {\n await startLongRunningJob(callbackId);\n },\n {\n timeout: { hours: 24 },\n heartbeatTimeout: { minutes: 5 } // Must receive heartbeat every 5 min\n }\n);\n\n// External system sends heartbeats:\n// aws lambda send-durable-execution-callback-heartbeat --callback-id \u003ccallbackId>\n```\n\n**CLI Heartbeat:**\n\n```bash\naws lambda send-durable-execution-callback-heartbeat \\\n --callback-id \u003ccallbackId>\n```\n\n## Wait for Condition\n\nPoll until a condition is met (job completion, resource availability):\n\n**TypeScript:**\n\n```typescript\nconst finalState = await context.waitForCondition(\n 'wait-for-job',\n async (currentState, ctx) => {\n const status = await checkJobStatus(currentState.jobId);\n return { ...currentState, status };\n },\n {\n initialState: { jobId: 'job-123', status: 'pending' },\n waitStrategy: createWaitStrategy({\n maxAttempts: 60,\n initialDelay: { seconds: 5 },\n maxDelay: { seconds: 30 },\n backoffRate: 1.5,\n shouldContinuePolling: (result) => result.status !== \"completed\"\n }),\n }\n);\n```\n\n**Python:**\n\n```python\n# Note: get_job_status is decorated with @durable_step\nfrom aws_durable_execution_sdk_python.waits import WaitForConditionConfig, create_wait_strategy, WaitStrategyConfig\nfrom aws_durable_execution_sdk_python.config import Duration\n\ndef check_job(state: dict, check_ctx):\n status = get_job_status(state['job_id'])\n return {'job_id': state['job_id'], 'status': status}\n\nwait_strategy = create_wait_strategy(\n WaitStrategyConfig(\n should_continue_polling=lambda state: state['status'] != 'completed',\n max_attempts=60,\n initial_delay=Duration.from_seconds(2),\n max_delay=Duration.from_seconds(60),\n backoff_rate=1.5\n )\n)\n\nresult = context.wait_for_condition(\n check=check_job,\n config=WaitForConditionConfig(\n initial_state={'job_id': 'job-123', 'status': 'pending'},\n wait_strategy=wait_strategy\n ),\n name='wait-for-job'\n)\n```\n\n### Custom Wait Strategy\n\n**TypeScript:**\n\n```typescript\nconst result = await context.waitForCondition(\n 'custom-poll',\n async (state) => {\n const data = await fetchData();\n return { ...state, data, attempts: state.attempts + 1 };\n },\n {\n initialState: { attempts: 0 },\n waitStrategy: (state, attempt) => {\n // Stop after 10 attempts\n if (state.attempts >= 10) {\n return { shouldContinue: false };\n }\n \n // Exponential backoff with max 60s\n return {\n shouldContinue: !state.data?.ready,\n delay: { seconds: Math.min(Math.pow(2, attempt), 60) }\n };\n }\n }\n);\n```\n\n## Callback Patterns\n\n### Human Approval Workflow\n\n**TypeScript:**\n\n```typescript\nexport const handler = withDurableExecution(async (event, context: DurableContext) => {\n const request = await context.step('create-request', async () =>\n createApprovalRequest(event)\n );\n\n const decision = await context.waitForCallback(\n 'wait-approval',\n async (callbackId) => {\n await sendEmail({\n to: event.approver,\n subject: 'Approval Required',\n body: `Approve: ${approvalUrl}?callback=${callbackId}&action=approve\\n` +\n `Reject: ${approvalUrl}?callback=${callbackId}&action=reject`\n });\n },\n { timeout: { hours: 48 } }\n );\n\n if (decision.action === 'approve') {\n await context.step('execute', async () => executeRequest(request));\n return { status: 'approved' };\n }\n \n return { status: 'rejected' };\n});\n```\n\n### Webhook Integration\n\n**TypeScript:**\n\n```typescript\nexport const handler = withDurableExecution(async (event, context: DurableContext) => {\n const order = await context.step('create-order', async () =>\n createOrder(event)\n );\n\n const payment = await context.waitForCallback(\n 'wait-payment',\n async (callbackId) => {\n await paymentProvider.createPayment({\n orderId: order.id,\n amount: order.total,\n webhookUrl: `${webhookUrl}?callback=${callbackId}`\n });\n },\n { timeout: { minutes: 15 } }\n );\n\n if (payment.status === 'success') {\n await context.step('fulfill', async () => fulfillOrder(order));\n }\n \n return { orderId: order.id, paymentStatus: payment.status };\n});\n```\n\n### Async Job Polling\n\n**TypeScript:**\n\n```typescript\nexport const handler = withDurableExecution(async (event, context: DurableContext) => {\n const jobId = await context.step('start-job', async () =>\n startBatchJob(event.data)\n );\n\n const result = await context.waitForCondition(\n 'poll-job',\n async (state) => {\n const job = await getJobStatus(state.jobId);\n return { jobId: state.jobId, status: job.status, result: job.result };\n },\n {\n initialState: { jobId, status: 'running' },\n waitStrategy: createWaitStrategy({\n maxAttempts: 60,\n initialDelay: { seconds: 5 },\n maxDelay: { seconds: 30 },\n backoffRate: 1.5,\n shouldContinuePolling: (result) => result.status === \"running\"\n }),\n }\n );\n\n return result;\n});\n```\n\n## Best Practices\n\n1. **Always name wait operations** for debugging\n2. **Set appropriate timeouts** to prevent indefinite waits\n3. **Use heartbeats** for long-running external processes\n4. **Handle callback failures** explicitly\n5. **Implement exponential backoff** for polling\n6. **Keep check functions lightweight** in waitForCondition\n7. **Store callback IDs securely** when sending to external systems\n8. **Validate callback payloads** before processing\n\n## Error Handling\n\n**TypeScript:**\n\n```typescript\ntry {\n const result = await context.waitForCallback(\n 'wait-approval',\n async (callbackId) => sendApproval(callbackId),\n { timeout: { hours: 24 } }\n );\n} catch (error) {\n if (error instanceof CallbackError) {\n if (error.errorType === 'Timeout') {\n context.logger.warn('Approval timed out');\n // Handle timeout\n } else {\n context.logger.error('Callback failed', error);\n // Handle failure\n }\n }\n}\n```\n\n**Python:**\n\n```python\nfrom aws_durable_execution_sdk_python.exceptions import CallbackError\nfrom aws_durable_execution_sdk_python.config import WaitForCallbackConfig\n\ntry:\n def submit_approval(callback_id: str, ctx):\n send_approval(callback_id)\n\n result = context.wait_for_callback(\n submitter=submit_approval,\n name='wait-approval',\n config=WaitForCallbackConfig(timeout=Duration.from_hours(24))\n )\nexcept CallbackError as error:\n if error.error_type == 'Timeout':\n context.logger.warning('Approval timed out')\n else:\n context.logger.error(f'Callback failed: {error}')\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9729,"content_sha256":"e2a5ad644965968eeddf4819bf13bf469aeefe91dcf865da751f12f3b7406267"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"AWS Lambda durable functions","type":"text"}]},{"type":"paragraph","content":[{"text":"Build resilient multi-step applications and AI workflows that can execute for up to 1 year while maintaining reliable progress despite interruptions.","type":"text"}]},{"type":"paragraph","content":[{"text":"Works best with","type":"text","marks":[{"type":"strong"}]},{"text":" the ","type":"text"},{"text":"AWS MCP server","type":"text","marks":[{"type":"link","attrs":{"href":"https://docs.aws.amazon.com/aws-mcp/","title":null}}]},{"text":" but is not required. All AWS interactions in this skill use standard AWS CLI commands that work in any environment with configured AWS credentials.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Critical Rules","type":"text"}]},{"type":"paragraph","content":[{"text":"Read these before writing any code. Each one is a constraint that will silently break a function if violated.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Durable execution must be enabled at function creation time — it cannot be retrofitted.","type":"text","marks":[{"type":"strong"}]},{"text":" A new Lambda function must be created with durable execution turned on. Migrate the logic into the new function; do not attempt to install the SDK and wrap the handler of the existing function and expect it to work.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Durable functions must be invoked with a qualified ARN","type":"text","marks":[{"type":"strong"}]},{"text":" — a specific version, an alias, or the literal ","type":"text"},{"text":"$LATEST","type":"text","marks":[{"type":"code_inline"}]},{"text":" suffix. An unqualified function name will fail. See the ","type":"text"},{"text":"Invocation Requirements","type":"text","marks":[{"type":"em"}]},{"text":" section below for examples.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Durable operations cannot be nested.","type":"text","marks":[{"type":"strong"}]},{"text":" You cannot call ","type":"text"},{"text":"context.step()","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"context.wait()","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"context.invoke()","type":"text","marks":[{"type":"code_inline"}]},{"text":" from inside another step's callback. Use ","type":"text"},{"text":"context.runInChildContext()","type":"text","marks":[{"type":"code_inline"}]},{"text":" to group operations instead.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"All non-deterministic code must run inside steps.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"Date.now()","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Math.random()","type":"text","marks":[{"type":"code_inline"}]},{"text":", UUID generation, API calls, and database queries outside a step will produce different values on replay and corrupt execution state.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Closure mutations are lost on replay","type":"text","marks":[{"type":"strong"}]},{"text":" - return values from steps","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Side effects outside steps repeat","type":"text","marks":[{"type":"strong"}]},{"text":" - use ","type":"text"},{"text":"context.logger","type":"text","marks":[{"type":"code_inline"}]},{"text":" (replay-aware)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Load Reference Files","type":"text"}]},{"type":"paragraph","content":[{"text":"Load the appropriate reference file based on what the user is working on:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Getting started","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"basic setup","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"example","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"ESLint","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"Jest setup","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"getting-started.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/getting-started.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Understanding replay model","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"determinism","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"non-deterministic errors","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"replay-model-rules.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/replay-model-rules.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Creating steps","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"atomic operations","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"retry logic","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"step-operations.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/step-operations.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Waiting","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"delays","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"callbacks","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"external systems","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"polling","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"wait-operations.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/wait-operations.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Parallel execution","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"map operations","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"batch processing","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"concurrency","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"concurrent-operations.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/concurrent-operations.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Error handling","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"retry strategies","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"saga pattern","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"compensating transactions","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"error-handling.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/error-handling.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Advanced error handling","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"timeout handling","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"circuit breakers","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"conditional retries","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"advanced-error-handling.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/advanced-error-handling.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Testing","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"local testing","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"cloud testing","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"test runner","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"flaky tests","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"testing-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/testing-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deployment","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"CloudFormation","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"CDK","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"SAM","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"log groups","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"deploy","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"infrastructure","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"deployment-iac.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/deployment-iac.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Advanced patterns","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"GenAI agents","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"completion policies","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"step semantics","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"custom serialization","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"advanced-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/advanced-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"troubleshooting","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"stuck execution","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"failed execution","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"debug execution ID","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"execution history","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"execution error","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"why did my execution fail","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"execution timed out","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"callback not received","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"diagnose execution","type":"text","marks":[{"type":"strong"}]},{"text":", or ","type":"text"},{"text":"root cause execution","type":"text","marks":[{"type":"strong"}]},{"text":" -> see ","type":"text"},{"text":"troubleshooting-executions.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/troubleshooting-executions.md","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Basic Handler Pattern","type":"text"}]},{"type":"paragraph","content":[{"text":"TypeScript:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js';\n\nexport const handler = withDurableExecution(async (event, context: DurableContext) => {\n const result = await context.step('process', async () => processData(event));\n return result;\n});","type":"text"}]},{"type":"paragraph","content":[{"text":"Python:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from aws_durable_execution_sdk_python import durable_execution, DurableContext\n\n@durable_execution\ndef handler(event: dict, context: DurableContext) -> dict:\n result = context.step(lambda _: process_data(event), name='process')\n return result","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Python API Differences","type":"text"}]},{"type":"paragraph","content":[{"text":"The Python SDK differs from TypeScript in several key areas:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Steps","type":"text","marks":[{"type":"strong"}]},{"text":": Use ","type":"text"},{"text":"@durable_step","type":"text","marks":[{"type":"code_inline"}]},{"text":" decorator + ","type":"text"},{"text":"context.step(my_step(args))","type":"text","marks":[{"type":"code_inline"}]},{"text":", or inline ","type":"text"},{"text":"context.step(lambda _: ..., name='...')","type":"text","marks":[{"type":"code_inline"}]},{"text":". Prefer the decorator for automatic step naming.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Wait","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"context.wait(duration=Duration.from_seconds(n), name='...')","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exceptions","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"ExecutionError","type":"text","marks":[{"type":"code_inline"}]},{"text":" (permanent), ","type":"text"},{"text":"InvocationError","type":"text","marks":[{"type":"code_inline"}]},{"text":" (transient), ","type":"text"},{"text":"CallbackError","type":"text","marks":[{"type":"code_inline"}]},{"text":" (callback failures)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Testing","type":"text","marks":[{"type":"strong"}]},{"text":": Use ","type":"text"},{"text":"DurableFunctionTestRunner","type":"text","marks":[{"type":"code_inline"}]},{"text":" class directly - instantiate with handler, use context manager, call ","type":"text"},{"text":"run(input=...)","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Invocation Requirements","type":"text"}]},{"type":"paragraph","content":[{"text":"Durable functions ","type":"text"},{"text":"require qualified ARNs","type":"text","marks":[{"type":"strong"}]},{"text":" (version, alias, or ","type":"text"},{"text":"$LATEST","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Valid\naws lambda invoke --function-name my-function:1 output.json\naws lambda invoke --function-name my-function:live output.json\n\n# Invalid - will fail\naws lambda invoke --function-name my-function output.json","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"IAM Permissions","type":"text"}]},{"type":"paragraph","content":[{"text":"Your Lambda execution role MUST have the ","type":"text"},{"text":"AWSLambdaBasicDurableExecutionRolePolicy","type":"text","marks":[{"type":"code_inline"}]},{"text":" managed policy attached. This includes:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"lambda:CheckpointDurableExecution","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Persist execution state","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"lambda:GetDurableExecutionState","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Retrieve execution state","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CloudWatch Logs permissions","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Additional permissions needed for:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Durable invokes","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"lambda:InvokeFunction","type":"text","marks":[{"type":"code_inline"}]},{"text":" on target function ARNs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"External callbacks","type":"text","marks":[{"type":"strong"}]},{"text":": Systems need ","type":"text"},{"text":"lambda:SendDurableExecutionCallbackSuccess","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"lambda:SendDurableExecutionCallbackFailure","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Validation Guidelines","type":"text"}]},{"type":"paragraph","content":[{"text":"When writing or reviewing durable function code, ALWAYS check for these replay model violations:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Non-deterministic code outside steps","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"Date.now()","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Math.random()","type":"text","marks":[{"type":"code_inline"}]},{"text":", UUID generation, API calls, database queries must all be inside steps","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Nested durable operations in step functions","type":"text","marks":[{"type":"strong"}]},{"text":": Cannot call ","type":"text"},{"text":"context.step()","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"context.wait()","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"context.invoke()","type":"text","marks":[{"type":"code_inline"}]},{"text":" inside a step function — use ","type":"text"},{"text":"context.runInChildContext()","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Closure mutations that won't persist","type":"text","marks":[{"type":"strong"}]},{"text":": Variables mutated inside steps are NOT preserved across replays — return values from steps instead","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Side effects outside steps that repeat on replay","type":"text","marks":[{"type":"strong"}]},{"text":": Use ","type":"text"},{"text":"context.logger","type":"text","marks":[{"type":"code_inline"}]},{"text":" for logging (it is replay-aware and deduplicates automatically)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When implementing or modifying tests for durable functions, ALWAYS verify:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"All operations have descriptive names","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tests get operations by NAME, never by index","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Replay behavior is tested with multiple invocations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"LocalDurableTestRunner","type":"text","marks":[{"type":"code_inline"}]},{"text":" for local testing","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Security Considerations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Checkpoint data encryption","type":"text","marks":[{"type":"strong"}]},{"text":": Execution state is persisted automatically. Enable KMS encryption on associated CloudWatch Log Groups to protect checkpointed data at rest.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sensitive data in step results","type":"text","marks":[{"type":"strong"}]},{"text":": Step return values are checkpointed and persisted. Do not return secrets, raw credentials, or PII from steps — store sensitive data in Secrets Manager or SSM Parameter Store and return references instead.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Input validation","type":"text","marks":[{"type":"strong"}]},{"text":": Validate and sanitize event payloads at the handler entry point before passing data to steps.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Credential management","type":"text","marks":[{"type":"strong"}]},{"text":": Retrieve secrets from AWS Secrets Manager or SSM Parameter Store within steps.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Callback payload validation","type":"text","marks":[{"type":"strong"}]},{"text":": Data received via ","type":"text"},{"text":"waitForCallback","type":"text","marks":[{"type":"code_inline"}]},{"text":" originates from external systems — validate and sanitize before processing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Logging","type":"text","marks":[{"type":"strong"}]},{"text":": Avoid ","type":"text"},{"text":"DEBUG","type":"text","marks":[{"type":"code_inline"}]},{"text":" log level in non-development environments as it may expose step results and execution state. Enable CloudWatch Logs encryption with KMS.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Resources","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"AWS Lambda durable functions Documentation","type":"text","marks":[{"type":"link","attrs":{"href":"https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"JavaScript SDK Repository","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/aws/aws-durable-execution-sdk-js","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Python SDK Repository","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/aws/aws-durable-execution-sdk-python","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"IAM Policy Reference","type":"text","marks":[{"type":"link","attrs":{"href":"https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicDurableExecutionRolePolicy.html","title":null}}]}]}]}]}]},"metadata":{"date":"2026-11-02","name":"aws-lambda-durable-functions","author":"@skillopedia","source":{"stars":3376,"repo_name":"agent-toolkit-for-aws","origin_url":"https://github.com/aws/agent-toolkit-for-aws/blob/HEAD/skills/specialized-skills/serverless-skills/aws-lambda-durable-functions/SKILL.md","repo_owner":"aws","body_sha256":"b5acc4ad2d570e835dde4b4582204c32cc62f3d806487cba7f292f5ccb40ac16","cluster_key":"dce909247f862a35499b44ba92c2d138b72185e795a8dcdb1326d97926afc714","clean_bundle":{"format":"clean-skill-bundle-v1","source":"aws/agent-toolkit-for-aws/skills/specialized-skills/serverless-skills/aws-lambda-durable-functions/SKILL.md","text_bytes":115537,"source_commit":"1761e96813ebcbd43ed81d19e837219033f0afe5","skills_sh_rank":3828,"attachment_count":11,"text_attachments":11,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/specialized-skills/serverless-skills/aws-lambda-durable-functions/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"devops-infrastructure","category_label":"DevOps"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"devops-infrastructure","import_tag":"skills-sh-top20k-2026-11","description":"Builds resilient, long-running, multi-step applications with AWS Lambda durable functions with automatic state persistence, retry logic, and orchestration for long-running executions. Covers the critical replay model, step operations, wait/callback patterns, error handling with saga pattern, testing with LocalDurableTestRunner. Triggers on phrases like lambda durable functions, durable execution, workflow orchestration, state machines, retry/checkpoint patterns, long-running stateful Lambda functions, saga pattern, human-in-the-loop callbacks, reliable serverless applications, context.step, context.wait, context.invoke, context.runInChildContext, withDurableExecution, DurableContext, UnrecoverableInvocationError, durable-execution-sdk, qualified ARN invocation, and durable handler replay."}},"renderedAt":1793671243759}

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. Works best with the AWS MCP server but is not required. All AWS interactions in this skill use standard AWS CLI commands that work in any environment with configured AWS credentials. Critical Rules Read these before writing any code. Each one is a constraint that will silently break a function if violated. 1. Durable execution must be enabled at function creation time — it cannot be retrofitted. A new Lambda function…