All skills
aws avatar

/aws-lambda-durable-functions

@eb96852

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.

Use this Skill: https://skilld.dev/gh/aws/agent-toolkit-for-aws/aws-lambda-durable-functions

This session only. Nothing lands on disk.

referencesstep-operations.md

≈2.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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.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)
)

Retryable Error Types

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 Semantics

AtLeastOncePerRetry (default)

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 }
);

AtMostOncePerRetry

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()
    )
)

Custom Serialization

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'
)

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 rethrow

For 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 (AtLeastOncePerRetry vs AtMostOncePerRetry)

Source: SKILL.md on GitHub

1 warning2mo3 checks · Risk SAFE
  • Gen Agent Trust Hub2mo

    This skill provides comprehensive guidance for building and managing AWS Lambda durable functions. It incorporates standard security practices and leverages official AWS SDKs and CLI tools for its operations. While the skill involves package installation and cloud environment interactions, these are aligned with its primary purpose of cloud application development.

  • Socket2mo

    No alerts

  • Snyk2mo

    Risk: MEDIUM · 1 issue

Signed by skilld at eb96852. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 2 months ago
version
1

README badge

README badge for aws/agent-toolkit-for-aws/aws-lambda-durable-functions