Chapter 102 · AWS Lambda Durable Functions
Subchapter 102.8
references/step-operations.mdMarkdown8 KBView on GitHub
Steps are atomic operations with automatic retry and state persistence.
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:
Use lambda for:
TypeScript:
const result = await context.step('fetch-user', async () => {
return await fetchUserFromAPI(userId);
});Best Practice: Always name steps for easier debugging and testing.
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))
)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.no_retry()
if attempt < 3:
return RetryDecision(
should_retry=True,
delay=Duration.from_seconds(2 ** attempt)
)
return RetryDecision.no_retry()
result = context.step(
risky_operation(),
config=StepConfig(retry_strategy=custom_retry)
)TypeScript:
class NetworkError extends Error {
name = 'NetworkError';
}
class TimeoutError extends Error {
name = 'TimeoutError';
}
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 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.
TypeScript:
import { StepSemantics } from '@aws/durable-execution-sdk-js';
const result = await context.step(
'idempotent-operation',
async () => idempotentAPI(),
{ semantics: StepSemantics.AtLeastOncePerRetry }
);Step 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 }).
TypeScript:
import { StepSemantics } from '@aws/durable-execution-sdk-js';
const result = await context.step(
'charge-payment',
async () => chargeCard(amount),
{
semantics: StepSemantics.AtMostOncePerRetry,
retryStrategy: () => ({ shouldRetry: false })
}
);Python:
from aws_durable_execution_sdk_python.config import StepSemantics, StepConfig
result = context.step(
charge_card(amount),
config=StepConfig(
step_semantics=StepSemantics.AT_MOST_ONCE_PER_RETRY,
retry_strategy=lambda error, attempt: RetryDecision.no_retry()
)
)For complex types, provide custom serialization:
TypeScript:
import { createClassSerdesWithDates } from '@aws/durable-execution-sdk-js';
class User {
id: string = '';
name: string = '';
createdAt: Date = new Date();
}
const userSerdes = createClassSerdesWithDates(User, ['createdAt']);
const user = await context.step(
'fetch-user',
async () => {
const user = new User();
user.id = '123';
user.name = 'Alice';
user.createdAt = new Date();
return user;
},
{ 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'
)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));
});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))AtLeastOncePerRetry vs AtMostOncePerRetry)