Skip to content

Development Environment

This page covers the day-to-day workflow for building durable functions: scaffolding a project, writing and testing the function locally, and deploying it. It uses the AWS SAM CLI for the local development loop (sam init to start, sam local invoke to run, and sam deploy to ship) and the AWS CDK when you productionize the surrounding infrastructure. It also covers the AI agent tooling (the Agent Toolkit for AWS) the team ships for building durable functions.

If you just want to deploy your first function with the AWS CLI, start with the Quickstart. This page is about the day-to-day workflow after that.

Development workflow

You develop durable functions in a tight local loop: write the function, write tests, and run them locally before you deploy. Once deployed, you run the same tests against the deployed function to validate packaging and runtime configuration.

flowchart LR
    subgraph dev["Development (Local)"]
        direction LR
        A["1. Write Function"]
        B["2. Write Tests"]
        C["3. Run Tests"]
    end

    subgraph prod["Production (AWS)"]
        direction LR
        D["4. Deploy"]
        E["5. Test in Cloud"]
    end

    A --> B --> C --> D --> E

    style dev fill:#e3f2fd
    style prod fill:#fff3e0

The local runner replays your handler in-process, so you catch bugs in milliseconds instead of waiting on a deploy. See Testing for the full workflow.

Prerequisites

  • An AWS account and the AWS CLI (2.33.22 or later) configured with credentials. Verify with aws sts get-caller-identity.
  • A language runtime (see the tabs below).
  • The AWS SAM CLI (1.153.1 or later), used throughout this guide to scaffold, test, and deploy. Version 1.153.1 is the minimum that recognizes the DurableConfig property. Earlier versions fail at sam validate and sam build with "property DurableConfig not defined". The AWS CDK (2.237.1 or later) is recommended once you productionize the surrounding infrastructure. Direct AWS CLI access also works.

1. Write Function

Scaffold a new durable application with sam init. Its AWS Quick Start templates include durable-function starters for TypeScript, Python, and Java that generate the handler, a template.yaml with DurableConfig already set, a tests folder, and the SDK dependency:

sam init

Choose AWS Quick Start Templates, pick the durable function template, then select your runtime. See Create your application in AWS SAM for the interactive flow, and the Quickstart for the full handler code in each language.

Bundle the SDK with your function code so you control the exact version, rather than relying on the Lambda runtime-provided copy. This gives you control over which version of the SDK is being deployed in your function code, rather than relying on which is being bundled in the lambda runtime.

Requires Node.js 22+.

npm install @aws/durable-execution-sdk-js

Requires Python 3.13+ (3.11 is the minimum the SDK supports; runtimes 3.13+ ship the SDK pre-installed).

pip install aws-durable-execution-sdk-python

Requires Java 17+ and Maven 3.8+. Add to your pom.xml:

<dependency>
    <groupId>software.amazon.lambda.durable</groupId>
    <artifactId>aws-durable-execution-sdk-java</artifactId>
    <version>1.1.0</version>
</dependency>

Requires the .NET 10 SDK.

dotnet add package Amazon.Lambda.DurableExecution

2. Write Tests

Drive your handler with the testing SDK. The runner executes the handler through the same replay-and-checkpoint loop the Lambda service uses, so local behavior matches the cloud. TypeScript, Java, and C# ship a LocalDurableTestRunner; Python uses DurableFunctionTestRunner.

The runner ships in a separate, dev-only testing package. Install it before you write tests (see Authoring tests for the full testing workflow):

npm install --save-dev @aws/durable-execution-sdk-js-testing

The testing package's peer range can lag the runtime SDK. If npm install reports an ERESOLVE error, pin the runtime SDK to a version the peer range allows (for example npm install @aws/durable-execution-sdk-js@2.1.0).

pip install aws-durable-execution-sdk-python-testing

Add the testing dependency to your pom.xml with test scope:

<dependency>
    <groupId>software.amazon.lambda</groupId>
    <artifactId>aws-durable-execution-sdk-java-testing</artifactId>
    <scope>test</scope>
</dependency>
dotnet add package Amazon.Lambda.DurableExecution.Testing

A minimal test creates a runner with your handler, runs it, and asserts on the result:

import { withDurableExecution, DurableContext } from "@aws/durable-execution-sdk-js";
import {
  LocalDurableTestRunner,
} from "@aws/durable-execution-sdk-js-testing";
import { ExecutionStatus } from "@aws-sdk/client-lambda";

const handler = withDurableExecution(async (event: unknown, context: DurableContext) => {
  const result = await context.step("greet", () => "hello");
  return result;
});

let runner: LocalDurableTestRunner;

beforeAll(async () => {
  await LocalDurableTestRunner.setupTestEnvironment();
});

afterAll(async () => {
  await LocalDurableTestRunner.teardownTestEnvironment();
});

beforeEach(() => {
  runner = new LocalDurableTestRunner({ handlerFunction: handler });
});

it("returns the expected result", async () => {
  const result = await runner.run();

  expect(result.getStatus()).toBe(ExecutionStatus.SUCCEEDED);
  expect(result.getResult()).toBe("hello");
});
from aws_durable_execution_sdk_python import DurableContext, durable_execution, durable_step
from aws_durable_execution_sdk_python.types import StepContext
from aws_durable_execution_sdk_python.execution import InvocationStatus
from aws_durable_execution_sdk_python_testing.runner import DurableFunctionTestRunner


@durable_step
def greet(ctx: StepContext) -> str:
    return "hello"


@durable_execution
def handler(event, context: DurableContext) -> str:
    return context.step(greet())


def test_returns_expected_result():
    runner = DurableFunctionTestRunner(handler=handler)
    with runner:
        result = runner.run(timeout=10)

    assert result.status is InvocationStatus.SUCCEEDED
    assert result.result == '"hello"'
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.model.ExecutionStatus;
import software.amazon.lambda.durable.testing.LocalDurableTestRunner;

class MinimalTest {

    @Test
    void returnsExpectedResult() {
        var runner = LocalDurableTestRunner.create(
            Void.class,
            (input, context) -> context.step("greet", String.class, ctx -> "hello")
        );

        var result = runner.runUntilComplete(null);

        assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
        assertEquals("hello", result.getResult(String.class));
    }
}
using Amazon.Lambda.DurableExecution;
using Amazon.Lambda.DurableExecution.Testing;
using Xunit;

public class MinimalTest
{
    private static Task<string> Workflow(object? input, IDurableContext ctx)
        => ctx.StepAsync(async (_, _) => "hello", name: "greet");

    [Fact]
    public async Task ReturnsExpectedResult()
    {
        await using var runner = new DurableTestRunner<object?, string>(
            Workflow,
            new TestRunnerOptions { SkipTime = true });

        TestResult<string> result = await runner.RunAsync(null);

        result.EnsureSucceeded();
        Assert.Equal("hello", result.Result);
    }
}

To drive an input-based handler, pass an event to the runner:

await runner.run({ payload: { orderId: "A-1" } });
runner.run(input='{"orderId": "A-1"}')
runner.runUntilComplete(input);
await runner.RunAsync(input);

3. Run Tests

Run the suite with your language's test runner:

npm test
pytest
mvn test
dotnet test

Get operations by name (never by index) and invoke the runner more than once to assert replay behavior. The Testing section covers installing the testing SDK, authoring tests, assertions, and workflow patterns.

Invoke locally with the SAM CLI

The test runner exercises your handler in-process. To run the packaged function in a local container that matches the Lambda runtime, build it first, then invoke it with the SAM CLI. This needs no deployment and no AWS credentials:

sam build

# Invoke the function; --durable-execution-name names the execution so you can inspect it
sam local invoke MyDurableFunction --durable-execution-name my-test

# Print the execution's checkpoint and operation history (steps, waits, callbacks)
sam local execution history <execution-id>

# Resolve a pending callback so an execution waiting on it resumes
sam local callback succeed <callback-id>

For the full command reference, see SAM CLI.

4. Deploy

You set the DurableConfig when the function is created. Adding it to an existing function triggers a resource replacement, which only succeeds when the function name is not explicitly set in the template; changing values inside an existing DurableConfig does not. See Lambda with durable configuration in the CDK docs for details. Every durable function needs three things, whichever tool you deploy with:

  1. A DurableConfig on the function (ExecutionTimeout is required; RetentionPeriodInDays is optional and defaults to 14 days). See the durable configuration reference.
  2. The AWSLambdaBasicDurableExecutionRolePolicy managed policy on the execution role, which grants the checkpoint permissions.
  3. A qualified ARN (a published version or an alias) to invoke. Durable execution is not supported on an unqualified function name.

Both SAM and CDK can deploy durable functions to production. SAM is great for iterating on a single function: it declares your function configuration, IAM role, version, and alias in a single template.yaml, one place you can check into source control, and pairs with the sam local invoke / sam deploy loop. For more complex systems (composing the function with the rest of your infrastructure, such as queues, tables, alarms, and multi-environment stages), AWS CDK gives you a typed, programmable app. Tune DurableConfig per environment: short timeouts and retention in development, longer values in production.

template.yaml:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  DurableFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: nodejs22.x
      Handler: index.handler
      CodeUri: ./src
      DurableConfig:
        ExecutionTimeout: 3600
        RetentionPeriodInDays: 7
      Policies:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy
      AutoPublishAlias: prod

Outputs:
  AliasArn:
    Value: !Ref DurableFunction.Alias

Deploy:

sam build
sam deploy --guided

AutoPublishAlias gives you the qualified ARN (the prod alias) that durable invocation requires.

sam build prepares your handler before packaging. What it needs depends on the language:

Add an esbuild build method so sam build transpiles TypeScript. Without it, SAM ships the .ts source, which only works for plain JavaScript:

DurableFunction:
  Type: AWS::Serverless::Function
  # Properties as above
  Metadata:
    BuildMethod: esbuild
    BuildProperties:
      Format: cjs
      Target: node22
      EntryPoints:
        - index.ts

No build method is required. sam build installs dependencies from requirements.txt and packages the source.

sam build builds with Maven or Gradle from your pom.xml or build.gradle.

sam build builds with the .NET CLI.

import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';

export class DurableFunctionStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const fn = new lambda.Function(this, 'DurableFunction', {
      runtime: lambda.Runtime.NODEJS_22_X,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      durableConfig: {
        executionTimeout: cdk.Duration.hours(1),
        retentionPeriod: cdk.Duration.days(7),
      },
    });

    // CDK adds the checkpoint permissions automatically when durableConfig is set.
    const alias = new lambda.Alias(this, 'ProdAlias', {
      aliasName: 'prod',
      version: fn.currentVersion,
    });

    new cdk.CfnOutput(this, 'AliasArn', { value: alias.functionArn });
  }
}

Deploy:

cdk deploy

You can also use CloudFormation directly (AWS::Lambda::Function with a DurableConfig property). For durable invokes, callbacks, multi-environment stages, and log-group management, see the deployment guidance in the Agent Toolkit below.

5. Test in Cloud

After deploying, validate using Lambda in the cloud.

  • Run your test suite against the deployed function with the Cloud Runner.

  • Invoke the deployed function with the SAM CLI:

    # Invoke the deployed function in the cloud
    sam remote invoke MyDurableFunction --stack-name my-stack --event '{"name": "world"}'
    
    # Print the deployed execution's checkpoint and operation history
    sam remote execution history <execution-arn>
    
    # Resolve a pending callback (pass the result the waiting execution expects)
    sam remote callback succeed <callback-id> --result '"approved"'
    

    For the full command reference, see SAM CLI.

Agentic development with Kiro

The Agent Toolkit for AWS ships a Lambda durable functions skill that teaches your AI coding assistant the replay model, step and wait patterns, error handling, testing, and IaC for durable functions. It is the fastest way to write correct durable functions with an agent, because it front-loads the determinism rules that are easy to get wrong.

Install it by following the agent setup guide, which covers Kiro, Claude Code, Cursor, GitHub Copilot, Codex, and other agents. Once installed, build workflows from natural language prompts:

Help me create a durable Lambda function that processes orders with retries

The agent loads the relevant guidance and walks through the handler, steps with retry strategies, error handling, tests with the local runner, and deployment. Mentioning keywords such as durable, workflow, saga, agentic, human-in-the-loop, or callback activates the durable functions skill.

For any agent that supports the open agent skills format, you can add the durable functions skill directly:

npx skills add https://github.com/aws/agent-toolkit-for-aws/tree/main/skills/specialized-skills/serverless-skills/aws-lambda-durable-functions --yes --global

Next steps