Skip to content

Serialization

Checkpointed data transformation

The SDK serializes durable operation results to checkpoint storage. When your durable function replays the SDK uses those stored results rather than re-running the code wrapped inside the operation. A SerDes (serializer/deserializer) is the object that serializes the operation result, and deserializes it during replay.

Each SDK ships a default SerDes that handles the most common types. You only need a custom SerDes when the default SerDes cannot handle your data types, or when you need special behavior such as encryption, compression or writing to external storage.

The following example uses no SerDes configuration. The SDK serializes and deserializes the step result automatically.

import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";

interface Order {
  id: string;
  total: string;
}

export const handler = withDurableExecution(
  async (event: unknown, context: DurableContext) => {
    // No SerDes config — the SDK serializes and deserializes the result automatically.
    const order = await context.step("fetch-order", async (): Promise<Order> => {
      return { id: "order-123", total: "99.99" };
    });
    return order;
  },
);
from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)


@durable_step
def fetch_order(ctx: StepContext) -> dict:
    return {"id": "order-123", "total": "99.99"}


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    # No SerDes config — the SDK serializes and deserializes the result automatically.
    order = context.step(fetch_order())
    return order
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;

public class Walkthrough extends DurableHandler<Object, Map<String, String>> {
    @Override
    public Map<String, String> handleRequest(Object input, DurableContext context) {
        // No SerDes config — the SDK serializes and deserializes the result automatically.
        Map<String, String> order = context.step(
            "fetch-order",
            Map.class,
            (StepContext ctx) -> Map.of("id", "order-123", "total", "99.99")
        );
        return order;
    }
}
package main

import (
    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type Order struct {
    ID    string `json:"id"`
    Total string `json:"total"`
}

func handler(ctx durable.Context, _ any) (Order, error) {
    // No serdes option: the SDK serializes and deserializes the result automatically.
    return durable.Step(ctx, "fetch-order", func(durable.StepContext) (Order, error) {
        return Order{ID: "order-123", Total: "99.99"}, nil
    })
}

func main() { durable.Start(handler) }
use aws_durable_execution_sdk as durable;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Order {
    id: String,
    total: String,
}

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<Order, durable::BoxError> {
    let order = ctx
        .step(|_| async {
            Ok(Order {
                id: "order-123".to_owned(),
                total: "99.99".to_owned(),
            })
        })
        .name("fetch-order")
        .await?;
    Ok(order)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class WalkthroughExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<object, Order>(Workflow, input, context);

    private async Task<Order> Workflow(object input, IDurableContext ctx)
    {
        // No serializer configuration on the operation — the SDK serializes and
        // deserializes the result with the ILambdaSerializer registered on
        // ILambdaContext.Serializer.
        Order order = await ctx.StepAsync(
            async (_, _) => new Order("order-123", "99.99"),
            name: "fetch-order");
        return order;
    }
}

public record Order(string Id, string Total);

Round-trip serialization

The SDK round-trips an operation result by serializing and then deserializing it before returning it on the first run. This round trip ensures that the first run returns the same value that replay reconstructs from the operation's checkpoint.

A serializer/deserializer pair should preserve the value across a round trip. deserialize(serialize(value)) should be equivalent to value. If it is not, the round trip will change the operation result.

Lambda handler serialization

The Durable Execution SDK SerDes only applies to durable operation results. It does not affect the final return value of your Lambda handler, which Lambda serializes separately.

Lambda serializes handler return values with JSON.stringify. The same types that work with defaultSerdes work as handler return values.

Lambda serializes handler return values with json.dumps. Types like datetime and Decimal are safe inside steps because ExtendedTypeSerDes handles them, but returning them directly from your handler raises TypeError: Object of type X is not JSON serializable. This is a common error when working with Amazon DynamoDB results. Convert those values to JSON-safe types before returning from the handler.

Lambda serializes handler return values with Jackson. The same JacksonSerDes configuration applies to both step results and handler return values when you use DurableHandler.

The SDK decodes the handler's input and encodes its return value with encoding/json, with HTML escaping disabled. A Serdes set with WithSerdes or ConfigureSerdes does not affect them. It applies only to durable operation results.

The SDK decodes the handler's event and encodes its return value with serde_json, so the event type implements Deserialize and the return type implements Serialize. A serdes set with .serdes(..) does not affect them. It applies only to durable operation results. If serde_json cannot encode the return value, the execution fails with error type Error and the serde_json error message.

The registered ILambdaSerializer serializes everything: durable operation results and the handler's final return value. There is no separate serializer for operation results, so the same configuration always applies to both.

Default serialization

Each SDK uses a default SerDes when you do not provide one.

The default is defaultSerdes, which uses JSON.stringify to serialize and JSON.parse to deserialize. It handles any value that JSON.stringify accepts.

The default is ExtendedTypeSerDes. It uses plain JSON for primitives (None, str, int, float, bool, and lists of primitives) and an envelope format for everything else. The envelope format preserves the exact Python type through the round-trip.

Supported types beyond primitives: datetime, date, Decimal, UUID, bytes, bytearray, memoryview, tuple, list, dict, and BatchResult.

The default is JacksonSerDes, which uses Jackson's ObjectMapper. It supports Java 8 time types, serializes dates as ISO-8601 strings, and ignores unknown properties during deserialization.

Pass a custom ObjectMapper to the JacksonSerDes constructor to override the default configuration.

The default is JSONSerdes, which encodes with encoding/json and HTML escaping disabled, so <, >, and & stay literal in the stored JSON. Map keys are sorted and a nil value encodes as null. It handles any value encoding/json accepts. A type that does not round-trip through encoding/json needs a custom Serdes.

The default is durable::JsonSerdes, which encodes compact JSON with serde_json. It handles any type that implements Serialize and DeserializeOwned, and the compiler rejects an operation whose result type does not. Serde attributes such as #[serde(rename = "...")] apply. The batch result of a map or parallel operation defaults to durable::PassthroughSerdes instead, as the map section below shows.

There is no per-operation default SerDes. Register a single ILambdaSerializer at the host boundary and the SDK uses it for every durable operation result.

Use DefaultLambdaJsonSerializer (from Amazon.Lambda.Serialization.SystemTextJson) for reflection-based serialization. For AOT or trim-friendly functions, use SourceGeneratorLambdaJsonSerializer<TContext> and register your JsonSerializerContext.

SerDes interface definition

import { Serdes, SerdesContext } from "@aws/durable-execution-sdk-js";

// Serdes<T> interface
interface SerdesInterface<T> {
  serialize: (value: T | undefined, context: SerdesContext) => Promise<string | undefined>;
  deserialize: (data: string | undefined, context: SerdesContext) => Promise<T | undefined>;
}

// SerdesContext
interface SerdesContextShape {
  entityId: string;
  durableExecutionArn: string;
}

Parameters:

  • serialize An async function that receives the value and a SerdesContext, and returns Promise<string | undefined>.
  • deserialize An async function that receives the serialized string and a SerdesContext, and returns Promise<T | undefined>.

Both methods are async so that implementations can interact with external services such as S3 or KMS.

SerdesContext fields:

  • entityId The operation ID for the current step or operation.
  • durableExecutionArn The ARN of the current durable execution.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Generic, TypeVar

T = TypeVar("T")


@dataclass(frozen=True)
class SerDesContext:
    operation_id: str = ""
    durable_execution_arn: str = ""


class SerDes(ABC, Generic[T]):
    @abstractmethod
    def serialize(self, value: T, serdes_context: SerDesContext) -> str: ...

    @abstractmethod
    def deserialize(self, data: str, serdes_context: SerDesContext) -> T: ...

Parameters:

  • serialize(value, serdes_context) Converts the value to a string.
  • deserialize(data, serdes_context) Converts the string back to the original type.

SerDesContext fields:

  • operation_id The operation ID for the current step or operation.
  • durable_execution_arn The ARN of the current durable execution.
import software.amazon.lambda.durable.TypeToken;
import software.amazon.lambda.durable.serde.SerDes;

// SerDes interface
interface SerDesInterface {
    String serialize(Object value);

    <T> T deserialize(String data, TypeToken<T> typeToken);
}

Parameters:

  • serialize(Object value) Converts the value to a JSON string. Returns null if value is null.
  • deserialize(String data, TypeToken<T> typeToken) Converts the JSON string back to type T. Returns null if data is null.

Use TypeToken<T> to capture generic type information that Java erases at runtime. For example: new TypeToken<List<String>>() {}.

The interface is Serdes. Its methods are Marshal and Unmarshal, which work with byte slices and take a context.Context for external calls.

type Serdes interface {
    Marshal(ctx context.Context, meta SerdesContext, v any) ([]byte, error)
    Unmarshal(ctx context.Context, meta SerdesContext, data []byte, v any) error
}

type SerdesContext struct {
    // OperationID is the positional ID of the operation being serialized
    // (e.g., "1", "1-2-3").
    OperationID string

    // DurableExecutionArn is the ARN of the current durable execution.
    DurableExecutionArn string
    // Has unexported fields.
}

Methods:

  • Marshal(ctx, meta, v) Converts the value to bytes.
  • Unmarshal(ctx, meta, data, v) Fills the pointer v from the bytes.

SerdesContext fields:

  • OperationID The operation ID for the current step or operation.
  • DurableExecutionArn The ARN of the current durable execution.

The interface takes any, so one Serdes serves every result type. For a serdes written for one type, use SerdesOf. A serdes failure is permanent by default. Return RetryableSerdesError to make it transient.

The trait is durable::Serdes<T>, where T is the operation's result type. Both methods return a future, so an implementation can call external services such as Amazon S3 or AWS KMS. A method that does no async work can return std::future::ready(..). The SDK serializes and deserializes step, child-context, with_retry, map item, and parallel branch results and wait-for-condition state. It only deserializes invoke results and callback payloads.

pub trait Serdes<T>: Send + Sync + 'static {
    fn serialize(
        &self,
        value: T,
        context: SerdesContext,
    ) -> impl Future<Output = Result<String, BoxError>> + Send;

    fn deserialize(
        &self,
        wire: String,
        context: SerdesContext,
    ) -> impl Future<Output = Result<T, BoxError>> + Send;
}

// ...

impl SerdesContext {
    // ...
    pub fn operation_id(&self) -> &str;
    // ...
    pub fn durable_execution_arn(&self) -> &str;
    // ...
    pub fn origin(&self) -> PayloadOrigin;
    // ...
}

// ...

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PayloadOrigin {
    SdkRecord,
    External,
}

Methods:

  • serialize(&self, value, context) Converts the value to a string.
  • deserialize(&self, wire, context) Converts the string back to T.

SerdesContext methods:

  • operation_id() The operation ID for the current step or operation.
  • durable_execution_arn() The ARN of the current durable execution.
  • origin() PayloadOrigin::SdkRecord for a value the SDK recorded, or PayloadOrigin::External for a callback payload or invoke result.

A serdes failure is permanent by default. Wrap the error in durable::serdes::RetryableSerdesError to make it transient. To share one serdes between operations, wrap it in an Arc, which implements Serdes<T> as well.

.NET has no per-operation SerDes interface. Serialization is controlled by the single ILambdaSerializer registered on ILambdaContext.Serializer.

// .NET has no per-operation SerDes interface. Serialization for every durable
// operation is handled by the single ILambdaSerializer registered on
// ILambdaContext.Serializer. Implement this interface to customize serialization.
public interface ILambdaSerializer
{
    T Deserialize<T>(Stream requestStream);

    void Serialize<T>(T response, Stream responseStream);
}

Methods:

  • Serialize<T>(T response, Stream responseStream) Writes the serialized form of response to responseStream.
  • Deserialize<T>(Stream requestStream) Reads requestStream and returns the deserialized value of type T.

The same ILambdaSerializer handles every durable operation result and the handler's return value.

Custom SerDes example

Implement the SerDes interface when the default cannot handle your types, or when you need special behavior such as encryption or compression.

import { Serdes, SerdesContext } from "@aws/durable-execution-sdk-js";

interface Order {
  id: string;
  total: string;
}

const orderSerdes: Serdes<Order> = {
  serialize: async (value: Order | undefined, _ctx: SerdesContext) => {
    if (value === undefined) return undefined;
    return JSON.stringify(value);
  },
  deserialize: async (data: string | undefined, _ctx: SerdesContext) => {
    if (data === undefined) return undefined;
    return JSON.parse(data) as Order;
  },
};
import json

from aws_durable_execution_sdk_python.serdes import SerDes, SerDesContext


class OrderSerDes(SerDes[dict]):
    def serialize(self, value: dict, ctx: SerDesContext) -> str:
        return json.dumps(value)

    def deserialize(self, data: str, ctx: SerDesContext) -> dict:
        return json.loads(data)
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.lambda.durable.TypeToken;
import software.amazon.lambda.durable.exception.SerDesException;
import software.amazon.lambda.durable.serde.SerDes;

public class OrderSerDes implements SerDes {
    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public String serialize(Object value) {
        try {
            return mapper.writeValueAsString(value);
        } catch (Exception e) {
            throw new SerDesException("Serialization failed", e);
        }
    }

    @Override
    public <T> T deserialize(String data, TypeToken<T> typeToken) {
        try {
            return mapper.readValue(data, mapper.getTypeFactory().constructType(typeToken.getType()));
        } catch (Exception e) {
            throw new SerDesException("Deserialization failed", e);
        }
    }
}

Implement Marshal and Unmarshal on a type. For a serdes that handles one result type, SerdesOf is shorter, as the next example shows.

package main

import (
    "context"
    "encoding/json"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type Order struct {
    ID    string `json:"id"`
    Total string `json:"total"`
}

// orderSerdes implements durable.Serdes for Order results.
type orderSerdes struct{}

func (orderSerdes) Marshal(_ context.Context, _ durable.SerdesContext, v any) ([]byte, error) {
    return json.Marshal(v)
}

func (orderSerdes) Unmarshal(_ context.Context, _ durable.SerdesContext, data []byte, v any) error {
    return json.Unmarshal(data, v)
}

Implement Serdes<T> on a type for the result type T it handles.

use aws_durable_execution_sdk as durable;
use durable::serdes::{Serdes, SerdesContext};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Order {
    id: String,
    total: String,
}

struct OrderSerdes;

impl Serdes<Order> for OrderSerdes {
    fn serialize(
        &self,
        value: Order,
        _context: SerdesContext,
    ) -> impl Future<Output = Result<String, durable::BoxError>> + Send {
        std::future::ready(serde_json::to_string(&value).map_err(Into::into))
    }

    fn deserialize(
        &self,
        wire: String,
        _context: SerdesContext,
    ) -> impl Future<Output = Result<Order, durable::BoxError>> + Send {
        std::future::ready(serde_json::from_str(&wire).map_err(Into::into))
    }
}

Implement ILambdaSerializer and register it at the host boundary. The custom serializer applies to every durable operation result, not a single operation.

using System.Text.Json;
using Amazon.Lambda.Core;

// .NET has no per-operation SerDes. Serialization is controlled by the single
// ILambdaSerializer registered on ILambdaContext.Serializer. To customize how
// durable operation results are serialized, implement ILambdaSerializer and
// register it at the host boundary instead of passing a SerDes per operation.
public class OrderLambdaSerializer : ILambdaSerializer
{
    private static readonly JsonSerializerOptions Options = new()
    {
        PropertyNameCaseInsensitive = true,
    };

    public T Deserialize<T>(Stream requestStream)
    {
        using var reader = new StreamReader(requestStream);
        string json = reader.ReadToEnd();
        return JsonSerializer.Deserialize<T>(json, Options)!;
    }

    public void Serialize<T>(T response, Stream responseStream)
    {
        string json = JsonSerializer.Serialize(response, Options);
        using var writer = new StreamWriter(responseStream, leaveOpen: true);
        writer.Write(json);
        writer.Flush();
    }
}

Custom SerDes on durable operations

Pass a SerDes instance in the configuration object for the operation you want to customize. The SDK uses that SerDes for that operation only. Other operations in the same handler continue to use the default.

StepConfig

import {
  DurableContext,
  Serdes,
  SerdesContext,
  StepConfig,
  withDurableExecution,
} from "@aws/durable-execution-sdk-js";

interface Order {
  id: string;
  total: string;
}

const orderSerdes: Serdes<Order> = {
  serialize: async (value: Order | undefined, _ctx: SerdesContext) =>
    value !== undefined ? JSON.stringify(value) : undefined,
  deserialize: async (data: string | undefined, _ctx: SerdesContext) =>
    data !== undefined ? (JSON.parse(data) as Order) : undefined,
};

const stepConfig: StepConfig<Order> = { serdes: orderSerdes };

export const handler = withDurableExecution(
  async (event: unknown, context: DurableContext) => {
    const order = await context.step(
      "fetch-order",
      async (): Promise<Order> => ({ id: "order-123", total: "99.99" }),
      stepConfig,
    );
    return order;
  },
);
import json

from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)
from aws_durable_execution_sdk_python.config import StepConfig
from aws_durable_execution_sdk_python.serdes import SerDes, SerDesContext


class OrderSerDes(SerDes[dict]):
    def serialize(self, value: dict, ctx: SerDesContext) -> str:
        return json.dumps(value)

    def deserialize(self, data: str, ctx: SerDesContext) -> dict:
        return json.loads(data)


@durable_step
def fetch_order(ctx: StepContext) -> dict:
    return {"id": "order-123", "total": "99.99"}


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    order = context.step(
        fetch_order(),
        config=StepConfig(serdes=OrderSerDes()),
    )
    return order
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.serde.JacksonSerDes;

public class StepConfigExample extends DurableHandler<Object, Map<String, String>> {
    @Override
    public Map<String, String> handleRequest(Object input, DurableContext context) {
        StepConfig config = StepConfig.builder()
            .serDes(new JacksonSerDes())
            .build();

        Map<String, String> order = context.step(
            "fetch-order",
            Map.class,
            (StepContext ctx) -> Map.of("id", "order-123", "total", "99.99"),
            config
        );
        return order;
    }
}

Pass WithStepSerdes to override the serializer for one step's result. Other operations keep the default. The other per-operation options are WithChildSerdes, WithInvokePayloadSerdes, WithInvokeResultSerdes, and WithTypedBranchSerdes. A per-operation option takes precedence over the handler default set with WithSerdes or ConfigureSerdes.

package main

import (
    "context"
    "encoding/json"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type Order struct {
    ID    string `json:"id"`
    Total string `json:"total"`
}

var orderSerdes = durable.SerdesOf(
    func(_ context.Context, _ durable.SerdesContext, o Order) ([]byte, error) {
        return json.Marshal(o)
    },
    func(_ context.Context, _ durable.SerdesContext, data []byte) (Order, error) {
        var o Order
        err := json.Unmarshal(data, &o)
        return o, err
    },
)

func handler(ctx durable.Context, _ any) (Order, error) {
    return durable.Step(ctx, "fetch-order", func(durable.StepContext) (Order, error) {
        return Order{ID: "order-123", Total: "99.99"}, nil
    }, durable.WithStepSerdes(orderSerdes))
}

func main() { durable.Start(handler) }

Pass a serdes to the step's .serdes(..). Other operations keep the default. The serdes must implement Serdes<O> for the step's result type O. The builders of child contexts, with_retry, invoke, callbacks, wait for callback, wait for condition, map, and parallel have the same .serdes(..) method. An invoke also has .payload_serdes(..) for its input, which takes a durable::PayloadSerializer. JsonSerdes is the default payload serializer.

use aws_durable_execution_sdk as durable;
use durable::serdes::{Serdes, SerdesContext};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Order {
    id: String,
    total: String,
}

struct OrderSerdes;

impl Serdes<Order> for OrderSerdes {
    fn serialize(
        &self,
        value: Order,
        _context: SerdesContext,
    ) -> impl Future<Output = Result<String, durable::BoxError>> + Send {
        std::future::ready(serde_json::to_string(&value).map_err(Into::into))
    }

    fn deserialize(
        &self,
        wire: String,
        _context: SerdesContext,
    ) -> impl Future<Output = Result<Order, durable::BoxError>> + Send {
        std::future::ready(serde_json::from_str(&wire).map_err(Into::into))
    }
}

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<Order, durable::BoxError> {
    let order = ctx
        .step(|_| async {
            Ok(Order {
                id: "order-123".to_owned(),
                total: "99.99".to_owned(),
            })
        })
        .name("fetch-order")
        .serdes(OrderSerdes)
        .await?;
    Ok(order)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}

StepConfig does not expose a serializer. The step result is serialized with the ILambdaSerializer registered at the host boundary. Register a custom ILambdaSerializer there to change how step results are serialized.

using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class StepConfigExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<object, Order>(Workflow, input, context);

    private async Task<Order> Workflow(object input, IDurableContext ctx)
    {
        // StepConfig has no serializer slot. The step result is serialized with
        // the ILambdaSerializer registered on ILambdaContext.Serializer. To
        // customize serialization, register a custom ILambdaSerializer at the
        // host boundary instead of setting a per-step SerDes.
        var config = new StepConfig
        {
            RetryStrategy = RetryStrategy.Exponential(maxAttempts: 3),
        };

        Order order = await ctx.StepAsync(
            async (_, _) => new Order("order-123", "99.99"),
            name: "fetch-order",
            config: config);
        return order;
    }
}

public record Order(string Id, string Total);

CallbackConfig

The callback SerDes controls how the SDK deserializes the payload that an external system sends when it completes the callback.

In TypeScript, the callback SerDes only needs deserialize. The serialize method is not used because the external system provides the payload directly.

import {
  DurableContext,
  Serdes,
  SerdesContext,
  withDurableExecution,
} from "@aws/durable-execution-sdk-js";

interface ApprovalResult {
  approved: boolean;
  reason: string;
}

// Callbacks only need deserialization — the external system sends the payload.
const approvalSerdes: Omit<Serdes<ApprovalResult>, "serialize"> = {
  deserialize: async (data: string | undefined, _ctx: SerdesContext) =>
    data !== undefined ? (JSON.parse(data) as ApprovalResult) : undefined,
};

export const handler = withDurableExecution(
  async (event: unknown, context: DurableContext) => {
    const [approval, callbackId] = await context.createCallback("await-approval", {
      serdes: approvalSerdes,
    });

    // Send callbackId to the external system here.
    console.log("Callback ID:", callbackId);

    const result = await approval;
    return result;
  },
);
import json

from aws_durable_execution_sdk_python import (
    DurableContext,
    durable_execution,
)
from aws_durable_execution_sdk_python.config import CallbackConfig
from aws_durable_execution_sdk_python.serdes import SerDes, SerDesContext


class ApprovalSerDes(SerDes[dict]):
    def serialize(self, value: dict, ctx: SerDesContext) -> str:
        return json.dumps(value)

    def deserialize(self, data: str, ctx: SerDesContext) -> dict:
        return json.loads(data)


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    callback = context.create_callback(
        "await-approval",
        config=CallbackConfig(serdes=ApprovalSerDes()),
    )

    # Send callback.callback_id to the external system here.
    result = callback.result()
    return result
import java.util.Map;
import software.amazon.lambda.durable.DurableCallbackFuture;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.config.CallbackConfig;
import software.amazon.lambda.durable.serde.JacksonSerDes;

public class CallbackConfigExample extends DurableHandler<Object, Map<String, Object>> {
    @Override
    public Map<String, Object> handleRequest(Object input, DurableContext context) {
        CallbackConfig config = CallbackConfig.builder()
            .serDes(new JacksonSerDes())
            .build();

        DurableCallbackFuture<Map<String, Object>> callback =
            context.createCallback("await-approval", Map.class, config);

        // Send callback.getCallbackId() to the external system here.
        return callback.get();
    }
}

The default callback deserializer is RawSerdes, which returns the submitted bytes unchanged. So by default the result type must be string, []byte, or json.RawMessage. Any other type fails with a *SerdesError. Pass WithCallbackSerdes(JSONSerdes) to decode a JSON payload into a struct.

For a callback payload, the per-operation WithCallbackSerdes takes precedence over the handler-wide Deserializer. When neither is set, the payload passes through RawSerdes. Set the handler-wide Deserializer with WithCallbackDeserializer, or from inside the handler with the CallbackDeserializer field of ConfigureSerdes. A Deserializer has one method, Unmarshal(data []byte, v any) error. The handler-level WithSerdes does not apply to callback payloads. WaitForCallback accepts the same WithCallbackSerdes option and decodes the payload once, at the WaitForCallback operation.

package main

import (
    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type ApprovalResult struct {
    Approved bool   `json:"approved"`
    Reason   string `json:"reason"`
}

func handler(ctx durable.Context, _ any) (ApprovalResult, error) {
    // JSONSerdes decodes the payload the external system sends into the struct.
    approval, err := durable.CreateCallback[ApprovalResult](ctx, "await-approval",
        durable.WithCallbackSerdes(durable.JSONSerdes))
    if err != nil {
        return ApprovalResult{}, err
    }

    // Send approval.ID() to the external system here.
    ctx.Logger().Info("Callback ID", "callbackId", approval.ID())

    return approval.Result(ctx)
}

func main() { durable.Start(handler) }

Pass the serdes to the callback builder's .serdes(..). The SDK calls only its deserialize, which decodes the payload as JSON in this example. The trait requires serialize too, but the SDK never calls it for a callback. Without .serdes(..), the default JsonSerdes decodes the payload as JSON into the result type. To receive the payload unchanged, use .serdes(PassthroughSerdes) with create_callback::<String>().

use aws_durable_execution_sdk as durable;
use durable::serdes::{Serdes, SerdesContext};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct ApprovalResult {
    approved: bool,
    reason: String,
}

struct ApprovalSerdes;

impl Serdes<ApprovalResult> for ApprovalSerdes {
    fn serialize(
        &self,
        value: ApprovalResult,
        _context: SerdesContext,
    ) -> impl Future<Output = Result<String, durable::BoxError>> + Send {
        std::future::ready(serde_json::to_string(&value).map_err(Into::into))
    }

    fn deserialize(
        &self,
        wire: String,
        _context: SerdesContext,
    ) -> impl Future<Output = Result<ApprovalResult, durable::BoxError>> + Send {
        std::future::ready(serde_json::from_str(&wire).map_err(Into::into))
    }
}

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<ApprovalResult, durable::BoxError> {
    let approval = ctx
        .create_callback::<ApprovalResult>()
        .name("await-approval")
        .serdes(ApprovalSerdes)
        .await?;

    // Send the callback ID to the external system here.
    tracing::info!("Callback ID: {}", approval.id());

    let result = approval.result().await?;
    Ok(result)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}

CallbackConfig does not expose a serializer. The payload the external system delivers is deserialized with the ILambdaSerializer registered at the host boundary. Register a custom ILambdaSerializer there to change how the callback payload is deserialized.

using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class CallbackConfigExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<object, ApprovalResult>(Workflow, input, context);

    private async Task<ApprovalResult> Workflow(object input, IDurableContext ctx)
    {
        // CallbackConfig has no serializer slot. The callback payload delivered by
        // the external system is deserialized with the ILambdaSerializer registered
        // on ILambdaContext.Serializer. To customize deserialization, register a
        // custom ILambdaSerializer at the host boundary.
        var config = new CallbackConfig
        {
            Timeout = TimeSpan.FromHours(1),
        };

        ICallback<ApprovalResult> callback =
            await ctx.CreateCallbackAsync<ApprovalResult>("await-approval", config);

        // Send callback.CallbackId to the external system here.
        return await callback.GetResultAsync();
    }
}

public record ApprovalResult(bool Approved, string Reason);

MapConfig & ParallelConfig

Map and parallel perations support two SerDes fields that apply at different levels.

import {
  BatchResult,
  DurableContext,
  MapConfig,
  Serdes,
  SerdesContext,
  withDurableExecution,
} from "@aws/durable-execution-sdk-js";

interface ProcessedItem {
  id: string;
  status: string;
}

const itemSerdes: Serdes<ProcessedItem> = {
  serialize: async (value: ProcessedItem | undefined, _ctx: SerdesContext) =>
    value !== undefined ? JSON.stringify(value) : undefined,
  deserialize: async (data: string | undefined, _ctx: SerdesContext) =>
    data !== undefined ? (JSON.parse(data) as ProcessedItem) : undefined,
};

const mapConfig: MapConfig<string, ProcessedItem> = {
  itemSerdes,
};

export const handler = withDurableExecution(
  async (event: unknown, context: DurableContext) => {
    const items = ["a", "b", "c"];
    const result: BatchResult<ProcessedItem> = await context.map(
      "process-items",
      items,
      async (_ctx, item) => ({ id: item, status: "done" }),
      mapConfig,
    );
    return result.getResults();
  },
);
  • itemSerdes serializes each item result.
  • serdes serializes the aggregated BatchResult.
  • ParallelConfig has the same two fields.
import json

from aws_durable_execution_sdk_python import (
    DurableContext,
    durable_execution,
)
from aws_durable_execution_sdk_python.config import MapConfig
from aws_durable_execution_sdk_python.serdes import SerDes, SerDesContext


class ItemSerDes(SerDes[dict]):
    def serialize(self, value: dict, ctx: SerDesContext) -> str:
        return json.dumps(value)

    def deserialize(self, data: str, ctx: SerDesContext) -> dict:
        return json.loads(data)


@durable_execution
def handler(event: dict, context: DurableContext) -> list:
    items = ["a", "b", "c"]
    result = context.map(
        items,
        lambda ctx, item, idx, arr: {"id": item, "status": "done"},
        config=MapConfig(item_serdes=ItemSerDes()),
    )
    return result.get_results()
  • item_serdes serializes each item result.
  • serdes serializes the aggregated BatchResult.
  • When you provide only serdes, the SDK uses it for both for backward compatibility. ParallelConfig has the same two fields.
import java.util.List;
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.config.MapConfig;
import software.amazon.lambda.durable.model.MapResult;
import software.amazon.lambda.durable.serde.JacksonSerDes;

public class MapConfigExample extends DurableHandler<Object, List<Map<String, String>>> {
    @Override
    public List<Map<String, String>> handleRequest(Object input, DurableContext context) {
        MapConfig config = MapConfig.builder()
            .serDes(new JacksonSerDes())
            .build();

        List<String> items = List.of("a", "b", "c");
        MapResult<Map<String, String>> result = context.map(
            "process-items",
            items,
            Map.class,
            (item, index, ctx) -> Map.of("id", item, "status", "done"),
            config
        );
        return result.succeeded();
    }
}
  • serDes applies to each item result.
  • Java ParallelConfig does not have a serDes field.

Two batch options apply to both Map and Parallel:

  • WithBatchSerdes serializes each item result. Default: the handler serdes.
  • WithBatchResultSerdes serializes the aggregated BatchResult. Without it, the SDK stores the BatchResult through the item serdes.
package main

import (
    "context"
    "encoding/json"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type ProcessedItem struct {
    ID     string `json:"id"`
    Status string `json:"status"`
}

var itemSerdes = durable.SerdesOf(
    func(_ context.Context, _ durable.SerdesContext, p ProcessedItem) ([]byte, error) {
        return json.Marshal(p)
    },
    func(_ context.Context, _ durable.SerdesContext, data []byte) (ProcessedItem, error) {
        var p ProcessedItem
        err := json.Unmarshal(data, &p)
        return p, err
    },
)

func handler(ctx durable.Context, _ any) ([]ProcessedItem, error) {
    items := []string{"a", "b", "c"}
    result, err := durable.Map(ctx, "process-items", items,
        func(_ durable.Context, item string, _ int) (ProcessedItem, error) {
            return ProcessedItem{ID: item, Status: "done"}, nil
        },
        durable.WithBatchSerdes(itemSerdes),
    )
    if err != nil {
        return nil, err
    }
    return result.Results(), nil
}

func main() { durable.Start(handler) }
use aws_durable_execution_sdk as durable;
use durable::serdes::{Serdes, SerdesContext};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct ProcessedItem {
    id: String,
    status: String,
}

struct ItemSerdes;

impl Serdes<ProcessedItem> for ItemSerdes {
    fn serialize(
        &self,
        value: ProcessedItem,
        _context: SerdesContext,
    ) -> impl Future<Output = Result<String, durable::BoxError>> + Send {
        std::future::ready(serde_json::to_string(&value).map_err(Into::into))
    }

    fn deserialize(
        &self,
        wire: String,
        _context: SerdesContext,
    ) -> impl Future<Output = Result<ProcessedItem, durable::BoxError>> + Send {
        std::future::ready(serde_json::from_str(&wire).map_err(Into::into))
    }
}

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<Vec<ProcessedItem>, durable::BoxError> {
    let items = ["a", "b", "c"];
    let results = ctx
        .map(items, |_ctx, item, _index| async move {
            Ok(ProcessedItem {
                id: item.to_owned(),
                status: "done".to_owned(),
            })
        })
        .name("process-items")
        .serdes(ItemSerdes)
        .await?;
    Ok(results)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}
  • .serdes(..) serializes each item result. Default: JsonSerdes.
  • .result_serdes(..) serializes the aggregated batch result. It is a Serdes<String> over the batch record's JSON text, and its deserialize must return the exact text its serialize received. Default: PassthroughSerdes, which stores the text unchanged.
  • The parallel builder has the same two methods.

MapConfig does not expose a serializer, and there is no separate item-level serializer. Each item result is serialized with the ILambdaSerializer registered at the host boundary. ParallelConfig likewise has no serializer field.

using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class MapConfigExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<object, IReadOnlyList<ProcessedItem>>(Workflow, input, context);

    private async Task<IReadOnlyList<ProcessedItem>> Workflow(object input, IDurableContext ctx)
    {
        // MapConfig has no serializer slot. Each item result is serialized with the
        // ILambdaSerializer registered on ILambdaContext.Serializer. To customize
        // serialization, register a custom ILambdaSerializer at the host boundary.
        var config = new MapConfig<string>
        {
            MaxConcurrency = 3,
        };

        var items = new[] { "a", "b", "c" };
        IBatchResult<ProcessedItem> result = await ctx.MapAsync(
            items,
            async (itemCtx, item, index, all, ct) =>
                await itemCtx.StepAsync(
                    async (_, _) => new ProcessedItem(item, "done"),
                    name: $"process-{index}"),
            name: "process-items",
            config: config);
        return result.GetResults();
    }
}

public record ProcessedItem(string Id, string Status);

Built-in SerDes helpers

createClassSerdes(cls) creates a Serdes<T> that preserves class instances through the round-trip. It deserializes by calling Object.assign(new cls(), JSON.parse(data)), so class methods are available on the deserialized value. The constructor must take no required parameters. Private fields and getters are not preserved.

import {
  DurableContext,
  createClassSerdes,
  withDurableExecution,
} from "@aws/durable-execution-sdk-js";

class Order {
  id: string = "";
  total: string = "";

  label() {
    return `Order ${this.id} — ${this.total}`;
  }
}

const orderSerdes = createClassSerdes(Order);

export const handler = withDurableExecution(
  async (event: unknown, context: DurableContext) => {
    const order = await context.step(
      "fetch-order",
      async () => Object.assign(new Order(), { id: "order-123", total: "99.99" }),
      { serdes: orderSerdes },
    );
    // order.label() works — class methods are preserved after deserialization
    return order.label();
  },
);

PassThroughSerDes stores the value as-is (the value must already be a string). JsonSerDes uses json.dumps and json.loads without the envelope format that ExtendedTypeSerDes adds for complex types.

from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)
from aws_durable_execution_sdk_python.config import StepConfig
from aws_durable_execution_sdk_python.serdes import JsonSerDes, PassThroughSerDes

# PassThroughSerDes — stores the value as-is (must already be a string)
# JsonSerDes — standard json.dumps / json.loads, no envelope format


@durable_step
def fetch_raw(ctx: StepContext) -> str:
    return '{"id":"order-123"}'


@durable_execution
def handler(event: dict, context: DurableContext) -> str:
    raw = context.step(
        fetch_raw(),
        config=StepConfig(serdes=PassThroughSerDes()),
    )
    return raw

The built-in JacksonSerDes handles class instances via Jackson's ObjectMapper. You can create your own passthrough SerDes like this:

import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;
import software.amazon.lambda.durable.TypeToken;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.exception.SerDesException;
import software.amazon.lambda.durable.serde.SerDes;

// A pass-through SerDes stores the value as-is (already a JSON string).
class PassThroughSerDes implements SerDes {
    @Override
    public String serialize(Object value) {
        return (String) value;
    }

    @Override
    public <T> T deserialize(String data, TypeToken<T> typeToken) {
        @SuppressWarnings("unchecked")
        T result = (T) data;
        return result;
    }
}

public class PassThroughSerdesExample extends DurableHandler<Object, String> {
    @Override
    public String handleRequest(Object input, DurableContext context) {
        StepConfig config = StepConfig.builder()
            .serDes(new PassThroughSerDes())
            .build();

        return context.step(
            "fetch-raw",
            String.class,
            (StepContext ctx) -> "{\"id\":\"order-123\"}",
            config
        );
    }
}

JSONSerdes is the default. RawSerdes stores the value as-is. The value must be a string, []byte, or json.RawMessage. SerdesOf is a generic adapter that turns typed marshal and unmarshal functions into a Serdes. The SDK needs no class serdes, because a struct's methods are available on any value after a JSON round trip.

package main

import (
    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

// RawSerdes stores the value as-is (it must be a string, []byte, or json.RawMessage).
// JSONSerdes is the default: encoding/json without HTML escaping.

func handler(ctx durable.Context, _ any) (string, error) {
    return durable.Step(ctx, "fetch_raw", func(durable.StepContext) (string, error) {
        return `{"id":"order-123"}`, nil
    }, durable.WithStepSerdes(durable.RawSerdes))
}

func main() { durable.Start(handler) }

JsonSerdes is the default. PassthroughSerdes stores a String as is. Pass either by value, as in .serdes(JsonSerdes). A struct's methods come from its type, so a value keeps them after a JSON round trip.

use aws_durable_execution_sdk as durable;
use durable::JsonSerdes;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Order {
    id: String,
    total: String,
}

impl Order {
    fn label(&self) -> String {
        format!("Order {} — {}", self.id, self.total)
    }
}

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<String, durable::BoxError> {
    let order = ctx
        .step(|_| async {
            Ok(Order {
                id: "order-123".to_owned(),
                total: "99.99".to_owned(),
            })
        })
        .name("fetch-order")
        .serdes(JsonSerdes)
        .await?;
    Ok(order.label())
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}

DefaultLambdaJsonSerializer handles class and record instances via System.Text.Json. You can create a pass-through serializer that stores string values as-is by implementing ILambdaSerializer and registering it at the host boundary:

using System.Text;
using Amazon.Lambda.Core;

// A pass-through serializer stores string values as-is (already a JSON string)
// instead of re-encoding them as a JSON string literal. .NET has no per-operation
// SerDes, so this behavior is expressed as a custom ILambdaSerializer registered
// on ILambdaContext.Serializer at the host boundary. It applies to every durable
// operation result in the handler.
public class PassThroughLambdaSerializer : ILambdaSerializer
{
    public T Deserialize<T>(Stream requestStream)
    {
        using var reader = new StreamReader(requestStream);
        string data = reader.ReadToEnd();
        // Return the raw payload unchanged when T is string.
        return (T)(object)data;
    }

    public void Serialize<T>(T response, Stream responseStream)
    {
        // Write the value as-is; the value is expected to already be a JSON string.
        string data = (string)(object)response!;
        var bytes = Encoding.UTF8.GetBytes(data);
        responseStream.Write(bytes, 0, bytes.Length);
    }
}

FileSystem serdes

The FileSystem serdes stores data on the filesystem and keeps a file pointer in the checkpoint. This can be useful when you do not want to store your data in the checkpoint itself, for example when your payload exceeds the durable execution checkpoint size limit. The persisted value lives on the filesystem and the SDK reads it on replay.

The FileSystem serdes requires a filesystem that persists across invocations and that every Lambda execution environment can read. In AWS Lambda, this means:

  • Amazon Elastic File System (Amazon EFS). Serverless file system that scales automatically with your workloads.
  • Amazon S3 Files. Serverless file system for mounting an Amazon S3 bucket. Amazon S3 Files provides access to Amazon S3 objects as files using standard file system operations such as read and write on the local mount path.

Do not use Lambda's /tmp directory

Lambda's /tmp filesystem is local to a single execution environment. A different execution environment may run the next invocation, so the SDK cannot find files written to /tmp on replay and deserialization fails. Always mount a persistent, shared filesystem.

Pass the FileSystem serdes to a single operation through StepConfig, CallbackConfig, MapConfig, or ParallelConfig. Other operations in the same handler continue to use the default serdes.

import {
  DurableContext,
  withDurableExecution,
  createFileSystemSerdes,
} from "@aws/durable-execution-sdk-js";

export const handler = withDurableExecution(
  async (event: { customerId: string }, context: DurableContext) => {
    const fsSerdes = createFileSystemSerdes("/mnt/s3");

    // Pass the FileSystem serdes to one step. Other operations in this handler
    // keep using the default JSON serdes.
    const profile = await context.step(
      "load-profile",
      async () => loadLargeProfile(event.customerId),
      { serdes: fsSerdes },
    );

    return { name: profile.name };
  },
);

declare function loadLargeProfile(
  customerId: string,
): Promise<{ name: string }>;

Pass the FileSystem serdes to a single operation through StepConfig, CallbackConfig, MapConfig, or ParallelConfig. Other operations in the same handler continue to use the default serdes.

from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)
from aws_durable_execution_sdk_python.config import StepConfig
from aws_durable_execution_sdk_python.filesystem_serdes import FileSystemSerDes


@durable_step
def fetch_order(ctx: StepContext) -> dict:
    return {"id": "order-123", "total": "99.99"}


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    fs_serdes = FileSystemSerDes("/mnt/s3")

    # Pass the FileSystem serdes to one step. Other operations in this handler
    # keep using the default serdes.
    order = context.step(
        fetch_order(),
        config=StepConfig(serdes=fs_serdes),
    )

    return order

Create a FileSystem serdes with NewFileSystemSerdes and pass it to one operation with an option such as WithStepSerdes. The file holds the value as JSON. The checkpoint holds an envelope with the full file path.

package main

import (
    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type Event struct {
    CustomerID string `json:"customerId"`
}

type Profile struct {
    Name string `json:"name"`
}

func handler(ctx durable.Context, event Event) (Profile, error) {
    fsSerdes := durable.NewFileSystemSerdes("/mnt/s3")

    // Pass the FileSystem serdes to one step. Other operations in this handler
    // keep using the default JSON serdes.
    profile, err := durable.Step(ctx, "load-profile", func(durable.StepContext) (Profile, error) {
        return loadLargeProfile(event.CustomerID)
    }, durable.WithStepSerdes(fsSerdes))
    if err != nil {
        return Profile{}, err
    }

    return Profile{Name: profile.Name}, nil
}

func loadLargeProfile(customerID string) (Profile, error) {
    return Profile{Name: "Customer " + customerID}, nil
}

func main() { durable.Start(handler) }

Create a FileSystem serdes with durable::serdes::FileSystemSerdes::new and pass it to one operation's .serdes(..). The file holds the value as JSON. The checkpoint holds an envelope with the file's path relative to the base path.

use aws_durable_execution_sdk as durable;
use durable::serdes::FileSystemSerdes;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CustomerEvent {
    customer_id: String,
}

#[derive(Serialize, Deserialize)]
struct Profile {
    name: String,
}

async fn handler(
    event: CustomerEvent,
    ctx: durable::DurableContext,
) -> Result<Profile, durable::BoxError> {
    let fs_serdes = FileSystemSerdes::new("/mnt/s3");

    // Pass the FileSystem serdes to one step. Other operations in this handler
    // keep using the default JSON serdes.
    let customer_id = event.customer_id;
    let profile = ctx
        .step(move |_| async move { Ok(load_large_profile(&customer_id)) })
        .name("load-profile")
        .serdes(fs_serdes)
        .await?;

    Ok(Profile { name: profile.name })
}

fn load_large_profile(customer_id: &str) -> Profile {
    Profile {
        name: format!("Customer {customer_id}"),
    }
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}

Not available. The .NET SDK has no FileSystem serdes. To keep large payloads out of the checkpoint, write them to a persistent store (such as Amazon S3) inside a step and checkpoint a pointer instead of the payload.

Create a FileSystem serdes

Create a FileSystem serdes and pass it to an operation's config.

createFileSystemSerdes(basePath, config?) returns a Serdes<unknown>.

import {
  Serdes,
  FileSystemSerdesConfig,
} from "@aws/durable-execution-sdk-js";

export type CreateFileSystemSerdes = (
  basePath: string,
  config?: FileSystemSerdesConfig,
) => Serdes<unknown>;

Parameters:

  • basePath Directory where the SDK writes data files. Set this to your filesystem mount point.
  • config (optional) FileSystemSerdesConfig controlling storage mode, path encoding, and preview generation.

Returns: A Serdes that reads and writes JSON files under basePath.

FileSystemSerDes(base_path, config?) is a SerDes subclass you instantiate.

from aws_durable_execution_sdk_python.filesystem_serdes import (
    FileSystemSerDes,
    FileSystemSerDesConfig,
)
from aws_durable_execution_sdk_python.serdes import SerDes

fs_serdes: SerDes = FileSystemSerDes("/mnt/s3")

Parameters:

  • base_path Directory where the SDK writes data files. Set this to your filesystem mount point.
  • config (optional) FileSystemSerDesConfig controlling storage mode, path encoding, and preview generation.

Returns: A SerDes instance that reads and writes JSON files under base_path.

Coming soon.

NewFileSystemSerdes(basePath, cfg...) returns a Serdes. The config is a trailing variadic argument. Omit it for the defaults. The SDK uses only the first config.

func NewFileSystemSerdes(basePath string, cfg ...FileSystemSerdesConfig) Serdes

Parameters:

  • basePath Directory where the SDK writes data files. Set this to your filesystem mount point.
  • cfg (optional) A FileSystemSerdesConfig controlling storage mode, path encoding, and preview generation.

Returns: A Serdes that reads and writes JSON files under basePath.

FileSystemSerdes::new(base_path) uses the default config. FileSystemSerdes::with_config(base_path, config) takes a FileSystemSerdesConfig. The serdes implements Serdes<T> for every serde type, so one value serves any operation. It runs its file I/O on the Tokio blocking pool, so it needs a Tokio runtime, which #[tokio::main] provides.

impl FileSystemSerdes {
    // ...
    pub fn new(base_path: impl Into<String>) -> Self;
    // ...
    pub fn with_config(base_path: impl Into<String>, config: FileSystemSerdesConfig) -> Self;
    // ...
}

// ...

impl<T> Serdes<T> for FileSystemSerdes
where
    T: Serialize + DeserializeOwned + Send + 'static,
{
    // ...
}

Parameters:

  • base_path Directory where the SDK writes data files. Set this to your filesystem mount point. The SDK creates it when it is missing.
  • config A FileSystemSerdesConfig controlling storage mode, path encoding, the overflow threshold, and preview generation.

Returns: A FileSystemSerdes that reads and writes JSON files under base_path.

Not available.

FileSystemSerdesConfig

import {
  FileSystemSerdesMode,
  FileSystemPathEncoding,
} from "@aws/durable-execution-sdk-js";

export interface FileSystemSerdesConfig {
  storageMode?: FileSystemSerdesMode;
  pathEncoding?: FileSystemPathEncoding;
  generatePreview?: (value: unknown) => Record<string, unknown> | undefined;
}

Fields:

  • storageMode (optional) A FileSystemSerdesMode value. Default: FileSystemSerdesMode.ALWAYS.
  • pathEncoding (optional) A FileSystemPathEncoding value. Default: FileSystemPathEncoding.URI.
  • generatePreview (optional) Function that returns a preview object. The SDK stores the preview inline in the checkpoint envelope alongside the file pointer. Use the buildPreview helper or write your own.
from aws_durable_execution_sdk_python.filesystem_serdes import (
    FileSystemSerDesConfig,
    FileSystemSerDesMode,
    FileSystemPathEncoding,
)

config = FileSystemSerDesConfig(
    storage_mode=FileSystemSerDesMode.ALWAYS,
    path_encoding=FileSystemPathEncoding.URI,
    generate_preview=None,
)

Fields:

  • storage_mode (optional) A FileSystemSerDesMode value. Default: FileSystemSerDesMode.ALWAYS.
  • path_encoding (optional) A FileSystemPathEncoding value. Default: FileSystemPathEncoding.URI.
  • generate_preview (optional) Callable that returns a preview dict or None. The SDK stores the preview inline in the checkpoint envelope alongside the file pointer. Use the build_preview helper or write your own.
  • serdes (optional) SerDes instance that serializes values before writing them to the filesystem. Default: ExtendedTypeSerDes.

Coming soon.

type FileSystemSerdesConfig struct {
    // Mode controls when data is written to the filesystem. Default is
    // FileSystemSerdesModeAlways.
    Mode FileSystemSerdesMode

    // PathEncoding controls the directory and file names of offloaded
    // values. Default is FileSystemPathEncodingURI, the readable layout.
    PathEncoding FileSystemPathEncoding

    // GeneratePreview, when set, is called with each value that is written
    // to a file, and its non-nil result is stored in the checkpoint
    // envelope next to the file reference as the "preview" member.
    GeneratePreview func(value any) map[string]any
    // Has unexported fields.
}

Fields:

  • Mode A FileSystemSerdesMode value. Default: FileSystemSerdesModeAlways.
  • PathEncoding A FileSystemPathEncoding value. Default: FileSystemPathEncodingURI.
  • GeneratePreview A function that returns a preview map. The SDK stores the result inline in the checkpoint envelope next to the file reference. Use the BuildPreview helper or write your own.

The FileSystem serdes always encodes values with encoding/json and HTML escaping disabled, as JSONSerdes does.

Build the config with FileSystemSerdesConfig::builder(), or use FileSystemSerdesConfig::default().

#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FileSystemSerdesConfig {
    pub storage_mode: FileSystemSerdesMode,
    pub path_encoding: FileSystemPathEncoding,
    pub overflow_threshold_bytes: usize,
    // ...
}

// ...

impl FileSystemSerdesConfigBuilder {
    pub fn storage_mode(mut self, mode: FileSystemSerdesMode) -> Self;
    // ...
    pub fn path_encoding(mut self, encoding: FileSystemPathEncoding) -> Self;
    // ...
    pub fn overflow_threshold_bytes(mut self, bytes: usize) -> Self;
    // ...
    pub fn preview<F>(mut self, f: F) -> Self
    where
        F: Fn(&str) -> Result<Option<String>, BoxError> + Send + Sync + 'static;
    // ...
    pub fn build(self) -> FileSystemSerdesConfig;
    // ...
}

Fields:

  • storage_mode A FileSystemSerdesMode value. Default: FileSystemSerdesMode::Always.
  • path_encoding A FileSystemPathEncoding value. Default: FileSystemPathEncoding::Uri.
  • overflow_threshold_bytes The envelope size above which overflow mode writes a file. Default: 261120 (255 KiB).
  • preview (builder only) A function that receives the value's JSON text and returns a JSON object as text, or None. The SDK stores the preview inline in the checkpoint envelope next to the file pointer. Use the build_preview helper or write your own. An Err fails the serialize. Text other than a JSON object fails it permanently.

The FileSystem serdes always encodes values with serde_json.

Not available.

Storage modes

The storageMode enumerated field controls when the SDK writes to the filesystem.

FileSystemSerdesMode.ALWAYS writes every value to a file. The checkpoint stores only the file pointer.

FileSystemSerdesMode.OVERFLOW uses the standard durable execution checkpoint store and only writes to the filesystem when the value would exceed the durable execution checkpoint size limit. See AWS Lambda service quotas.

import {
  createFileSystemSerdes,
  FileSystemSerdesMode,
} from "@aws/durable-execution-sdk-js";

// Small payloads stay inline in the checkpoint. The SDK only writes to the
// filesystem when the checkpoint envelope would exceed the size limit.
export const overflowFileSystemSerdes = createFileSystemSerdes("/mnt/s3", {
  storageMode: FileSystemSerdesMode.OVERFLOW,
});
from aws_durable_execution_sdk_python.filesystem_serdes import (
    FileSystemSerDes,
    FileSystemSerDesConfig,
    FileSystemSerDesMode,
)

# In OVERFLOW mode, small payloads stay inline in the checkpoint and no file
# is written. The SDK only writes to the filesystem when the serialized value
# exceeds the checkpoint size limit.
overflow_fs_serdes = FileSystemSerDes(
    "/mnt/s3",
    FileSystemSerDesConfig(storage_mode=FileSystemSerDesMode.OVERFLOW),
)

Coming soon.

In overflow mode, the FileSystem serdes writes the value to a file when the inline envelope exceeds 255KB. GeneratePreview is not called for a value stored inline.

package main

import (
    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

// Small payloads stay inline in the checkpoint. The SDK only writes to the
// filesystem when the checkpoint envelope would exceed the size limit.
var overflowFileSystemSerdes = durable.NewFileSystemSerdes("/mnt/s3", durable.FileSystemSerdesConfig{
    Mode: durable.FileSystemSerdesModeOverflow,
})

In FileSystemSerdesMode::Overflow, the FileSystem serdes writes the value to a file when the inline envelope is longer than overflow_threshold_bytes. The preview function is not called for a value stored inline.

use aws_durable_execution_sdk as durable;
use durable::serdes::{FileSystemSerdes, FileSystemSerdesConfig, FileSystemSerdesMode};

// Small payloads stay inline in the checkpoint. The SDK only writes to the
// filesystem when the checkpoint envelope would exceed the size limit.
fn overflow_file_system_serdes() -> FileSystemSerdes {
    FileSystemSerdes::with_config(
        "/mnt/s3",
        FileSystemSerdesConfig::builder()
            .storage_mode(FileSystemSerdesMode::Overflow)
            .build(),
    )
}

Not available.

Path encoding

The pathEncoding field controls how the durable execution ARN and the entity ID become the on-disk directory and file names.

FileSystemPathEncoding.URI builds a per-execution directory from the function name, execution name, and invocation ID parsed from the ARN, and encodes the entity ID with encodeURIComponent for the file name. Names stay readable when you read files directly from the mount. A very long entity ID may exceed the filesystem's per-name length limit, commonly 255 bytes.

FileSystemPathEncoding.HASH replaces the ARN and the entity ID with their SHA-256 hex digests. Names are a fixed 64 characters and are always filesystem-safe regardless of the input characters or length. Choose HASH when entity IDs may contain characters that are unsafe in a file name, such as /, or may be long enough to exceed the name-length limit.

import {
  createFileSystemSerdes,
  FileSystemPathEncoding,
} from "@aws/durable-execution-sdk-js";

// Hash the ARN and the entity ID into fixed-length, filesystem-safe segments.
// Use HASH when entity IDs may contain unsafe characters or be very long.
export const hashedFileSystemSerdes = createFileSystemSerdes("/mnt/s3", {
  pathEncoding: FileSystemPathEncoding.HASH,
});
from aws_durable_execution_sdk_python.filesystem_serdes import (
    FileSystemSerDes,
    FileSystemSerDesConfig,
    FileSystemPathEncoding,
)

# Hash the ARN and the entity ID into fixed-length, filesystem-safe segments.
# Use HASH when entity IDs may contain unsafe characters or be very long.
hashed_fs_serdes = FileSystemSerDes(
    "/mnt/s3",
    FileSystemSerDesConfig(path_encoding=FileSystemPathEncoding.HASH),
)

Coming soon.

FileSystemPathEncodingURI, the default, writes <functionName>/<executionName>/<invocationId>/<operationID>.json under the base path. It takes the first three segments from the execution ARN. It percent-encodes each segment, so bytes other than letters, digits, -, _, ., and ~ become %XX.

FileSystemPathEncodingHash writes <arnHash>/<operationID>.json. arnHash is the hex encoding of the first 16 bytes of the SHA-256 digest of the execution ARN, so it is 32 characters long. FileSystemPathEncodingHash does not hash or encode the operation ID. The SDK generates operation IDs such as 1 or 1-2-3, so the file name is always safe.

The checkpoint envelope stores the full file path. So the SDK reads a value back correctly after you change the encoding.

package main

import (
    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

// Hash the ARN into a fixed-length, filesystem-safe directory name.
var hashedFileSystemSerdes = durable.NewFileSystemSerdes("/mnt/s3", durable.FileSystemSerdesConfig{
    PathEncoding: durable.FileSystemPathEncodingHash,
})

FileSystemPathEncoding::Uri, the default, writes <functionName>/<executionName>/<invocationId>/<file name> under the base path. It takes the three directory segments from a durable execution ARN and percent-encodes each one, so bytes other than letters, digits, -, _, ., and ~ become %XX. It also percent-encodes the operation ID in the file name. If the ARN does not match the durable execution shape, it percent-encodes the whole ARN as one directory.

FileSystemPathEncoding::Hash writes <arnHash>/<file name>. arnHash is the 64-character SHA-256 hex digest of the execution ARN.

The file name is <operation ID>-<unique suffix>.json, so every write creates a new file. Hash replaces the operation ID with its SHA-256 hex digest. The checkpoint envelope stores the path relative to the base path, and the SDK reads a file only from under the base path.

use aws_durable_execution_sdk as durable;
use durable::serdes::{FileSystemPathEncoding, FileSystemSerdes, FileSystemSerdesConfig};

// Hash the execution ARN and the operation ID to keep directory and file names
// safe for the filesystem.
fn hashed_file_system_serdes() -> FileSystemSerdes {
    FileSystemSerdes::with_config(
        "/mnt/s3",
        FileSystemSerdesConfig::builder()
            .path_encoding(FileSystemPathEncoding::Hash)
            .build(),
    )
}

Not available.

Preview and PII masking

When the FileSystem serdes writes to a file, the checkpoint envelope only contains the file pointer, so the GetDurableExecution API and the AWS Console cannot show the actual data for that operation. Configure generatePreview to embed a small object inline so you can see the data without reading the file.

Use buildPreview to compute the preview from a PreviewConfig. The config selects which fields to include, exclude, or mask. Masking replaces a field's value with *** or a custom string. The preview object is capped at 4096 bytes by default.

import {
  createFileSystemSerdes,
  buildPreview,
  PreviewMode,
} from "@aws/durable-execution-sdk-js";

// The checkpoint envelope stores the file pointer plus a small preview object,
// so the console can show id and status without reading the full file. The
// email field is visible but masked, so PII does not land in the checkpoint.
export const previewFileSystemSerdes = createFileSystemSerdes("/mnt/s3", {
  generatePreview: (value) =>
    buildPreview(value, {
      mode: PreviewMode.EXCLUDE_ALL,
      include: [{ name: "id" }, { name: "status" }],
      mask: [{ name: "email" }],
    }),
});

PreviewConfig fields:

  • mode Either PreviewMode.INCLUDE_ALL or PreviewMode.EXCLUDE_ALL. Sets the starting point before applying exclude and mask.
  • include (optional) Fields to add when starting from EXCLUDE_ALL.
  • exclude (optional) Fields to remove. Always wins over mask.
  • mask (optional) Fields whose values become maskString. A masked field is visible in the preview unless it is also excluded.
  • maskString (optional) Replacement value for masked fields. Default: ***.
  • maxPreviewBytes (optional) Maximum size in bytes for the preview object when JSON-serialized. Default: 4096.

Each PreviewField selector has a name and an optional match. Use FieldMatchMode.ANYWHERE (default) to match the field name at any depth in the object tree, or FieldMatchMode.PATH to match an exact dot-notation path from the root.

Selectors cannot address field names that contain a dot because a dot is indistinguishable from a path separator. The preview merges array elements into a plain object at the array's path rather than preserving array structure.

from aws_durable_execution_sdk_python.filesystem_serdes import (
    FileSystemSerDes,
    FileSystemSerDesConfig,
)
from aws_durable_execution_sdk_python.preview import (
    PreviewConfig,
    PreviewField,
    PreviewMode,
    build_preview,
)

# The checkpoint envelope stores the file pointer plus a small preview object,
# so the console can show id and status without reading the full file. The
# email field is visible but masked, so PII does not land in the checkpoint.
preview_fs_serdes = FileSystemSerDes(
    "/mnt/s3",
    FileSystemSerDesConfig(
        generate_preview=lambda value: build_preview(
            value,
            PreviewConfig(
                mode=PreviewMode.EXCLUDE_ALL,
                include=[PreviewField(name="id"), PreviewField(name="status")],
                mask=[PreviewField(name="email")],
            ),
        ),
    ),
)

PreviewConfig fields:

  • mode Either PreviewMode.INCLUDE_ALL or PreviewMode.EXCLUDE_ALL. Sets the starting point before applying exclude and mask.
  • include (optional) Fields to add when starting from EXCLUDE_ALL.
  • exclude (optional) Fields to remove. Always wins over mask.
  • mask (optional) Fields whose values become mask_string. A masked field is visible in the preview unless it is also excluded.
  • mask_string (optional) Replacement value for masked fields. Default: "***".
  • max_preview_bytes (optional) Maximum size in bytes for the preview object when JSON-serialized. Default: 4096.

Each PreviewField selector has a name and an optional match. Use FieldMatchMode.ANYWHERE (default) to match the field name at any depth in the object tree, or FieldMatchMode.PATH to match an exact dot-notation path from the root.

Selectors cannot address field names that contain a dot because a dot is indistinguishable from a path separator. The preview merges array elements into a plain object at the array's path rather than preserving array structure.

Coming soon.

package main

import (
    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

// The checkpoint envelope stores the file pointer plus a small preview object,
// so the console can show id and status without reading the full file. The
// email field is visible but masked, so PII does not land in the checkpoint.
var previewFileSystemSerdes = durable.NewFileSystemSerdes("/mnt/s3", durable.FileSystemSerdesConfig{
    GeneratePreview: func(value any) map[string]any {
        return durable.BuildPreview(value, durable.PreviewConfig{
            Mode:    durable.PreviewExcludeAll,
            Include: []durable.PreviewField{{Name: "id"}, {Name: "status"}},
            Mask:    []durable.PreviewField{{Name: "email"}},
        })
    },
})

PreviewConfig fields:

  • Mode Either PreviewIncludeAll or PreviewExcludeAll. Sets the starting point before Exclude and Mask. Default: PreviewIncludeAll.
  • Include Fields to add when starting from PreviewExcludeAll.
  • Exclude Fields to remove. Always wins over Include and Mask.
  • Mask Fields whose values become MaskString. A masked field is visible under either mode unless it is also excluded.
  • MaskString Replacement for masked fields. Default: ***.
  • MaxPreviewBytes Maximum JSON size of the preview in bytes. Default: 4096. When the cap leaves fields out, the preview carries the key $truncated set to true.
  • MaxDepth How many nested objects and slices are traversed. Default: 32.

Each PreviewField has a Name and a Match. Use FieldMatchAnywhere, the default, to match the field name at any depth, or FieldMatchPath for an exact dot-path from the root. A field name that contains a dot cannot be selected. The preview merges the fields of slice elements under the slice's path.

Pass a function to FileSystemSerdesConfigBuilder::preview that calls durable::serdes::build_preview with a PreviewConfig. build_preview returns Option<String>, so the function wraps it in Ok.

use aws_durable_execution_sdk as durable;
use durable::serdes::{
    FileSystemSerdes, FileSystemSerdesConfig, PreviewConfig, PreviewMode, build_preview,
};

// The checkpoint envelope stores the file pointer and a preview object. The
// console can show id and status without reading the full file. The checkpoint
// stores email only in masked form.
fn preview_file_system_serdes() -> FileSystemSerdes {
    let preview = PreviewConfig::builder(PreviewMode::ExcludeAll)
        .include("id")
        .include("status")
        .mask("email")
        .build();
    FileSystemSerdes::with_config(
        "/mnt/s3",
        FileSystemSerdesConfig::builder()
            .preview(move |json| Ok(build_preview(json, &preview)))
            .build(),
    )
}

PreviewConfig builder methods:

  • PreviewConfig::builder(mode) Takes PreviewMode::IncludeAll or PreviewMode::ExcludeAll. Sets the starting point before exclude and mask.
  • include Adds a field to show when starting from ExcludeAll.
  • exclude Adds a field to remove. Always wins over include and mask.
  • mask Adds a field whose value becomes the mask string. A masked field is visible under either mode unless it is also excluded.
  • mask_string Replacement for masked fields. Default: ***.
  • max_preview_bytes Maximum estimated size in bytes. build_preview stops before the first field whose estimated path and compact value would exceed the limit. Default: 4096.

include, exclude, and mask take a PreviewField. A &str becomes PreviewField::anywhere, which matches the field name at any depth. PreviewField::path matches an exact dot-separated path from the root. A field name that contains a dot cannot be selected. The preview merges the fields of array elements under the array's path.

Not available.

Set as the default for the handler

When you want every step result, child-context result, invoke result, and waitForCondition result in the handler to use the FileSystem serdes, configure it once with configureSerdes.

import {
  DurableContext,
  withDurableExecution,
  createFileSystemSerdes,
} from "@aws/durable-execution-sdk-js";

export const handler = withDurableExecution(
  async (event: { customerId: string }, context: DurableContext) => {
    // Configure once. Every step result, child-context result, invoke result,
    // and waitForCondition result in this handler is now stored on /mnt/s3.
    context.configureSerdes({
      defaultSerdes: createFileSystemSerdes("/mnt/s3"),
    });

    const profile = await context.step("load-profile", async () => {
      return loadLargeProfile(event.customerId);
    });

    return { name: profile.name };
  },
);

declare function loadLargeProfile(
  customerId: string,
): Promise<{ name: string }>;

Create a single FileSystemSerDes instance and a shared StepConfig, then pass it to every operation in the handler.

from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)
from aws_durable_execution_sdk_python.config import StepConfig
from aws_durable_execution_sdk_python.filesystem_serdes import FileSystemSerDes


@durable_step
def fetch_order(ctx: StepContext) -> dict:
    return {"id": "order-123", "total": "99.99"}


@durable_step
def fetch_preferences(ctx: StepContext) -> dict:
    return {"theme": "dark", "language": "en"}


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    # Create once and pass to every operation in the handler.
    fs_serdes = FileSystemSerDes("/mnt/s3")
    config = StepConfig(serdes=fs_serdes)

    order = context.step(fetch_order(), config=config)
    preferences = context.step(fetch_preferences(), config=config)

    return {"order": order, "preferences": preferences}

Set the FileSystem serdes handler-wide from inside the handler with ConfigureSerdes, or at construction with durable.Start(handler, WithSerdes(...)). ConfigureSerdes must run identically on every invocation, including replays. The handler serdes also serves Map and Parallel item results. It does not apply to callback payloads.

package main

import (
    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type Event struct {
    CustomerID string `json:"customerId"`
}

type Profile struct {
    Name string `json:"name"`
}

func handler(ctx durable.Context, event Event) (Profile, error) {
    // Configure once. Every step result, child-context result, invoke result,
    // and WaitForCondition result in this handler is now stored on /mnt/s3.
    if err := durable.ConfigureSerdes(ctx, durable.SerdesConfig{
        Serdes: durable.NewFileSystemSerdes("/mnt/s3"),
    }); err != nil {
        return Profile{}, err
    }

    profile, err := durable.Step(ctx, "load-profile", func(durable.StepContext) (Profile, error) {
        return loadLargeProfile(event.CustomerID)
    })
    if err != nil {
        return Profile{}, err
    }

    return Profile{Name: profile.Name}, nil
}

func loadLargeProfile(customerID string) (Profile, error) {
    return Profile{Name: "Customer " + customerID}, nil
}

func main() { durable.Start(handler) }

Each operation takes its own serdes. To store step, child-context, with_retry, map item, parallel branch, and wait-for-condition results with the FileSystem serdes, create one Arc<FileSystemSerdes> and pass a clone to each operation's .serdes(..). Arc<S> implements Serdes<T> for every T that S handles.

use aws_durable_execution_sdk as durable;
use durable::serdes::FileSystemSerdes;
use serde::{Deserialize, Serialize};
use std::sync::Arc;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CustomerEvent {
    customer_id: String,
}

#[derive(Serialize, Deserialize)]
struct Profile {
    name: String,
}

async fn handler(
    event: CustomerEvent,
    ctx: durable::DurableContext,
) -> Result<Profile, durable::BoxError> {
    let fs_serdes = Arc::new(FileSystemSerdes::new("/mnt/s3"));

    let customer_id = event.customer_id;
    let profile = ctx
        .step(move |_| async move { Ok(load_large_profile(&customer_id)) })
        .name("load-profile")
        .serdes(Arc::clone(&fs_serdes))
        .await?;

    Ok(Profile { name: profile.name })
}

fn load_large_profile(customer_id: &str) -> Profile {
    Profile {
        name: format!("Customer {customer_id}"),
    }
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}

Not available. The single ILambdaSerializer you register at the host boundary is already applied to every durable operation result in the handler.

Serialization errors

When serialization or deserialization fails, each SDK raises or throws a specific exception type. See Serialization errors for the exception hierarchy and how to handle serialization failures.

See also