Skip to content

Callback

Wait for external systems

A callback suspends a durable function and waits for an external system to provide input. When you create a callback, the SDK checkpoints the operation and returns a unique callback ID. After you send the callback ID to an external system, the SDK suspends the durable function and terminates the current invocation. The function does not incur compute charges while suspended.

External systems send callback results using the SendDurableExecutionCallbackSuccess or SendDurableExecutionCallbackFailure Lambda APIs.

When the external system calls back with the ID, the backend starts a new invocation to resume your function from where it suspended.

Use callbacks when you need to pause execution until a human approves a request, or a payment processor confirms a transaction, or any other external system completes work asynchronously. The SDK provides two ways to work with callbacks, either create a callback and manage the external system notification and wait yourself, or use waitForCallback to handle submission and waiting in one operation.

The SDK provides two operations for working with callbacks.

Create Callback

Create a callback and return a callback ID along with a handle to wait for the result. The wait does not consume compute.

You send the callback ID to an external system. The external system uses that ID to send callback results using the Lambda API.

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

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    const [callbackPromise, callbackId] = await context.createCallback(
      "wait-for-approval",
    );

    // Send callbackId to the external system that will resume this function.
    await sendApprovalRequest(callbackId, event.requestId);

    // Execution suspends here until the external system calls back.
    const result = await callbackPromise;
    return { approved: true, result };
  },
);
from typing import Any

from aws_durable_execution_sdk_python import DurableContext, durable_execution


@durable_execution
def handler(event: Any, context: DurableContext) -> dict:
    callback = context.create_callback(name="wait-for-approval")

    # Send callback.callback_id to the external system that will resume this function.
    send_approval_request(callback.callback_id, event["request_id"])

    # Execution suspends here until the external system calls back.
    result = callback.result()
    return {"approved": True, "result": result}
import software.amazon.lambda.durable.DurableCallbackFuture;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

public class BasicCallbackExample extends DurableHandler<Object, String> {
    @Override
    public String handleRequest(Object input, DurableContext context) {
        DurableCallbackFuture<String> callback =
                context.createCallback("wait-for-approval", String.class);

        // Send callback.callbackId() to the external system that will resume this function.
        sendApprovalRequest(callback.callbackId());

        // Execution suspends here until the external system calls back.
        return callback.get();
    }
}
package main

import (
    "encoding/json"

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

type approvalRequest struct {
    RequestID string `json:"requestId"`
}

type approvalResponse struct {
    Approved bool            `json:"approved"`
    Result   json.RawMessage `json:"result"`
}

func handler(ctx durable.Context, event approvalRequest) (approvalResponse, error) {
    cb, err := durable.CreateCallback[json.RawMessage](ctx, "wait-for-approval")
    if err != nil {
        return approvalResponse{}, err
    }

    // Send the callback ID to the external system that will resume this function.
    if err := sendApprovalRequest(cb.ID(), event.RequestID); err != nil {
        return approvalResponse{}, err
    }

    // Execution suspends here until the external system calls back.
    result, err := cb.Result(ctx)
    if err != nil {
        return approvalResponse{}, err
    }
    return approvalResponse{Approved: true, Result: result}, nil
}

func sendApprovalRequest(callbackID, requestID string) error {
    return nil
}

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

The SDK decodes the callback payload as JSON into the payload type you name, here serde_json::Value.

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

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

#[derive(Serialize)]
struct ApprovalResult {
    approved: bool,
    result: Value,
}

async fn handler(
    event: ApprovalEvent,
    ctx: durable::DurableContext,
) -> Result<ApprovalResult, durable::BoxError> {
    let callback = ctx
        .create_callback::<Value>()
        .name("wait-for-approval")
        .await?;

    // Send the callback ID to the external system that will resume this function.
    send_approval_request(callback.id(), &event.request_id);

    // Execution suspends here until the external system calls back.
    let result = callback.result().await?;
    Ok(ApprovalResult {
        approved: true,
        result,
    })
}

fn send_approval_request(_callback_id: &str, _request_id: &str) {}

#[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 BasicCallbackExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<RequestEvent, ApprovalResult>(Workflow, input, context);

    private async Task<ApprovalResult> Workflow(RequestEvent input, IDurableContext ctx)
    {
        ICallback<string> callback = await ctx.CreateCallbackAsync<string>("wait-for-approval");

        // Send callback.CallbackId to the external system that will resume this function.
        await SendApprovalRequestAsync(callback.CallbackId, input.RequestId);

        // Execution suspends here until the external system calls back.
        string result = await callback.GetResultAsync();
        return new ApprovalResult(Approved: true, Result: result);
    }

    private static Task SendApprovalRequestAsync(string callbackId, string requestId)
        => Task.CompletedTask;
}

public record RequestEvent(string RequestId);
public record ApprovalResult(bool Approved, string Result);

Wait for Callback

Wait for Callback combines callback creation, submission, and waiting for the result in one operation.

Internally the SDK creates a child context containing a create callback operation followed by a step that runs the submitter function and then waits for the result.

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

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    const result = await context.waitForCallback(
      "wait-for-approval",
      async (callbackId) => {
        await sendApprovalRequest(callbackId, event.requestId);
      },
    );
    return { approved: true, result };
  },
);
from typing import Any

from aws_durable_execution_sdk_python import DurableContext, durable_execution
from aws_durable_execution_sdk_python.types import WaitForCallbackContext


@durable_execution
def handler(event: Any, context: DurableContext) -> dict:
    def submit(callback_id: str, ctx: WaitForCallbackContext) -> None:
        send_approval_request(callback_id, event["request_id"])

    result = context.wait_for_callback(submitter=submit, name="wait-for-approval")
    return {"approved": True, "result": result}
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

public class WaitForCallbackExample extends DurableHandler<Object, String> {
    @Override
    public String handleRequest(Object input, DurableContext context) {
        return context.waitForCallback(
                "wait-for-approval",
                String.class,
                (callbackId, stepCtx) -> sendApprovalRequest(callbackId));
    }
}

The submitter is func(ctx durable.StepContext, callbackID string) error. The context is the first argument and the callback ID is the second. The SDK retries the submitter on failure with the step default strategy, exponential backoff.

package main

import (
    "encoding/json"

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

type approvalRequest struct {
    RequestID string `json:"requestId"`
}

type approvalResponse struct {
    Approved bool            `json:"approved"`
    Result   json.RawMessage `json:"result"`
}

func handler(ctx durable.Context, event approvalRequest) (approvalResponse, error) {
    result, err := durable.WaitForCallback[json.RawMessage](ctx, "wait-for-approval",
        func(_ durable.StepContext, callbackID string) error {
            return sendApprovalRequest(callbackID, event.RequestID)
        },
    )
    if err != nil {
        return approvalResponse{}, err
    }
    return approvalResponse{Approved: true, Result: result}, nil
}

func sendApprovalRequest(callbackID, requestID string) error {
    return nil
}

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

The submitter is a closure that takes a durable::StepContext and the callback ID as a String, and returns a future of Result<(), durable::BoxError>. The SDK runs the submitter as a step and retries it on failure with the default step retry strategy, 6 attempts with exponential backoff. The SDK decodes the callback payload as JSON into the payload type you name, here serde_json::Value.

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

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

#[derive(Serialize)]
struct ApprovalResult {
    approved: bool,
    result: Value,
}

async fn handler(
    event: ApprovalEvent,
    ctx: durable::DurableContext,
) -> Result<ApprovalResult, durable::BoxError> {
    let request_id = event.request_id;
    let result = ctx
        .wait_for_callback::<Value>(move |_step_ctx, callback_id| async move {
            send_approval_request(&callback_id, &request_id);
            Ok(())
        })
        .name("wait-for-approval")
        .await?;
    Ok(ApprovalResult {
        approved: true,
        result,
    })
}

fn send_approval_request(_callback_id: &str, _request_id: &str) {}

#[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 WaitForCallbackExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<RequestEvent, ApprovalResult>(Workflow, input, context);

    private async Task<ApprovalResult> Workflow(RequestEvent input, IDurableContext ctx)
    {
        string result = await ctx.WaitForCallbackAsync<string>(
            async (callbackId, _, _) => await SendApprovalRequestAsync(callbackId, input.RequestId),
            name: "wait-for-approval");
        return new ApprovalResult(Approved: true, Result: result);
    }

    private static Task SendApprovalRequestAsync(string callbackId, string requestId)
        => Task.CompletedTask;
}

public record RequestEvent(string RequestId);
public record ApprovalResult(bool Approved, string Result);

Callback lifecycle

A callback spans multiple invocations within a single execution.

sequenceDiagram
    participant Inv1 as Invocation 1
    participant Backend as Durable Backend
    participant Ext as External System
    participant Inv2 as Invocation 2

    note over Inv1, Inv2: Single Execution

    Inv1->>Backend: createCallback → checkpoint & get callback ID
    Inv1->>Ext: Send callback ID
    Inv1->>Backend: Suspend (invocation terminates, no compute)

    Ext->>Ext: Do async work
    Ext->>Backend: SendDurableExecutionCallbackSuccess/Failure

    Backend->>Inv2: Start new invocation, resume from checkpoint
    Inv2->>Inv2: Continue execution with callback result
  • Function The durable Lambda function. This contains your code.
  • Execution The complete end-to-end lifecycle of an AWS Lambda durable function
  • Invocation A single invocation of the function during the over-all execution. The invocation terminates at a callback wait. The backend re-invokes the function when the callback resolves, and replays to resume at the callback wait point with the result.

Method signatures

createCallback

// Named overload
createCallback<TOutput = string>(
  name: string | undefined,
  config?: CreateCallbackConfig<TOutput>,
): DurablePromise<[DurablePromise<TOutput>, string]>;

// Anonymous overload
createCallback<TOutput = string>(
  config?: CreateCallbackConfig<TOutput>,
): DurablePromise<[DurablePromise<TOutput>, string]>;

Parameters:

  • name (optional) A name for the callback. Pass undefined to omit.
  • config (optional) A CreateCallbackConfig<TOutput> object.

Returns: DurablePromise<[DurablePromise<TOutput>, string]>. Destructure with await to get [callbackPromise, callbackId]. Await callbackPromise to suspend until the external system calls back.

Throws: CallbackError if the callback fails, times out, or the external system reports failure. The error is thrown by callbackPromise, not by createCallback itself.

def create_callback(
    self,
    name: str | None = None,
    config: CallbackConfig | None = None,
) -> Callback: ...

Parameters:

  • name (optional) A name for the callback.
  • config (optional) A CallbackConfig object.

Returns: A Callback object. Access callback.callback_id to get the ID to send to the external system. Call callback.result() to suspend until the external system calls back.

Raises: CallbackError from callback.result() if the callback fails or times out.

// With Class<T>
<T> DurableCallbackFuture<T> createCallback(String name, Class<T> resultType)
<T> DurableCallbackFuture<T> createCallback(String name, Class<T> resultType, CallbackConfig config)

// With TypeToken<T> for generic result types
<T> DurableCallbackFuture<T> createCallback(String name, TypeToken<T> resultType)
<T> DurableCallbackFuture<T> createCallback(String name, TypeToken<T> resultType, CallbackConfig config)

Parameters:

  • name (required) A name for the callback.
  • resultType The Class<T> or TypeToken<T> for deserialization.
  • config (optional) A CallbackConfig object.

Returns: DurableCallbackFuture<T>. Call callback.callbackId() to get the ID to send to the external system. Call callback.get() to suspend until the external system calls back.

Throws: CallbackFailedException if the external system reports failure. CallbackTimeoutException if the callback times out.

func CreateCallback[O any](ctx Context, name string, opts ...CallbackOption) (*Callback[O], error)

Parameters:

  • ctx (required) The durable context, always the first argument.
  • name (required) A name for the callback. Pass "" to omit it.
  • opts (optional) Variadic CallbackOption values. See CallbackConfig.

Returns: (*Callback[O], error). Call cb.ID() to get the ID to send to the external system, and cb.Result(ctx) to suspend until the external system submits a result. The result flows through RawSerdes by default, so O must be string, []byte, or json.RawMessage and the handler receives the submitted bytes unchanged. Pass durable.WithCallbackSerdes(durable.JSONSerdes) to decode the payload into another type.

Errors: CreateCallback returns an error for an invalid option or a checkpoint failure. The callback's own failure surfaces from cb.Result, not from CreateCallback. A failure the external system sends returns a *CallbackExternalError whose ErrorType and Message are the values the system supplied. A timeout returns a *CallbackTimeoutError. Both match *CallbackError. A result the deserializer cannot decode into O returns a *SerdesError.

pub struct CreateCallbackBuilder<O, S = JsonSerdes> {
    // ...
}

impl DurableContext {
    // ...

    pub fn create_callback<O>(&self) -> CreateCallbackBuilder<O>
    where
        O: Send + 'static;

    // ...
}

impl<O: Send + 'static, S> CreateCallbackBuilder<O, S> {
    pub fn name(mut self, name: impl Into<String>) -> Self;

    // ...

    pub fn future(self) -> DurableFuture<Callback<O>>
    where
        S: Serdes<O>;

    // ...

    pub fn spawn(self) -> DurableFuture<Callback<O>>
    where
        S: Serdes<O>;

    // ...
}

impl<O, S> IntoFuture for CreateCallbackBuilder<O, S>
where
    O: Send + 'static,
    S: Serdes<O>,
{
    type Output = Result<Callback<O>, OperationError>;
    // ...
}

Parameters:

  • O (required) The payload type. By default the SDK decodes the payload as JSON into O, so O must implement serde::Serialize and serde::de::DeserializeOwned. See CallbackConfig to change the deserializer.
  • name (optional) A name for the callback, set with .name(). Omit the call to leave the callback unnamed.
  • The other builder methods are listed in CallbackConfig.

Returns: durable::builders::CreateCallbackBuilder<O>. Await it to get Result<durable::builders::callback::Callback<O>, durable::OperationError>. Call callback.id() to get the ID to send to the external system, and callback.result().await to suspend until the external system calls back. .future() returns a lazy durable::DurableFuture<durable::builders::callback::Callback<O>> without starting the operation. .spawn() starts the creation at once and returns the future to await later.

Errors: The callback's own failure surfaces from callback.result() as a durable::OperationError whose kind() is durable::OperationErrorKind::Callback. Its durable::CallbackErrorKind is ExternalFailure for a failure the external system sends or a callback that ends CANCELLED or STOPPED, TimedOut or HeartbeatTimedOut for a timeout, DeserializationFailed for a payload that does not decode into O, or Internal for an SDK or service failure. For ExternalFailure, err.wire() returns the recorded type, data, and stack trace, and the recorded message, or Callback failed when the message is absent or empty. For a timeout, err.wire() returns the recorded type, message, and data, with Callback timed out when the message is absent or empty.

Awaiting the builder returns a durable::OperationError of kind durable::OperationErrorKind::NonDeterministicExecution when replay finds a different operation at this point.

Task<ICallback<T>> CreateCallbackAsync<T>(
    string? name = null,
    CallbackConfig? config = null,
    CancellationToken cancellationToken = default);

Parameters:

  • name (optional) A name for the callback. Omit it to infer one from the call site.
  • config (optional) A CallbackConfig object.
  • cancellationToken (optional) A token to observe for cancellation.

Returns: Task<ICallback<T>>. Access callback.CallbackId to get the ID to send to the external system. Call callback.GetResultAsync() to suspend until the external system calls back.

Throws: CallbackFailedException if the external system reports failure. CallbackTimeoutException if the callback times out. Both are thrown from GetResultAsync(), not from CreateCallbackAsync itself.

Callback Handle

The object returned by createCallback.

[callbackPromise, callbackId] — a tuple destructured from the awaited result.

  • callbackId (string) The unique ID to send to the external system.
  • callbackPromise (DurablePromise<TOutput>) Await to suspend until the external system calls back.
class Callback:
    callback_id: str
    def result(self) -> T | None: ...
  • callback_id The unique ID to send to the external system.
  • result() Suspends until the external system calls back. Raises CallbackError if the callback fails or times out.
interface DurableCallbackFuture<T> extends DurableFuture<T> {
    String callbackId();
    T get();
}
  • callbackId() The unique ID to send to the external system.
  • get() Blocks until the external system calls back. Throws CallbackFailedException or CallbackTimeoutException on failure.
type Callback[O any] struct {
    // Has unexported fields.
}

func (c *Callback[O]) ID() string
func (c *Callback[O]) Result(ctx Context) (O, error)
  • ID() The unique ID to send to the external system.
  • Result(ctx) Suspends until the external system submits a result. Returns a *CallbackExternalError or *CallbackTimeoutError on failure, both matching *CallbackError. When the invocation suspends, it returns the suspension signal, which you return unchanged. Pass the context of the code that reads the outcome.
pub struct Callback<O> {
    // ...
}

impl<O: Send + 'static> Callback<O> {
    // ...
    pub fn id(&self) -> &str;
    // ...
    pub fn result(self) -> DurableFuture<O>;
    // ...
    pub fn spawn_result(self) -> DurableFuture<O>;
    // ...
}
  • id() The unique ID to send to the external system.
  • result() Returns a durable::DurableFuture<O>. Await it to suspend until the external system calls back. It resolves to Result<O, durable::OperationError>, and the error's kind() is durable::OperationErrorKind::Callback when the callback fails or times out. It consumes the handle, so read id() first.
  • spawn_result() Waits for the result on its own task and returns a durable::DurableFuture<O> for it, so other operations can run while the callback is pending.
public interface ICallback<T>
{
    string CallbackId { get; }
    Task<T> GetResultAsync(CancellationToken cancellationToken = default);
}
  • CallbackId The unique ID to send to the external system.
  • GetResultAsync() Suspends until the external system calls back. Throws CallbackFailedException or CallbackTimeoutException on failure.

CallbackConfig

interface CreateCallbackConfig<TOutput = string> {
  timeout?: Duration;
  heartbeatTimeout?: Duration;
  serdes?: Omit<Serdes<TOutput>, "serialize">;
}

Parameters:

  • timeout (optional) Maximum time to wait for the callback result. See Duration for how to specify durations.
  • heartbeatTimeout (optional) Maximum time between heartbeat signals from the external system. If the external system does not send a heartbeat within this interval, the callback fails.
  • serdes (optional) Custom deserializer for the callback result. See Serialization.
@dataclass(frozen=True)
class CallbackConfig:
    timeout: Duration = Duration()
    heartbeat_timeout: Duration = Duration()
    serdes: SerDes | None = None

Parameters:

  • timeout (optional) Maximum time to wait for the callback result. See Duration for how to specify durations.
  • heartbeat_timeout (optional) Maximum time between heartbeat signals from the external system. If the external system does not send a heartbeat within this interval, the callback fails.
  • serdes (optional) Custom SerDes for the callback result. See Serialization.
CallbackConfig.builder()
    .timeout(Duration)          // optional
    .heartbeatTimeout(Duration) // optional
    .serDes(SerDes)             // optional
    .build()

Parameters:

  • timeout (optional) Maximum time to wait for the callback result. Uses java.time.Duration.
  • heartbeatTimeout (optional) Maximum time between heartbeat signals from the external system. If the external system does not send a heartbeat within this interval, the callback fails.
  • serDes (optional) Custom SerDes for the callback result. See Serialization.

Pass CallbackOption values to CreateCallback. The per-operation WithCallbackSerdes takes precedence over the handler-level WithCallbackDeserializer. When neither is set, the result passes through RawSerdes.

func WithCallbackTimeout(d time.Duration) CallbackOption
func WithCallbackHeartbeatTimeout(d time.Duration) CallbackOption
func WithCallbackSerdes(s Serdes) CallbackOption
func WithCallbackSubType(subType string) CallbackOption

Parameters:

  • WithCallbackTimeout Maximum time to wait for the result, as a time.Duration rounded up to whole seconds. Omit it or pass 0 to disable the timeout. A negative duration is an error. On expiry the callback fails with a *CallbackTimeoutError.
  • WithCallbackHeartbeatTimeout Maximum interval between heartbeats, with the same duration handling. On expiry the callback fails with a *CallbackTimeoutError whose Heartbeat field is true.
  • WithCallbackSerdes Deserializer for the callback result.
  • WithCallbackSubType The operation subtype recorded for the callback. It takes 1 to 32 characters from A-Z, a-z, 0-9, -, and _. An empty value selects the default, and the SDK's own subtypes are rejected. The subtype is part of the operation's identity on replay, so it must not change between invocations.

Call these methods on the builder that create_callback returns.

impl<O: Send + 'static, S> CreateCallbackBuilder<O, S> {
    // ...
    pub fn timeout(mut self, timeout: Duration) -> Self;
    // ...
    pub fn heartbeat_timeout(mut self, timeout: Duration) -> Self;
    // ...
    pub fn sub_type(mut self, sub_type: OperationSubType) -> Self;
    // ...
    pub fn serdes<S2>(self, serdes: S2) -> CreateCallbackBuilder<O, S2>
    where
        S2: Serdes<O>;
    // ...
}

Parameters:

  • timeout Maximum time to wait for the result, as a std::time::Duration rounded up to whole seconds and capped at i32::MAX seconds. Omit it or pass a zero duration to disable the timeout. On expiry the callback fails with durable::CallbackErrorKind::TimedOut.
  • heartbeat_timeout Maximum interval between heartbeats, with the same duration handling. On expiry the callback fails with durable::CallbackErrorKind::HeartbeatTimedOut.
  • sub_type The operation subtype recorded for the callback in place of Callback. durable::OperationSubType::new accepts 1 to 32 characters from A-Z, a-z, 0-9, -, and _, and returns a durable::InvalidOperationSubType error for any other value. The subtype is part of the operation's identity on replay, so it must not change between invocations.
  • serdes Deserializer for the callback payload, in place of the default durable::JsonSerdes. It must implement durable::Serdes<O>. With O set to String, durable::PassthroughSerdes returns the payload unchanged. See Serialization.
public class CallbackConfig
{
    public TimeSpan Timeout { get; set; }          // default TimeSpan.Zero (no timeout)
    public TimeSpan HeartbeatTimeout { get; set; } // default TimeSpan.Zero (no timeout)
}

Parameters:

  • Timeout (optional) Maximum time to wait for the callback result. TimeSpan.Zero (default) disables the overall timeout. Positive values must be at least 1 second.
  • HeartbeatTimeout (optional) Maximum time between heartbeat signals from the external system. If the external system does not send a heartbeat within this interval, the callback fails. TimeSpan.Zero (default) disables the heartbeat timeout.

The callback result is serialized with the ILambdaSerializer registered on ILambdaContext.Serializer; there is no per-callback serializer. See Serialization.

waitForCallback

waitForCallback is a composite operation that combines createCallback with a step that runs your submitter function. The submitter receives the callback ID and is responsible for sending it to the external system. If the submitter fails, the SDK retries it according to the step's retry strategy.

The external system sends callback results using the SendDurableExecutionCallbackSuccess or SendDurableExecutionCallbackFailure Lambda APIs.

Use waitForCallback when you want the SDK to handle the retry logic for submitting the callback ID rather than coding it yourself.

// Named overload
waitForCallback<TOutput = string>(
  name: string | undefined,
  submitter: (callbackId: string, context: WaitForCallbackContext) => Promise<void>,
  config?: WaitForCallbackConfig<TOutput>,
): DurablePromise<TOutput>;

// Anonymous overload
waitForCallback<TOutput = string>(
  submitter: (callbackId: string, context: WaitForCallbackContext) => Promise<void>,
  config?: WaitForCallbackConfig<TOutput>,
): DurablePromise<TOutput>;

Parameters:

  • name (optional) A name for the operation. Pass undefined to omit.
  • submitter A function that receives the callback ID and a WaitForCallbackContext, and submits the ID to the external system.
  • config (optional) A WaitForCallbackConfig<TOutput> object.

Returns: DurablePromise<TOutput>. Await to get the callback result.

Throws: CallbackError if the callback fails, times out, or the external system reports failure.

def wait_for_callback(
    self,
    submitter: Callable[[str, WaitForCallbackContext], None],
    name: str | None = None,
    config: WaitForCallbackConfig | None = None,
) -> Any: ...

Parameters:

  • submitter A callable that receives the callback ID and a WaitForCallbackContext, and submits the ID to the external system.
  • name (optional) A name for the operation.
  • config (optional) A WaitForCallbackConfig object.

Returns: The callback result.

Raises: CallbackError if the callback fails or times out.

// sync
<T> T waitForCallback(String name, Class<T> resultType, BiConsumer<String, StepContext> func)
<T> T waitForCallback(String name, Class<T> resultType, BiConsumer<String, StepContext> func, WaitForCallbackConfig config)

// async
<T> DurableFuture<T> waitForCallbackAsync(String name, Class<T> resultType, BiConsumer<String, StepContext> func)
<T> DurableFuture<T> waitForCallbackAsync(String name, Class<T> resultType, BiConsumer<String, StepContext> func, WaitForCallbackConfig config)

Parameters:

  • name (required) A nullable name for the operation.
  • resultType The Class<T> or TypeToken<T> for deserialization.
  • func A BiConsumer<String, StepContext> that receives the callback ID and submits it to the external system.
  • config (optional) A WaitForCallbackConfig object.

Returns: T (sync) or DurableFuture<T> (async via waitForCallbackAsync()).

Throws: CallbackFailedException if the external system reports failure. CallbackTimeoutException if the callback times out. CallbackSubmitterException if the submitter step fails after exhausting retries.

func WaitForCallback[O any](ctx Context, name string, submitter func(ctx StepContext, callbackID string) error, opts ...WaitForCallbackOption) (O, error)

Parameters:

  • ctx (required) The durable context, always the first argument.
  • name (required) A name for the operation. Pass "" to omit it.
  • submitter (required) func(ctx StepContext, callbackID string) error. It receives the callback ID and delivers it to the external system. The SDK runs it as a step and retries it on failure.
  • opts (optional) Variadic WaitForCallbackOption values. See WaitForCallbackConfig.

Returns: (O, error), the callback result. The result flows through RawSerdes by default. Pass durable.WithCallbackSerdes(durable.JSONSerdes) to decode it. The result serializer runs once, at the WaitForCallback operation. To run WaitForCallback concurrently, wrap it in durable.Go.

Errors: *CallbackExternalError, *CallbackTimeoutError, or *CallbackSubmitterError when the submitter step exhausts its retries. All three match *CallbackError. A result the deserializer cannot decode returns a *SerdesError whose Operation is the WaitForCallback name.

impl DurableContext {
    // ...

    pub fn wait_for_callback<O>(
        &self,
        submitter: impl Submitter,
    ) -> WaitForCallbackBuilder<O, impl Submitter>
    where
        O: Send + 'static;

    // ...
}

pub trait Submitter:
    FnOnce(StepContext, String) -> <Self as Submitter>::Fut + Clone + Send + 'static
{
    type Fut: Future<Output = Result<(), BoxError>> + Send + 'static;
}

// ...

pub struct WaitForCallbackBuilder<O, F, S = JsonSerdes> {
    // ...
}

// ...

impl<O, F, S> WaitForCallbackBuilder<O, F, S>
where
    O: Send + 'static,
    F: Submitter,
{
    pub fn name(mut self, name: impl Into<String>) -> Self;

    // ...
}

impl<O, F, S> WaitForCallbackBuilder<O, F, S>
where
    O: Send + 'static,
    F: Submitter,
    S: Serdes<O>,
{
    pub fn future(self) -> DurableFuture<O>;

    // ...

    pub fn spawn(self) -> DurableFuture<O>;

    // ...
}

impl<O, F, S> IntoFuture for WaitForCallbackBuilder<O, F, S>
where
    O: Send + 'static,
    F: Submitter,
    S: Serdes<O>,
{
    type Output = Result<O, OperationError>;
    // ...
}

Parameters:

  • O (required) The payload type, named with wait_for_callback::<O>. By default the SDK decodes the payload as JSON into O.
  • submitter (required) A closure that receives a durable::StepContext and the callback ID, and returns a future of Result<(), durable::BoxError> that delivers the ID to the external system. Every such closure that is Clone, Send, and 'static, and whose future is Send and 'static, implements durable::builders::Submitter. The SDK runs it as a step and retries it on failure. It is Clone because each attempt calls a fresh clone.
  • name (optional) A name for the operation, set with .name(). Omit the call to leave the operation unnamed.
  • The other builder methods are listed in WaitForCallbackConfig.

Returns: durable::builders::WaitForCallbackBuilder<O, F>. Await it to get Result<O, durable::OperationError>, the callback result. .future() returns a lazy durable::DurableFuture<O> without starting the operation. .spawn() starts the operation at once and returns the future to await later.

Errors: A durable::OperationError whose kind() is durable::OperationErrorKind::Callback. Its durable::CallbackErrorKind is ExternalFailure, TimedOut, HeartbeatTimedOut, SubmitterFailed when the submitter step exhausts its retries, DeserializationFailed for a payload that does not decode into O, or Internal for another SDK or service failure. For ExternalFailure, err.wire() has type Callback.ExternalFailure and keeps the external data and message, or Callback failed when the message is absent or empty. For TimedOut and HeartbeatTimedOut, err.wire() has type Callback.Timeout or Callback.Heartbeat and keeps the recorded data and message, or Callback timed out when the message is absent or empty. A DeserializationFailed error repeats on every invocation that reads the recorded payload.

Replay can also return a durable::OperationError of kind durable::OperationErrorKind::NonDeterministicExecution when the recorded operation at this point does not match.

Task<T> WaitForCallbackAsync<T>(
    Func<string, IWaitForCallbackContext, CancellationToken, Task> submitter,
    string? name = null,
    WaitForCallbackConfig? config = null,
    CancellationToken cancellationToken = default);

Parameters:

  • submitter A function that receives the callback ID, an IWaitForCallbackContext, and a CancellationToken, and submits the ID to the external system.
  • name (optional) A name for the operation. Omit it to infer one from the call site.
  • config (optional) A WaitForCallbackConfig object.
  • cancellationToken (optional) A token to observe for cancellation.

Returns: Task<T>. Use await to get the callback result.

Throws: CallbackFailedException if the external system reports failure. CallbackTimeoutException if the callback times out. CallbackSubmitterException if the submitter step fails after exhausting retries.

WaitForCallbackConfig

WaitForCallbackConfig extends CallbackConfig with retry configuration for the submitter step.

interface WaitForCallbackConfig<TOutput = string> {
  timeout?: Duration;
  heartbeatTimeout?: Duration;
  retryStrategy?: (error: Error, attemptCount: number) => RetryDecision;
  serdes?: Omit<Serdes<TOutput>, "serialize">;
}
  • retryStrategy (optional) A function returning a RetryDecision for the submitter step. See Retry strategies.
@dataclass(frozen=True)
class WaitForCallbackConfig(CallbackConfig):
    retry_strategy: Callable[[Exception, int], RetryDecision] | None = None
  • retry_strategy (optional) A callable returning a RetryDecision for the submitter step. See Retry strategies.
WaitForCallbackConfig.builder()
    .stepConfig(StepConfig)       // optional
    .callbackConfig(CallbackConfig) // optional
    .build()
  • stepConfig (optional) A StepConfig for the submitter step, including retry strategy. See Retry strategies.
  • callbackConfig (optional) A CallbackConfig for the callback wait.

WaitForCallback accepts every CallbackOption plus one submitter-only option.

func WithSubmitterRetry(s RetryStrategy) WaitForCallbackOption
  • WithSubmitterRetry A retry strategy for the submitter step. Omit it and the submitter retries with the step default, exponential backoff. WithSubmitterRetry is not a CallbackOption, so passing it to CreateCallback is a compile error.

The builder that wait_for_callback returns has timeout, heartbeat_timeout, and serdes, as described in CallbackConfig, and two methods for the submitter step.

impl<O, F, S> WaitForCallbackBuilder<O, F, S>
where
    O: Send + 'static,
    F: Submitter,
{
    // ...
    pub fn submitter_retry<R>(mut self, strategy: R) -> Self
    where
        R: Fn(&crate::StepError, u32) -> crate::RetryDecision + Send + Sync + 'static;
    // ...
    pub fn submitter_retry_config(mut self, config: crate::builders::RetryStrategyConfig) -> Self;
    // ...
}
  • submitter_retry A function that receives the durable::StepError and the number of the attempt that failed, starting at 1, and returns a durable::RetryDecision for the submitter step. See Retry strategies.
  • submitter_retry_config A durable::builders::RetryStrategyConfig for the submitter step. The last call to either method wins. Omit both and the submitter retries with the default step strategy, 6 attempts with exponential backoff.
public class WaitForCallbackConfig : CallbackConfig
{
    public IRetryStrategy? RetryStrategy { get; set; } // null = no retry
}
  • RetryStrategy (optional) An IRetryStrategy applied to the submitter step. When null (default), submitter failures are not retried. Use the RetryStrategy factory (for example RetryStrategy.Exponential(...)) to build one. See Retry strategies.
  • Inherits Timeout and HeartbeatTimeout from CallbackConfig for the callback wait.

Timeout configuration

Set a timeout to limit the duration the function waits for the external system. If the timeout expires before the external system calls back, the SDK raises a CallbackTimeoutException (Java) or CallbackError (TypeScript, Python) from the result call.

The timeout is bound by the ExecutionTimeout you set on the DurableConfig. The ExecutionTimeout applies to the entire durable execution, not individual function invocations.

The durable execution terminates with a timeout error if the callback does not receive a response within the ExecutionTimeout.

Since the durable function suspends and does not consume compute during the wait, the callback timeout can exceed the Lambda function invocation timeout.

Set a heartbeatTimeout when the external system can send periodic heartbeat signals during long-running work. If the external system stops sending heartbeats within the interval, the callback fails before the main timeout expires. Heartbeat timeouts can help detect stalled external systems sooner.

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

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    const [callbackPromise, callbackId] = await context.createCallback(
      "wait-for-payment",
      {
        timeout: { hours: 24 },
        heartbeatTimeout: { minutes: 30 },
      },
    );

    await submitPaymentRequest(callbackId, event.amount);
    return await callbackPromise;
  },
);
from typing import Any

from aws_durable_execution_sdk_python import DurableContext, durable_execution
from aws_durable_execution_sdk_python.config import CallbackConfig, Duration


@durable_execution
def handler(event: Any, context: DurableContext) -> dict:
    config = CallbackConfig(
        timeout=Duration.from_hours(24),
        heartbeat_timeout=Duration.from_minutes(30),
    )
    callback = context.create_callback(name="wait-for-payment", config=config)

    submit_payment_request(callback.callback_id, event["amount"])
    return callback.result()
import java.time.Duration;
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;

public class CallbackConfigExample extends DurableHandler<Object, String> {
    @Override
    public String handleRequest(Object input, DurableContext context) {
        CallbackConfig config = CallbackConfig.builder()
                .timeout(Duration.ofHours(24))
                .heartbeatTimeout(Duration.ofMinutes(30))
                .build();

        DurableCallbackFuture<String> callback =
                context.createCallback("wait-for-payment", String.class, config);

        submitPaymentRequest(callback.callbackId());
        return callback.get();
    }
}
package main

import (
    "encoding/json"
    "time"

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

type paymentRequest struct {
    Amount int `json:"amount"`
}

func handler(ctx durable.Context, event paymentRequest) (json.RawMessage, error) {
    cb, err := durable.CreateCallback[json.RawMessage](ctx, "wait-for-payment",
        durable.WithCallbackTimeout(24*time.Hour),
        durable.WithCallbackHeartbeatTimeout(30*time.Minute),
    )
    if err != nil {
        return nil, err
    }

    if err := submitPaymentRequest(cb.ID(), event.Amount); err != nil {
        return nil, err
    }
    return cb.Result(ctx)
}

func submitPaymentRequest(callbackID string, amount int) error {
    return nil
}

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

On expiry, callback.result() returns a durable::OperationError whose durable::CallbackErrorKind is TimedOut, or HeartbeatTimedOut for a missed heartbeat.

use std::time::Duration;

use aws_durable_execution_sdk as durable;
use serde::Deserialize;
use serde_json::Value;

#[derive(Deserialize)]
struct PaymentEvent {
    amount: f64,
}

async fn handler(
    event: PaymentEvent,
    ctx: durable::DurableContext,
) -> Result<Value, durable::BoxError> {
    let callback = ctx
        .create_callback::<Value>()
        .name("wait-for-payment")
        .timeout(Duration::from_hours(24))
        .heartbeat_timeout(Duration::from_mins(30))
        .await?;

    submit_payment_request(callback.id(), event.amount);
    Ok(callback.result().await?)
}

fn submit_payment_request(_callback_id: &str, _amount: f64) {}

#[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 CallbackConfigExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<PaymentEvent, string>(Workflow, input, context);

    private async Task<string> Workflow(PaymentEvent input, IDurableContext ctx)
    {
        var config = new CallbackConfig
        {
            Timeout = TimeSpan.FromHours(24),
            HeartbeatTimeout = TimeSpan.FromMinutes(30),
        };

        ICallback<string> callback = await ctx.CreateCallbackAsync<string>(
            "wait-for-payment", config);

        await SubmitPaymentRequestAsync(callback.CallbackId, input.Amount);
        return await callback.GetResultAsync();
    }

    private static Task SubmitPaymentRequestAsync(string callbackId, decimal amount)
        => Task.CompletedTask;
}

public record PaymentEvent(decimal Amount);

Naming callbacks

Name callbacks to make them easier to identify in logs and tests. Use a name that describes what the callback is waiting for.

The name is the first argument. Pass undefined to omit it.

Pass name as a keyword argument.

The name is always the first argument. Pass null to omit it.

The name is the second argument, after the context. Pass "" to omit it.

Set the name with .name() on the builder. Omit the call to leave the callback unnamed.

The name is the optional name argument. Omit it to infer one from the call site.

Send callback results

The calling principal needs permission to send callbacks for the durable execution. See Security and permissions for Lambda durable functions in the AWS Lambda Developer Guide.

External systems send results back using the SendDurableExecutionCallbackSuccess or SendDurableExecutionCallbackFailure Lambda APIs. The callback ID returned from createCallback or passed into waitForCallback is the key that routes the result back to the waiting function. For long-running callbacks, external systems can send periodic SendDurableExecutionCallbackHeartbeat signals to prevent the heartbeatTimeout from expiring.

Most production integrations call these APIs using the AWS SDK for your programming language. You can also send callbacks from the Console, the AWS CLI, or the SAM CLI. In the Console, open the Durable Executions tab, select the running execution, find the callback under Durable Operations, and use the Actions drop-down to send a response.

AWS CLI

The AWS CLI sends callbacks directly to the Lambda backend and requires AWS credentials.

Success:

aws lambda send-durable-execution-callback-success \
  --callback-id <callback-id> \
  --cli-binary-format raw-in-base64-out \
  --result '{"status":"approved"}'

--result is a blob. AWS CLI v2 expects blobs as base64 by default, so pass --cli-binary-format raw-in-base64-out to send a plain string inline, or use fileb://result.json to read from a file.

Failure:

aws lambda send-durable-execution-callback-failure \
  --callback-id <callback-id> \
  --error ErrorType=PaymentDeclined,ErrorMessage="Insufficient funds"

The --error value accepts shorthand syntax (Key=value,...) or JSON, and supports ErrorType, ErrorMessage, ErrorData, and StackTrace.

SAM CLI

The SAM CLI provides two subcommands depending on where your function is running.

sam local callback sends the callback to the SAM local runner. Use this during local development when your function is running via sam local start-lambda. It does not require AWS credentials.

sam remote callback sends the callback to the actual Lambda backend. Use this when your function is deployed to AWS. It requires AWS credentials.

Success:

# local runner
sam local callback succeed <callback-id> --result '{"status":"approved"}'

# Lambda backend
sam remote callback succeed <callback-id> --result '{"status":"approved"}'

Failure:

# local runner
sam local callback fail <callback-id> \
  --error-type "PaymentDeclined" \
  --error-message "Insufficient funds"

# Lambda backend
sam remote callback fail <callback-id> \
  --error-type "PaymentDeclined" \
  --error-message "Insufficient funds"

See also

Checkpoint consumption

Durable operations consume checkpoints. To understand how this operation affects your checkpoint usage, see Checkpoint consumption.