durable

package
v0.0.0-...-968ce03 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 20, 2026 License: Apache-2.0 Imports: 37 Imported by: 0

Documentation

Overview

Package durable provides the AWS Lambda Durable Functions programming model for Go.

A durable function executes as a series of checkpointed operations. Completed operation results are persisted, and after a suspension or interruption the function replays: previously completed operations return their stored results without re-executing, and execution continues from the first incomplete operation.

Handlers receive a Context in place of the standard Lambda context and invoke durable operations through the package-level generic functions such as Step, Wait, Invoke, Map, and Parallel. Blocking variants return results directly. Async variants return a Future and run the operation body concurrently.

Determinism

Replay pairs stored results with operations positionally, so code between checkpoints must be a pure function of the handler input and previously checkpointed results. Durable operations must be created in a deterministic order on each context. Use Go to fan out durable work instead of the go statement, and do not create durable operations while iterating a map (map iteration order is randomized; sort keys first). Nondeterminism inside a step body is safe: the step's checkpointed result is frozen once persisted.

Goroutine Ownership

Every Context is owned by the goroutine it was created on: the handler goroutine owns the root Context, and a child goroutine started by Go or RunInChildContextAsync owns the child Context it receives. Durable operations on a Context must be invoked from its owning goroutine. Two goroutines claiming operations on one Context would claim them in a scheduling-dependent order, and replay would then pair stored results with the wrong operations.

The rule is enforced at run time. Every durable operation checks the calling goroutine against the Context's owner before it claims an operation ID, and a call from any other goroutine fails with ErrWrongGoroutine without claiming an ID or recording a checkpoint. The check costs a few microseconds per operation (see the benchmark in the package tests); build with -tags durablenocheck to compile it out, at the price of leaving foreign-goroutine calls undetected.

Go is the replay-safe way to run durable work concurrently. It claims the child's operation ID on the calling goroutine, so the order is deterministic, and then starts a goroutine that owns a fresh child Context. Use the child Context inside the function, never the parent:

fut := durable.Go(ctx, "work", func(child durable.Context) (T, error) {
	return durable.Step(child, "step", func(durable.StepContext) (T, error) {
		return doWork()
	})
})
result, err := fut.Result()

Awaiting Several Futures

Await a set of futures with a combinator, not with a sequence of Future.Result calls. All, AllSettled, Any, and Race take futures of one result type. Join takes futures of different result types through the Awaitable interface, waits for all of them, and returns the first error in argument order; the values are then read with Result.

fa := durable.StepAsync(ctx, "charge", chargeCard)
fb := durable.Go(ctx, "notify", notifyWarehouse)
if err := durable.Join(ctx, "settle", []durable.Awaitable{fa, fb}); err != nil {
	return err
}
receipt, _ := fa.Result()
ok, _ := fb.Result()

The hand-written form below looks equivalent but is not:

a, err := fa.Result()
if err != nil {
	return err
}
b, err := fb.Result()

When fa's branch suspends, fa.Result returns the suspension signal and the handler returns before awaiting fb. fb's branch therefore never reaches its blocking point in this invocation and its progress is not checkpointed. The combinators drain instead: once one future reports a suspension they await every remaining future, so each branch checkpoints as far as it can, and only then propagate the suspension. The defect appears only when a branch suspends, so it passes every test in which no branch suspends.

Error Propagation

Return an error from a durable operation immediately unless the handler intentionally handles it as a business-level outcome. A nil error means the operation completed, either live or on replay. A non-nil error is one of two things: a documented terminal failure of that operation, or a signal that the invocation is suspending and the handler should unwind.

The two are distinguishable. Every terminal failure is matchable with errors.As against a public type such as StepError or InvokeError, and all of them also match OperationError. A suspension signal matches none of them. An error that matches no public type therefore MUST be returned unchanged.

// Correct: handle a documented terminal failure, propagate anything else.
result, err := durable.Step(ctx, "charge", func(sc durable.StepContext) (Receipt, error) {
	return chargeCard(sc, order)
})
var stepErr *durable.StepError
switch {
case err == nil:
	// Use result.
case errors.As(err, &stepErr) && stepErr.ErrorType == "CardDeclinedError":
	// A business-level decision belongs here.
default:
	return OrderResult{}, err
}

A typed failure never carries the original error value returned by the operation body. Its Err field is a stand-in rebuilt from the recorded ErrorType and Message, on the first invocation and on replay alike, so errors.As against the handler's own error types is always false. Match on the ErrorType field instead, as above. Structured data travels with the failure through WithErrorData. See OperationError.

Wait is stricter. Its error carries no business-level terminal result, so return it immediately in every case.

If code swallows a suspension signal and continues, the invocation still suspends and does not produce an incorrect result, but statements after the swallowed error execute and subsequent durable operations on the same context refuse to proceed. The handler appears to run past the blocking point while later operations fail.

Struct Literals

Exported configuration and result structs such as RetryConfig, ConditionConfig, Branch, BatchItem, and Settled begin with a blank zero-size field of type [0]func(). The field lets the SDK add fields to these structs later without breaking user code. It has four visible effects:

  • An unkeyed literal such as Branch[string]{"name", fn} fails to compile outside this package. Use keyed fields: Branch[string]{Name: "name", Func: fn}.
  • The struct is not comparable. Operators and functions that require a comparable type, such as ==, slices.Contains, and slices.Index, do not compile over it. The *Func variants such as slices.ContainsFunc still work. This is intended, because adding a slice or func field later would otherwise remove comparability and break callers.
  • The fmt verb %+v prints the blank field as _:[].
  • Copying, JSON encoding, reflect.DeepEqual, errors.Is, errors.As, and use as a map value are unaffected.

Index

Constants

View Source
const (
	// OperationSubTypeStep is the subtype of a [Step] or [StepAsync]
	// operation. Its type is [OperationTypeStep].
	OperationSubTypeStep = "Step"

	// OperationSubTypeWait is the subtype of a [Wait] or [WaitAsync]
	// operation. Its type is [OperationTypeWait].
	OperationSubTypeWait = "Wait"

	// OperationSubTypeCallback is the subtype of a [CreateCallback]
	// operation, including the callback that [WaitForCallback] creates
	// inside its context. Its type is [OperationTypeCallback].
	OperationSubTypeCallback = "Callback"

	// OperationSubTypeChainedInvoke is the subtype of an [Invoke] or
	// [InvokeAsync] operation. Its type is [OperationTypeChainedInvoke].
	OperationSubTypeChainedInvoke = "ChainedInvoke"

	// OperationSubTypeRunInChildContext is the default subtype of a
	// [RunInChildContext], [RunInChildContextAsync], or [Go] operation;
	// [WithChildSubType] records a caller-defined subtype instead. Its
	// type is [OperationTypeContext].
	OperationSubTypeRunInChildContext = "RunInChildContext"

	// OperationSubTypeWaitForCallback is the subtype of the context
	// operation that [WaitForCallback] records around its callback and
	// submitter step. Its type is [OperationTypeContext].
	OperationSubTypeWaitForCallback = "WaitForCallback"

	// OperationSubTypeWaitForCondition is the subtype of a
	// [WaitForCondition] operation. Its type is [OperationTypeStep].
	OperationSubTypeWaitForCondition = "WaitForCondition"

	// OperationSubTypeMap is the subtype of the parent operation a [Map]
	// call records. Its type is [OperationTypeContext].
	OperationSubTypeMap = "Map"

	// OperationSubTypeMapIteration is the subtype of the child operation
	// recorded for each item of a [Map]. Its type is
	// [OperationTypeContext], and its parent is the Map operation.
	OperationSubTypeMapIteration = "MapIteration"

	// OperationSubTypeParallel is the subtype of the parent operation a
	// [Parallel] call records. Its type is [OperationTypeContext].
	OperationSubTypeParallel = "Parallel"

	// OperationSubTypeParallelBranch is the subtype of the child operation
	// recorded for each branch of a [Parallel]. Its type is
	// [OperationTypeContext], and its parent is the Parallel operation.
	OperationSubTypeParallelBranch = "ParallelBranch"
)

Operation subtypes.

Every durable operation the SDK records carries a type and a subtype. The type is one of the OperationType constants and says what kind of record the service keeps: a step, a wait, a callback, a chained invoke, or a context. The subtype names the SDK function that created the operation and distinguishes functions that share a type: Map, Parallel, RunInChildContext, and WaitForCallback all record OperationTypeContext operations, and each Map item or Parallel branch is itself a context operation with its own subtype. A child context is the one operation whose subtype the caller may set, with WithChildSubType.

These constants are the values the SDK writes to the SubType field of each checkpointed operation and reports in OperationHookInfo.SubType. They are untyped string constants so they compare directly with those string fields. Plugins should compare against these constants rather than string literals.

View Source
const (
	// DefaultPreviewMaskString is the replacement for masked values when
	// [PreviewConfig.MaskString] is empty.
	DefaultPreviewMaskString = "***"

	// DefaultPreviewMaxBytes is the preview size cap when
	// [PreviewConfig.MaxPreviewBytes] is zero.
	DefaultPreviewMaxBytes = 4096

	// DefaultPreviewMaxDepth is the traversal depth bound when
	// [PreviewConfig.MaxDepth] is zero.
	DefaultPreviewMaxDepth = 32

	// PreviewTruncatedKey is the key [BuildPreview] adds, with the value
	// true, when the size cap left some fields out of the preview. A field
	// of the value with this name is overwritten by the marker when
	// truncation occurs.
	PreviewTruncatedKey = "$truncated"
)
View Source
const DefaultRetryDelay = time.Second

DefaultRetryDelay is the delay before the next attempt when a RetryStrategy returns a RetryDecision with Retry set and a zero Delay. It is also the smallest delay the SDK sends: a positive fractional Delay rounds up to one second.

View Source
const MaxStackTraceFrames = 32

MaxStackTraceFrames is the most frames the SDK records in a failure's stack trace. Frames are counted from the point of failure outward, so a deeper stack loses its outermost frames. The bound keeps a deep stack from inflating a checkpoint past the operation size limit.

View Source
const Version = "0.1.0"

Version is the semantic version of the aws-durable-execution-sdk-go module.

Variables

View Source
var ErrCallbackTimedOut = errors.New("durable: callback timed out")

ErrCallbackTimedOut indicates that a callback's timeout elapsed before an external system submitted a result.

View Source
var ErrExecutionCancelled = errors.New("durable: execution cancelled")

ErrExecutionCancelled indicates that a durable execution was cancelled.

View Source
var ErrExecutionStopped = errors.New("durable: execution stopped")

ErrExecutionStopped indicates that a durable execution was explicitly stopped via the StopDurableExecution API.

View Source
var ErrInvokeTimedOut = errors.New("durable: invoke timed out")

ErrInvokeTimedOut indicates that an invoked function's execution timed out before producing a result.

View Source
var ErrWrongGoroutine = errors.New(
	"durable: operation called from a goroutine that does not own the context; use durable.Go for concurrent durable work")

ErrWrongGoroutine is returned when a durable operation is invoked on a Context from a goroutine other than the one that owns that Context. Every operation that claims an operation ID performs the check, so every exported operation on a Context can return it: Step, StepAsync, Wait, WaitAsync, Invoke, InvokeAsync, RunInChildContext, RunInChildContextAsync, Go, CreateCallback, WaitForCallback, WaitForCondition, Map, Parallel, All, AllSettled, Any, and Race. The check runs before the operation claims its operation ID, so a rejected call consumes no ID and records no checkpoint. An Async operation reports the error through the returned Future. Match it with errors.Is; the returned error wraps ErrWrongGoroutine and names the owner and caller goroutines.

Every Context is owned by the goroutine it was created on: the handler goroutine for the root Context, and the child goroutine for a Context created by Go or RunInChildContextAsync. Durable operations must be claimed in a deterministic order for replay to work, which requires confining each Context to one goroutine. Use Go to run durable work concurrently.

The check is enabled in every default build. Building with -tags durablenocheck compiles it out; in that build ErrWrongGoroutine is declared but never returned, and a foreign-goroutine call is not detected. If the runtime does not expose the goroutine identity (see goid), the check is disabled at runtime rather than rejecting correct programs.

View Source
var JSONSerdes = jsonSerdes{}

JSONSerdes is the default Serdes. It encodes with json.Marshal and decodes with json.Unmarshal. The SDK uses it for every operation result when no serializer option is supplied: no handler-wide WithSerdes, no per-operation option such as WithStepSerdes, and no ConfigureSerdes call in the handler.

JSONSerdes holds no state, so it is safe for concurrent use from any number of goroutines and executions.

Use it to write a custom serdes that handles a few types itself and defers everything else to the default encoding. The serdes below stores a time.Time as Unix nanoseconds and leaves every other type to JSONSerdes:

type unixTimeSerdes struct{}

func (unixTimeSerdes) Marshal(ctx context.Context, meta durable.SerdesContext, v any) ([]byte, error) {
	if t, ok := v.(time.Time); ok {
		return []byte(strconv.FormatInt(t.UnixNano(), 10)), nil
	}
	return durable.JSONSerdes.Marshal(ctx, meta, v)
}

func (unixTimeSerdes) Unmarshal(ctx context.Context, meta durable.SerdesContext, data []byte, v any) error {
	if t, ok := v.(*time.Time); ok {
		ns, err := strconv.ParseInt(string(data), 10, 64)
		if err != nil {
			return err
		}
		*t = time.Unix(0, ns)
		return nil
	}
	return durable.JSONSerdes.Unmarshal(ctx, meta, data, v)
}

durable.Start(handler, durable.WithSerdes(unixTimeSerdes{}))

Functions

func All

func All[O any](ctx Context, name string, fs []*Future[O], opts ...ChildOption) ([]O, error)

All records a combinator operation and waits for every future to succeed, returning the values in input order. A non-suspension error observed before any suspension fails All immediately with that error, without awaiting the remaining futures (matching Promise.all's fail-fast rejection). Once a suspension is observed, All awaits all remaining futures so their branches reach blocking points and checkpoint progress, then propagates the suspension; suspension takes precedence over terminal outcomes observed after it because the suspended branch completes only on a later invocation.

All uses RunInChildContext internally, so the aggregate result is checkpointed: on replay, the stored result is returned without re-awaiting the futures. opts configure that child-context operation; WithChildSerdes selects the serializer for the aggregate result.

Empty input returns an empty slice immediately (matching Promise.all([])).

func Any

func Any[O any](ctx Context, name string, fs []*Future[O], opts ...ChildOption) (O, error)

Any records a combinator operation and returns the value of the first future to succeed. A success observed before any suspension returns immediately, without awaiting the remaining futures. Once a suspension is observed, Any awaits all remaining futures so their branches reach blocking points, then propagates the suspension; suspension takes precedence over terminal outcomes observed after it because the suspended branch completes only on a later invocation. If every future fails with a non-suspension error, Any returns a *CombinatorError wrapping all individual errors.

Any uses RunInChildContext internally, so the winning result is checkpointed: on replay, the same winner is returned deterministically regardless of future settlement order. opts configure that child-context operation; WithChildSerdes selects the serializer for the winner.

Empty input fails immediately with a *CombinatorError (no futures can succeed), matching Promise.any([]).

func BuildPreview

func BuildPreview(value any, cfg PreviewConfig) map[string]any

BuildPreview returns a compact, redacted view of value for storage alongside a checkpoint reference, such as the one written by NewFileSystemSerdes. The FileSystemSerdesConfig.GeneratePreview hook is the intended caller.

A preview is advisory metadata. It exists so that an operation log can show what an offloaded value contained without reading it back. Masking or excluding a field here changes only the preview. The offloaded payload itself is stored in full, so a value that must not be persisted has to be removed before the operation returns it.

The value is first encoded with encoding/json, so fields are named and omitted exactly as the checkpoint would name and omit them. The encoded tree is then walked and each scalar field is kept, masked, or dropped according to cfg. Fields are compared with the selectors by their dot-separated path from the root:

  • Exclude wins. An excluded field is never shown, even if it is also in Mask or Include, and its children are not visited.
  • Mask implies visibility. A masked field is shown as PreviewConfig.MaskString under either mode unless it is excluded.
  • Otherwise PreviewIncludeAll shows the field and PreviewExcludeAll shows it only if it matches Include.

A selector that matches an object or slice with FieldMatchAnywhere also matches every field below it, because the name is a segment of each descendant's path. The same selector with FieldMatchPath matches only that one path, so under PreviewExcludeAll including a container by path shows nothing until its fields are included as well.

Slices do not appear as slices in the preview. The fields of each element are merged under the slice's own path, and when elements have different shapes at one path the later element overwrites the earlier one. A slice of scalars is therefore omitted, since its elements have no field name to merge under.

The result is capped at PreviewConfig.MaxPreviewBytes of JSON. When the cap leaves fields out, the preview carries PreviewTruncatedKey set to true so truncation is visible. Traversal stops at PreviewConfig.MaxDepth; deeper objects and slices are omitted.

BuildPreview returns nil when value encodes to something other than a JSON object or array, when no field is visible, when nothing fits within the cap, or when value cannot be encoded. A cyclic value cannot be encoded, so it yields nil rather than a panic or unbounded recursion. The returned map is safe to modify.

func ConfigureLogging

func ConfigureLogging(ctx Context, cfg LogConfig) error

ConfigureLogging replaces the logging settings for the rest of the invocation. It is the in-handler counterpart of WithLogHandler and WithReplayLogMode, for a handler that must choose its logger from the event payload or from runtime configuration rather than at construction time, or that wants replayed log output only for one execution.

The new settings apply to ctx and to every child context and concurrent branch derived from ctx after the call. A context derived before the call, such as a branch already started with Go, keeps the settings it was derived with. A new handler reaches loggers obtained from Context.Logger or StepContext.Logger after the call; a logger obtained before the call keeps the previous handler. A new ReplayLogMode reaches every logger of ctx and of contexts derived from it after the call, including loggers obtained before the call, because the mode is read on every record.

The settings last for the current invocation only. The next invocation of the execution starts from the construction-time options again. The handler body runs from its start on every invocation, so a call placed before the first durable operation re-applies the settings each time.

ConfigureLogging does not affect determinism. It claims no operation ID, writes no checkpoint, and changes no operation's ordering or result, so it may be called conditionally and at different points on different invocations without causing a non-deterministic replay. This differs from ConfigureSerdes, whose settings are baked into checkpoint content.

ConfigureLogging must be called on the goroutine that owns ctx, like every durable operation; from any other goroutine it fails with ErrWrongGoroutine. It returns an error only for that case and for a Context not created by the SDK.

func handler(ctx durable.Context, event OrderEvent) (OrderResult, error) {
	if event.Debug {
		if err := durable.ConfigureLogging(ctx, durable.LogConfig{
			Handler:       slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}),
			ReplayLogMode: durable.ReplayLogModeEmit,
		}); err != nil {
			return OrderResult{}, err
		}
	}
	// Records from here on, including from replayed steps, reach the
	// text handler; replayed records carry replay=true.
	...
}

func ConfigureSerdes

func ConfigureSerdes(ctx Context, cfg SerdesConfig) error

ConfigureSerdes replaces the handler-level serializer defaults for the rest of the invocation. It is the in-handler counterpart of WithSerdes and WithCallbackDeserializer, for a handler that must choose its serializer from the event payload rather than at construction time.

The new defaults apply to operations started on ctx after the call and to every child context and concurrent branch derived from ctx after the call. A context derived before the call, such as a branch already started with Go, keeps the configuration it was derived with. Per-operation options such as WithStepSerdes and WithCallbackSerdes continue to take precedence over the configured defaults, exactly as they do over the construction-time options. The handler's own input and result are always JSON and are not affected.

ConfigureSerdes must run identically on every invocation of an execution, including replays. A step's result is written to the checkpoint with the serdes in effect when the step first runs, and read back from the checkpoint with the serdes in effect when the step is replayed. If those differ, the replay cannot decode the checkpointed bytes and the operation fails with a SerdesError; the execution cannot recover, because the checkpoint is fixed. So decide the configuration from inputs that are the same on every invocation: the event payload, or a value already returned by a durable operation. Never decide it from wall-clock time, random values, environment that can change between invocations, or the outcome of a non-durable call. For the same reason, call ConfigureSerdes at the same point in the handler on every invocation, normally before the first durable operation.

ConfigureSerdes must be called on the goroutine that owns ctx, like every durable operation; from any other goroutine it fails with ErrWrongGoroutine. It returns an error only for that case and for a Context not created by the SDK.

func handler(ctx durable.Context, event OrderEvent) (OrderResult, error) {
	if event.Compressed {
		if err := durable.ConfigureSerdes(ctx, durable.SerdesConfig{Serdes: gzipSerdes{}}); err != nil {
			return OrderResult{}, err
		}
	}
	// Every operation from here on uses gzipSerdes unless it passes
	// its own serdes option.
	...
}

func ErrorFromObject

func ErrorFromObject(obj *ErrorObject) error

ErrorFromObject rebuilds the typed error a recorded failure represents. obj is the wire error record the SDK writes for a failed operation, as read back from checkpoint state ([Operation.Error], StepDetails.Error, and the other details types). Tooling that reads checkpoint state uses it to match on Go types instead of on ErrorType strings:

err := durable.ErrorFromObject(op.Error)
var stepErr *durable.StepError
if errors.As(err, &stepErr) { ... }

The result is determined by obj's ErrorType:

Every result satisfies errors.As against *OperationError, and the OperationError reached that way reports the record's ErrorMessage as Message. A nil obj yields nil: it records no failure.

func ExecutionStartTime

func ExecutionStartTime(ctx Context) time.Time

ExecutionStartTime returns the start timestamp of the durable execution. This is the checkpointed start time of the root EXECUTION operation, recorded when the execution was created. It is the same value on every invocation of one execution (including replays), making it safe to use between durable operations without introducing non-determinism.

For a wall-clock timestamp that varies across invocations, compute it inside a Step so it is checkpointed once and reused verbatim on replay.

ExecutionStartTime is a package-level function rather than a Context method because Context is sealed and kept minimal: it exposes only Context.ExecutionArn (required by the SDK for operation identity on every checkpoint call) while derived conveniences that read from the checkpoint state—like start time—live as package functions layered on top.

func Invoke

func Invoke[O, I any](ctx Context, name, functionID string, input I, opts ...InvokeOption) (O, error)

Invoke durably invokes another Lambda function and returns its result. The invoked function runs as its own durable execution: the calling execution suspends after starting it and resumes when it completes. The output type parameter is specified by the caller and the input type is inferred:

receipt, err := durable.Invoke[Receipt](ctx, "charge", paymentFnArn, order)

functionID is a function name or ARN. Durable target functions require a version or alias qualifier. name identifies the operation for tracking and debugging; pass "" for an unnamed invoke.

If the invoked function fails, Invoke returns an *InvokeError.

func IsCheckpointRetryable

func IsCheckpointRetryable(err error) bool

IsCheckpointRetryable reports whether err (or any error in its chain) is a retryable checkpoint failure. Returns false for nil.

func Join

func Join(ctx Context, name string, fs []Awaitable, opts ...ChildOption) error

Join records a combinator operation and waits for every future to settle, then returns the first non-nil error in argument order, or nil when all succeeded. Values are read from each future afterwards with Future.Result, which returns immediately once Join has returned nil. Join is the replacement for awaiting several futures by hand: a hand-written sequence of Result calls that returns on the first error leaves the remaining futures unawaited, so their branches never reach a blocking point when the invocation suspends and their progress is not checkpointed.

Join never abandons a future: a durable branch has no cancellation, so an error observed early does not stop the remaining futures from being awaited. Once a suspension is observed, Join awaits all remaining futures so their branches reach blocking points and checkpoint progress, then propagates the suspension; suspension takes precedence over terminal outcomes because the suspended branch completes only on a later invocation. On the resume invocation the futures replay and the first error is returned then. This matches the draining behaviour of All.

Join uses RunInChildContext internally, so its outcome is checkpointed: on replay, the stored outcome is returned without re-awaiting the futures, and a failure is returned as a *ChildContextError wrapping the first error. opts configure that child-context operation.

Empty input returns nil immediately.

fa := durable.StepAsync(ctx, "charge", chargeCard)
fb := durable.Go(ctx, "notify", notifyWarehouse)
if err := durable.Join(ctx, "settle", []durable.Awaitable{fa, fb}); err != nil {
	return err
}
receipt, _ := fa.Result()
ok, _ := fb.Result()

func Race

func Race[O any](ctx Context, name string, fs []*Future[O], opts ...ChildOption) (O, error)

Race records a combinator operation and returns the outcome of the first future to settle with a terminal result (success or non-suspension error). A terminal outcome observed before any suspension returns immediately, without awaiting the remaining futures. Once a suspension is observed, Race awaits all remaining futures so their branches reach blocking points, then propagates the suspension; suspension takes precedence over terminal outcomes observed after it because the suspended branch completes only on a later invocation.

Race uses RunInChildContext internally, so the winner is checkpointed: on replay, the same outcome is returned deterministically regardless of future settlement order. opts configure that child-context operation; WithChildSerdes selects the serializer for the winner.

Race returns the winner's value but not which future produced it. Code that must branch on the winner's identity should use Select, which runs named branches and checkpoints the winner's name with its value.

Empty input suspends (no future will ever settle), matching Promise.race([]) which returns a forever-pending promise.

func Retry

func Retry[O any](ctx Context, name string, fn func(ctx Context, attempt int) (O, error), strategy RetryStrategy, opts ...RetryOption) (O, error)

Retry runs fn until it succeeds or strategy stops retrying, suspending the execution between attempts. It is retry for a group of durable operations: where Step retries one function that must not create durable operations, fn may call Step, Wait, Invoke, WaitForCallback, or any other operation, and a failed attempt re-runs the whole of fn from its beginning. attempt is the 1-based number of the attempt fn is running.

By default each attempt runs in its own child context, named "<name>-attempt-<n>", so the operations of one attempt are recorded under that attempt and their identity does not depend on how far an earlier attempt got before it failed. A failed attempt returns a *ChildContextError, and that error is what strategy receives as RetryAttempt.Err; its ErrorType names the error that escaped fn. The error is rebuilt from the recorded failure on the first invocation and on replay alike, so strategy sees the same input on both. WithAttemptChildContext turns the per-attempt child context off. Then fn runs in ctx, its operations are recorded at the top level, and strategy receives the error fn returned. WithAttemptChildOptions configures the per-attempt child context.

After a failed attempt strategy decides whether to retry and after what delay. A retry waits for the delay with Wait, in an operation named "<name>-backoff-<n>", so no compute is consumed while waiting: the invocation ends and the execution resumes in a new invocation. A zero delay selects DefaultRetryDelay; a negative delay is an error. When strategy stops retrying, Retry returns a *RetryError carrying the number of attempts made and the final attempt's error.

A suspension from inside fn, for example from a Wait or a callback that has not resolved, is propagated unchanged. It is never a failed attempt and never reaches strategy; the attempt continues in a later invocation. On replay a completed attempt returns its checkpointed outcome without running fn, so a group that already succeeded returns its result and a group that already failed returns its error.

strategy must be deterministic, as RetryStrategy requires: replay calls it again for every recorded failed attempt, and a different decision would diverge from the recorded operations. Pass "" as name for unnamed attempt and backoff operations.

func RunInChildContext

func RunInChildContext[O any](ctx Context, name string, fn func(Context) (O, error), opts ...ChildOption) (O, error)

RunInChildContext runs fn in a child context with isolated operation tracking. Use it to group durable operations into a named sub-workflow whose overall result is checkpointed: on replay of a completed child context, the stored result is returned without re-executing fn. If fn fails, RunInChildContext returns a *ChildContextError, or the error a WithChildErrorMapper mapper derives from it.

func Select

func Select[O any](ctx Context, name string, branches []Branch[O], opts ...ChildOption) (winner string, value O, err error)

Select runs the branches concurrently, each in its own child context named by Branch.Name, and returns the name and value of the first branch to settle with a terminal outcome (success or non-suspension error). It is Race for callers who need to know which branch won: the value alone cannot tell a primary quote from a fallback, but winner can.

A terminal outcome observed before any suspension wins immediately, without awaiting the remaining branches. Once a suspension is observed, Select awaits all remaining branches so they reach their blocking points, then propagates the suspension; suspension takes precedence over terminal outcomes observed after it because the suspended branch completes only on a later invocation.

If the winning branch fails, err is that branch's error, a *ChildContextError naming the branch, and winner names it too. The Select operation itself is recorded as SUCCEEDED in that case, with the winner's failure as part of its result, so replay returns the same winner and the same error; the failing branch's own child context is recorded as FAILED.

Select uses RunInChildContext internally, so the winner's name and value are checkpointed together: on replay, the same winner is returned even when a different branch would finish first if the branches ran again. opts configure that child-context operation; WithChildSerdes selects the serializer for the checkpointed record, an object with a "winner" field holding the name and an "outcome" field holding the value or error in the shape AllSettled stores.

Empty branches returns an error immediately, without recording an operation, because no branch could ever win. Duplicate branch names are rejected the same way, because the winner would be ambiguous.

func Start

func Start[I, O any](handler Handler[I, O], opts ...HandlerOption)

Start registers handler as the Lambda function handler and begins processing invocations. It is the durable analogue of lambda.Start and does not return.

func Step

func Step[O any](ctx Context, name string, fn func(StepContext) (O, error), opts ...StepOption) (O, error)

Step executes fn as a durable step: its result is checkpointed, and on replay the stored result is returned without re-executing fn. name identifies the step for tracking and debugging; pass "" for an unnamed step.

A step is a single atomic unit of work and must not create durable operations. To group durable operations, use RunInChildContext or Go.

If fn fails and its retry strategy schedules another attempt, the execution suspends and resumes in a new invocation when the retry delay elapses. If fn fails after exhausting its retry strategy, Step returns a *StepError.

func Wait

func Wait(ctx Context, name string, d time.Duration, opts ...WaitOption) error

Wait suspends the execution for duration d without consuming compute resources: the invocation ends and the execution resumes in a new invocation when the duration elapses. On replay a completed wait returns immediately. name identifies the wait for tracking and debugging; pass "" for an unnamed wait.

The duration is rounded up to a whole number of seconds.

func WaitForCallback

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

WaitForCallback creates a callback, runs submitter to deliver the callback identifier to an external system, and blocks until the system submits a result or the timeout elapses. Failures are returned as a *CallbackExternalError, a *CallbackTimeoutError, or, when the submitter step fails, a *CallbackSubmitterError; all match *CallbackError.

Internally WaitForCallback wraps a child context containing a callback and a submitter step, matching the WaitForCallback wire shape used by all SDK implementations.

WaitForCallback accepts every CallbackOption, which it applies to the callback it creates, plus WaitForCallbackOption values such as WithSubmitterRetry that configure the submitter step.

func WaitForCondition

func WaitForCondition[S any](ctx Context, name string, check func(StepContext, S) (S, error), cfg ConditionConfig[S]) (S, error)

WaitForCondition polls check until the configured wait strategy stops, checkpointing the state between attempts and suspending for the strategy's delay. It returns the final state.

On each invocation cycle the SDK re-executes check with the previously checkpointed state (or InitialState for the first attempt). The wait strategy observes the round-tripped state and the 1-based attempt number and decides whether to continue waiting or stop.

If check returns an error, the operation fails immediately: there is no internal retry. The error is checkpointed and returned as a *WaitForConditionError.

If the wait strategy's Continue field is true, its Delay determines how long the execution suspends before re-invoking. When Continue is false, the final state is checkpointed and returned.

Every state that check returns is serialized and checkpointed, whether the strategy continues or stops. So the result size limit applies to each intermediate state as well as to the final result. When a serialized state exceeds the limit, WaitForCondition returns a *ResultTooLargeError without checkpointing that state.

func WithErrorData

func WithErrorData(err error, data string) error

WithErrorData returns an error that wraps err and carries data as the failure's ErrorData. When the error escapes a step, a child context, a wait-for-condition check, or the handler itself, the SDK records data in the checkpoint's ErrorData field alongside the error's type and message. The data is then readable as the ErrorData field of the StepError, ChildContextError, or other typed error the caller receives, and it is identical on the first invocation and on replay.

The returned error reports err's message and unwraps to err, so errors.Is and errors.As see through it, and it does not change the ErrorType recorded for err. When several wrapped errors appear in one chain, the outermost data wins. Wrapping a nil error returns nil.

Data longer than 256 KiB is truncated on a UTF-8 boundary when it is recorded; the truncated form is what the typed error carries.

func Wrap

func Wrap[I, O any](handler Handler[I, O], opts ...HandlerOption) func(context.Context, []byte) ([]byte, error)

Wrap adapts handler into a raw payload function for callers that compose their own Lambda entry point. The returned function implements no aws-lambda-go interface, but its signature matches the runtime's raw byte handler method, so adapting it to an entry point is a one-method wrapper. Start performs exactly that wrapping. Most programs should use Start.

Do not pass the returned function to lambda.Start directly: the reflective handler path JSON-decodes the payload into the parameter type, and encoding/json expects base64 text for []byte, while the durable invocation payload is a JSON object. Register it through the runtime's raw byte interface instead, as Start does.

Types

type AttemptEndHookInfo

type AttemptEndHookInfo struct {
	OperationHookInfo
	Attempt int
	Outcome PluginAttemptOutcome
	Error   error
}

AttemptEndHookInfo carries context for the OnOperationAttemptEnd hook. A minor release may add fields; construct it with keyed fields.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

type AttemptHookInfo

type AttemptHookInfo struct {
	OperationHookInfo
	Attempt int
}

AttemptHookInfo carries context for attempt-level hooks. A minor release may add fields; construct it with keyed fields.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

type Awaitable

type Awaitable interface {
	// contains filtered or unexported methods
}

Awaitable is a future whose outcome can be awaited without naming its result type. Every *Future satisfies it, so futures of different result types can be passed to Join together.

type BatchError

type BatchError struct {
	// Name is the batch operation's name.
	Name string

	// Reason is the completion reason of the failed batch.
	Reason CompletionReason

	// Errors contains the per-item errors, in input order.
	Errors []error
	// contains filtered or unexported fields
}

BatchError is returned as err by Map and Parallel when the batch's BatchResult.Status is BatchItemFailed: at least one item failed, or the batch-level completion indicates failure. The batch result is returned alongside it, populated, so the caller can inspect and compensate the partial outcome.

Reason is the batch's CompletionReason. It is CompletionFailureToleranceExceeded when the completion policy stopped the batch (the fail-fast default, or an exceeded tolerance); CompletionCustomFailed when a custom completion decision failed the batch, possibly with no failed item; CompletionAllCompleted or CompletionMinSuccessfulReached when the failures were within a configured tolerance and the batch ran on.

Errors holds the per-item errors in input order. errors.Is and errors.As reach them through Unwrap, so a caller can match an item's error type or a sentinel it carries. errors.As against *OperationError also matches the BatchError itself.

A value rebuilt from a checkpoint record (for example the Err of a rejected Settled) recovers Reason from the recorded message, holds one stand-in in Errors carrying that message, and keeps the recorded text as its Error() text.

func (*BatchError) As

func (e *BatchError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*BatchError) Error

func (e *BatchError) Error() string

func (*BatchError) Unwrap

func (e *BatchError) Unwrap() []error

Unwrap returns the per-item errors for use with errors.Is and errors.As.

type BatchItem

type BatchItem[O any] struct {

	// Index is the zero-based position of this item in the original input
	// slice.
	Index int

	// Name identifies the item or branch.
	Name string

	// Status is the item's terminal status.
	Status BatchItemStatus

	// Result is the item's result. It is the zero value unless Status is
	// [BatchItemSucceeded].
	Result O

	// Err is the item's error. It is nil unless Status is
	// [BatchItemFailed].
	Err error
	// contains filtered or unexported fields
}

BatchItem is the outcome of one item or branch in a batch operation.

type BatchItemProgress

type BatchItemProgress struct {

	// Index is the zero-based position of the item in the input slice.
	Index int

	// Name identifies the item or branch. It is empty when the item has
	// no name.
	Name string

	// Status is [BatchItemNotStarted] for an item the batch has not yet
	// admitted, [BatchItemStarted] for an item in flight, and
	// [BatchItemSucceeded] or [BatchItemFailed] once it is terminal.
	Status BatchItemStatus
	// contains filtered or unexported fields
}

BatchItemProgress is the state of one item in a BatchProgress snapshot.

type BatchItemStatus

type BatchItemStatus int

BatchItemStatus is the terminal status of one item or branch in a batch operation.

const (
	// BatchItemNotStarted is the zero value. It never appears in a
	// [BatchResult]: items that never started are omitted from Items. It
	// appears only in a [BatchProgress] snapshot, for the items the batch
	// has not yet admitted when a custom completion callback runs.
	BatchItemNotStarted BatchItemStatus = 0

	// BatchItemSucceeded indicates the item completed and produced a
	// result.
	BatchItemSucceeded BatchItemStatus = 1

	// BatchItemFailed indicates the item failed.
	BatchItemFailed BatchItemStatus = 2

	// BatchItemStarted indicates the item started but the batch completed
	// early before it reached a terminal state, so its work was abandoned:
	// the parent stopped awaiting it and does not count it as a success or
	// a failure. It is still reported (and counted by TotalCount) because
	// the work was begun. Items that never started are omitted entirely.
	//
	// In a [BatchProgress] snapshot it marks an item that is in flight.
	BatchItemStarted BatchItemStatus = 4
)

Batch item statuses. Values are persisted in checkpoints, so they are pinned explicitly rather than derived from iota ordering.

func (BatchItemStatus) String

func (s BatchItemStatus) String() string

String returns the wire representation of the status.

type BatchOption

type BatchOption interface {
	// contains filtered or unexported methods
}

BatchOption configures a Map or Parallel operation.

func WithBatchResultSerdes

func WithBatchResultSerdes(s Serdes) BatchOption

WithBatchResultSerdes overrides the serializer for the entire batch result when checkpointing the parent context's terminal event. This is the operation-level serdes: it serializes and deserializes the whole BatchResult rather than individual items.

func WithBatchSerdes

func WithBatchSerdes(s Serdes) BatchOption

WithBatchSerdes overrides the serializer for each item's result within the batch (the per-item serdes).

func WithBatchSummary

func WithBatchSummary[O any](fn func(result BatchResult[O]) string) BatchOption

WithBatchSummary supplies a summary function for the result of a Map or Parallel operation. O is the operation's item result type; a mismatch is a configuration error the operation returns before it claims an operation ID.

The SDK checkpoints the whole BatchResult when it is at most 256 KiB serialized. A larger result is not stored: the checkpoint records that the batch's child operations are kept, plus a compact record of the completion reason and which items finished, and replay rebuilds the result from the children. Without a summary that record says nothing about what the items produced. With a summary, the SDK calls fn with the result and stores the returned string in the record under the "summary" key:

durable.Map(ctx, "resize", images, resize,
	durable.WithBatchSummary(func(r durable.BatchResult[Image]) string {
		return fmt.Sprintf("%d of %d resized", r.SuccessCount(), r.TotalCount())
	}))

fn runs only when the serialized result exceeds the limit, and only on the invocation that completed the batch. The summary is advisory: the SDK never reads it back, and replay correctness never depends on it. The record with the summary must itself fit the checkpoint limit. The SDK truncates a summary on a UTF-8 boundary until the record fits, and omits it when no prefix fits; an empty summary is omitted. A panic in fn fails the operation with an error naming the batch. fn must be deterministic and free of side effects: it runs at most once per execution, so a summary that varies between runs is a defect a reader of the history cannot detect.

func WithCompletion

func WithCompletion(c CompletionConfig) BatchOption

WithCompletion sets the batch's completion policy: thresholds or a custom CompletionConfig.ShouldComplete callback. Without it the batch is fail-fast; see CompletionConfig.

func WithItemNamer

func WithItemNamer(namer func(index int) string) BatchOption

WithItemNamer sets display names for the items of a Map operation. The namer receives the item's zero-based index, not the item or the items slice; close over the input slice to derive a name from the item value, as Map documents for fn:

durable.Map(ctx, "process", orders, processOrder,
    durable.WithItemNamer(func(i int) string { return orders[i].ID }))

namer must be a deterministic function of its argument.

For Parallel branches, set Branch.Name directly instead.

func WithMaxConcurrency

func WithMaxConcurrency(n int) BatchOption

WithMaxConcurrency bounds how many items or branches run at once. A value of zero or negative is invalid and causes Map/Parallel to return an error.

Changing n between deployments does not move the operation IDs of a batch's items or of the operations inside them, so an execution that is in flight inside the batch keeps replaying correctly. Items are identified by input index whether they run one at a time or concurrently: a NestingFlat item is numbered under the batch, and a NestingNormal item takes the next operation ID after the batch in the enclosing context.

A NestingFlat batch also consumes the same number of enclosing-context IDs for every n, so changing n never affects the operations that follow it. A NestingNormal batch consumes one enclosing-context ID per item that starts: with n == 1 only the items that actually start, with n > 1 every item. Those counts differ only when the completion policy (the fail-fast default or a WithCompletion policy) stops the batch early. For a NestingNormal batch that can stop early, changing n shifts the IDs of the operations after the batch for executions that are in flight; treat that combination as a breaking change for in-flight executions.

func WithNesting

func WithNesting(m NestingMode) BatchOption

WithNesting sets the nesting mode for the batch. NestingFlat causes items to run in virtual contexts without per-item context events. It is the per-batch form of WithChildVirtual, which makes one standalone child context virtual; the two produce the same checkpoint shape for the operations they contain.

type BatchProgress

type BatchProgress struct {

	// TotalCount is the number of items in the batch, started or not.
	TotalCount int

	// CompletedCount is the number of items that have reached a terminal
	// state: SuccessCount + FailureCount.
	CompletedCount int

	// SuccessCount is the number of items that have succeeded so far.
	SuccessCount int

	// FailureCount is the number of items that have failed so far.
	FailureCount int

	// Items holds one entry per item of the batch, in input order, so
	// Items[i] is the item at input index i whatever the order in which
	// items finish. Its length is TotalCount.
	Items []BatchItemProgress
	// contains filtered or unexported fields
}

BatchProgress is the snapshot of a batch's progress passed to CompletionConfig.ShouldComplete. It is taken directly after one item reached a terminal state, before any further item is admitted.

type BatchResult

type BatchResult[O any] struct {

	// Items holds the per-item outcomes in input order. Items that started
	// but were abandoned when the batch completed early are included with
	// status [BatchItemStarted]; items that never started are omitted.
	Items []BatchItem[O]

	// Reason records why the batch completed.
	Reason CompletionReason
	// contains filtered or unexported fields
}

BatchResult is the collected outcome of a Map or Parallel operation.

func Map

func Map[I, O any](ctx Context, name string, items []I, fn func(ctx Context, item I, index int) (O, error), opts ...BatchOption) (BatchResult[O], error)

Map processes items concurrently, applying fn to each in its own child context, and returns the collected results. Concurrency and completion behavior are configured with BatchOption values.

Each item runs in a MapIteration child context. Items are identified by their zero-based index; use WithItemNamer to assign display names from item values. MaxConcurrency bounds in-flight items.

The source collection

fn receives the item and its zero-based index, not the items slice. This is deliberate: the other Durable Execution SDKs pass the collection as a fourth argument, but in Go a function literal at the call site already has the slice in scope, so close over it when an item's processing depends on the rest of the collection:

result, err := durable.Map(ctx, "diffs", readings,
	func(c durable.Context, r Reading, i int) (float64, error) {
		if i == 0 {
			return 0, nil
		}
		return r.Value - readings[i-1].Value, nil
	})

Like every input to a durable operation, the slice must hold the same items in the same order on every invocation of the handler. WithItemNamer uses the same idiom for the same reason.

Completion and failure

The default completion policy is fail-fast: with no WithCompletion option, the first item failure completes the batch with CompletionFailureToleranceExceeded and the items not yet started are omitted from the result. CompletionConfig documents the thresholds that tolerate failures or complete the batch early, and the CompletionReason each one produces.

When at least one item failed, Map returns a BatchError as err and still returns the populated BatchResult, so the partial results remain available for compensation. The error's Reason is the batch's completion reason and its Errors are the per-item errors in input order. This holds whether or not the failures were within a configured tolerance; check BatchError.Reason to distinguish an exceeded tolerance from tolerated failures. Every other non-nil err (invalid options, suspension, replay divergence, a checkpoint failure) is returned with a zero result and must be propagated unchanged:

result, err := durable.Map(ctx, "reserve", items, fn, durable.WithCompletion(cfg))
var berr *durable.BatchError
switch {
case err == nil:
	// use result
case errors.As(err, &berr):
	// items failed; result is populated for compensation
default:
	return err // suspension or SDK failure: propagate unchanged
}

The batch's own checkpoint records the batch operation as SUCCEEDED regardless: the batch operation completed, and the failure of its items is recorded in the result it stores. Only the Go return value reports the failure. Replaying a checkpointed batch returns the same result and the same BatchError, rebuilt from the stored items and reason.

func Parallel

func Parallel[O any](ctx Context, name string, branches []Branch[O], opts ...BatchOption) (BatchResult[O], error)

Parallel executes branches concurrently, each in its own child context, and returns the collected results. All branches must produce the same type; for heterogeneous fan-out, use Go with futures of different types.

Each branch runs in a ParallelBranch child context named by Branch.Name. MaxConcurrency bounds in-flight branches.

Completion and failure follow the same rules as Map: the default policy is fail-fast, CompletionConfig documents the thresholds and the CompletionReason each produces, and when at least one branch failed Parallel returns a BatchError as err together with the populated BatchResult. The batch's checkpoint records the operation as SUCCEEDED regardless, and replay returns the same error.

func (BatchResult[O]) Errors

func (r BatchResult[O]) Errors() []error

Errors returns the errors from failed items, in input order.

func (BatchResult[O]) Failed

func (r BatchResult[O]) Failed() []BatchItem[O]

Failed returns the items that failed, in input order.

func (BatchResult[O]) FailureCount

func (r BatchResult[O]) FailureCount() int

FailureCount returns the number of items that failed.

func (BatchResult[O]) HasFailure

func (r BatchResult[O]) HasFailure() bool

HasFailure reports whether any item failed.

func (BatchResult[O]) Item

func (r BatchResult[O]) Item(name string) *BatchItem[O]

Item returns the item or branch with the given name, or nil if no item has that name. When several items share the name, it returns the first in input order. The pointer refers into Items.

func (BatchResult[O]) Result

func (r BatchResult[O]) Result(name string) (value O, ok bool)

Result returns the successful result of the item or branch with the given name. ok is false if no item has that name or the item did not succeed. When several items share the name, the first in input order is used, whatever its status.

func (BatchResult[O]) Results

func (r BatchResult[O]) Results() []O

Results returns the successful results in input order. Failed or not-started items are omitted.

func (BatchResult[O]) Started

func (r BatchResult[O]) Started() []BatchItem[O]

Started returns the items that were started and then abandoned when the batch completed early, in input order. These are the items with status BatchItemStarted. They are included in BatchResult.TotalCount but are neither successes nor failures. Items that never started are not in Items and so are not returned.

func (BatchResult[O]) StartedCount

func (r BatchResult[O]) StartedCount() int

StartedCount returns the number of items that were started and then abandoned when the batch completed early. BatchResult.TotalCount is SuccessCount + FailureCount + StartedCount.

func (BatchResult[O]) Status

func (r BatchResult[O]) Status() BatchItemStatus

Status returns the overall batch status. A custom completion decision is authoritative: CompletionCustomFailed yields BatchItemFailed and CompletionCustomSucceeded yields BatchItemSucceeded, whatever the item outcomes. Otherwise Status is BatchItemFailed if any item failed or the batch-level completion indicates failure, and BatchItemSucceeded if not.

Status is derived from the exported Items and Reason fields, so it is authoritative for any BatchResult — including one reconstructed by a custom Serdes round trip or constructed directly in tests.

func (BatchResult[O]) Succeeded

func (r BatchResult[O]) Succeeded() []BatchItem[O]

Succeeded returns the items that succeeded, in input order.

func (BatchResult[O]) SuccessCount

func (r BatchResult[O]) SuccessCount() int

SuccessCount returns the number of items that succeeded.

func (BatchResult[O]) TotalCount

func (r BatchResult[O]) TotalCount() int

TotalCount returns the number of items that were started: successes, failures, and started-but-abandoned items. Items that never started (because the batch completed early) are excluded.

type Branch

type Branch[O any] struct {

	// Name identifies the branch. It may be empty in [Parallel]. [Select]
	// returns it as the winner and rejects duplicate names.
	Name string

	// Func is the branch body.
	Func func(Context) (O, error)
	// contains filtered or unexported fields
}

Branch is one branch of a Parallel or Select operation.

type Callback

type Callback[O any] struct {
	// contains filtered or unexported fields
}

Callback is a pending callback operation. It carries the identifier that an external system uses to submit a result, and it settles when the submission arrives or the timeout elapses.

The only way to wait on a Callback is Callback.Result. Like Future, a Callback exposes no channel, because a select over settle signals is not replay-safe.

func CreateCallback

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

CreateCallback creates a callback that an external system completes with the SendDurableExecutionCallbackSuccess or SendDurableExecutionCallbackFailure APIs. The returned callback exposes the identifier to hand off and the settled result.

The submitted payload is deserialized into O with, in order of precedence, the per-operation WithCallbackSerdes, the handler-level WithCallbackDeserializer, or the handler-level Serdes set with WithSerdes, which defaults to encoding/json. So a payload of "42" deserializes into an int and a payload of "\"ok\"" into a string. This differs from the other Durable Execution SDKs, whose callbacks default to returning the raw payload string; in Go the result is typed, so it goes through the same JSON decoding as every other operation result.

CreateCallback accepts only CallbackOption values. Options that configure the submitter step of WaitForCallback, such as WithSubmitterRetry, have no effect here and do not compile.

func (*Callback[O]) ID

func (c *Callback[O]) ID() string

ID returns the callback identifier to hand to the external system.

func (*Callback[O]) Result

func (c *Callback[O]) Result() (O, error)

Result blocks until the external system submits a result or the timeout elapses, then returns the outcome. Failures are returned as a *CallbackExternalError or a *CallbackTimeoutError; both match *CallbackError.

type CallbackDetails

type CallbackDetails struct {
	// CallbackId is the identifier external systems use to resolve the
	// callback. It is assigned by the backend when the callback starts.
	CallbackId *string

	// Result is the payload the external system submitted, set on success.
	Result *string

	// Error carries failure details, set on failure.
	Error *ErrorObject
}

CallbackDetails carries CALLBACK operation state.

type CallbackError

type CallbackError struct {
	// Name is the callback operation's name.
	Name string

	// CallbackID is the identifier that was issued to the external system.
	CallbackID string

	// ErrorType is the wire ErrorType recorded for the failure: the type
	// the external system reported, or the SDK's own name for the failure
	// mode when the record carries none.
	ErrorType string

	// Message is the recorded failure message.
	Message string

	// ErrorData is the recorded ErrorData, if any.
	ErrorData string

	// StackTrace holds recorded stack trace lines, when captured.
	StackTrace []string

	// Err is the stand-in for the recorded failure.
	Err error
}

CallbackError indicates that a callback failed. It is the base error for all callback-related failures; errors.As against *CallbackError matches every subtype:

Err is a stand-in rebuilt from ErrorType and Message, the same on the first invocation and on replay. For a timeout it unwraps to ErrCallbackTimedOut. Match on the subtype or on ErrorType. See OperationError.

func (*CallbackError) As

func (e *CallbackError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*CallbackError) Error

func (e *CallbackError) Error() string

func (*CallbackError) Unwrap

func (e *CallbackError) Unwrap() error

type CallbackExternalError

type CallbackExternalError struct {
	CallbackError
}

CallbackExternalError indicates that the external system completed the callback with a failure (SendDurableExecutionCallbackFailure). ErrorType and Message are the values the external system supplied. The wrapped cause is a reconstructed stand-in; match on ErrorType. See CallbackError.

func (*CallbackExternalError) As

func (e *CallbackExternalError) As(target any) bool

As supports errors.As matching against *CallbackError and *OperationError.

func (*CallbackExternalError) Error

func (e *CallbackExternalError) Error() string

type CallbackOption

type CallbackOption interface {
	WaitForCallbackOption
	// contains filtered or unexported methods
}

CallbackOption configures the callback created by CreateCallback or WaitForCallback. Every CallbackOption is also a WaitForCallbackOption, so the same value can be passed to either function.

Options that configure only the submitter step of WaitForCallback, such as WithSubmitterRetry, are not CallbackOptions. Passing one to CreateCallback is a compile error rather than a silently ignored option.

func WithCallbackHeartbeatTimeout

func WithCallbackHeartbeatTimeout(d time.Duration) CallbackOption

WithCallbackHeartbeatTimeout bounds the interval between heartbeats from the external system. If no heartbeat arrives within the interval, the callback fails with a *CallbackTimeoutError whose Heartbeat field is true, matching ErrCallbackTimedOut.

func WithCallbackSerdes

func WithCallbackSerdes(s Serdes) CallbackOption

WithCallbackSerdes overrides the serializer for the callback result. The deserialize path is used when replaying a SUCCEEDED callback to unmarshal the stored payload into the typed result. In WaitForCallback it applies to the callback the operation creates.

func WithCallbackTimeout

func WithCallbackTimeout(d time.Duration) CallbackOption

WithCallbackTimeout bounds how long the callback waits for an external submission. On expiry the callback fails with a *CallbackTimeoutError matching ErrCallbackTimedOut.

type CallbackOptions

type CallbackOptions struct {
	// TimeoutSeconds bounds how long the callback waits for an external
	// submission. Zero disables the timeout.
	TimeoutSeconds int32

	// HeartbeatTimeoutSeconds bounds the interval between heartbeats.
	// Zero disables the heartbeat timeout.
	HeartbeatTimeoutSeconds int32
}

CallbackOptions configures a CALLBACK operation update.

type CallbackSubmitterError

type CallbackSubmitterError struct {
	CallbackError
}

CallbackSubmitterError indicates that the submitter step of a WaitForCallback failed after exhausting its retry strategy. ErrorType and Message describe the submitter's final error. The wrapped cause is a reconstructed stand-in; match on ErrorType. See CallbackError.

func (*CallbackSubmitterError) As

func (e *CallbackSubmitterError) As(target any) bool

As supports errors.As matching against *CallbackError and *OperationError.

func (*CallbackSubmitterError) Error

func (e *CallbackSubmitterError) Error() string

type CallbackTimeoutError

type CallbackTimeoutError struct {
	CallbackError

	// Heartbeat is true when the heartbeat timeout elapsed, and false when
	// the overall callback timeout elapsed. It is derived from the timeout
	// message the service recorded.
	Heartbeat bool
}

CallbackTimeoutError indicates that the callback's timeout, or its heartbeat timeout, elapsed before the external system submitted a result. Err unwraps to ErrCallbackTimedOut on both the first invocation and on replay. The wrapped cause is a reconstructed stand-in; match on the type, on Heartbeat, or on ErrorType. See CallbackError.

func (*CallbackTimeoutError) As

func (e *CallbackTimeoutError) As(target any) bool

As supports errors.As matching against *CallbackError and *OperationError.

func (*CallbackTimeoutError) Error

func (e *CallbackTimeoutError) Error() string

type ChainedInvokeDetails

type ChainedInvokeDetails struct {
	// Result is the invoked function's serialized result, set on success.
	Result *string

	// Error carries failure details, set on failure.
	Error *ErrorObject
}

ChainedInvokeDetails carries CHAINED_INVOKE operation state.

type ChainedInvokeOptions

type ChainedInvokeOptions struct {
	// FunctionName is the name or ARN of the function to invoke.
	FunctionName *string

	// TenantId is the tenant identifier for the chained invocation.
	TenantId *string
}

ChainedInvokeOptions configures a CHAINED_INVOKE operation update.

type CheckpointError

type CheckpointError struct {
	// Err is the original error from the checkpoint API call.
	Err error
	// contains filtered or unexported fields
}

CheckpointError wraps a checkpoint API failure with its ErrorScope. Use errors.As to extract it from wrapped errors, CheckpointError.Scope to see how far the failure reaches, and CheckpointError.Retryable to branch on retry decisions.

Retryability and scope

Retryable is derived from the scope: it is true when Scope is ErrorScopeInvocation, with one exception. An invocation-scoped failure is transient, so the SDK retries the checkpoint call before giving up on the invocation. An execution-scoped failure is permanent, so the SDK does not retry it.

The exception is a stale checkpoint token. The service rejects a checkpoint whose token a newer invocation has superseded. The failure is invocation-scoped: the execution continues in the newer invocation, so the current one ends with an error and nothing is lost. But the token never becomes valid again, so Retryable is false and the SDK does not retry the call.

How the SDK acts on the scope

When a CheckpointError escapes the handler, the SDK reads its scope. An invocation-scoped error ends the invocation with an error, so the execution resumes in a later invocation from its last checkpoint. An execution-scoped error ends the execution with a FAILED response. Handler code that wants an ordinary failure should return its own error rather than pass a CheckpointError through.

A stale-token rejection ends the invocation with an error even when the handler does not pass it through. The SDK stops checkpointing the moment the rejection arrives, and the invocation's outcome cannot be reported with a token the service no longer accepts.

The same rule applies to the checkpoint the SDK makes on the handler's behalf when a result is too large to return inline: an invocation-scoped failure ends the invocation with an error, and an execution-scoped failure ends the execution with a FAILED response.

A CheckpointError rebuilt from a checkpoint record by ErrorFromObject carries no scope: Scope is the zero value and Retryable is false. The record does not store the classification.

func (*CheckpointError) Error

func (e *CheckpointError) Error() string

func (*CheckpointError) Retryable

func (e *CheckpointError) Retryable() bool

Retryable reports whether the checkpoint failure is transient and the caller should retry the request. It is true when [Scope] is ErrorScopeInvocation, except for a stale checkpoint token, which no retry can make valid again.

func (*CheckpointError) Scope

func (e *CheckpointError) Scope() ErrorScope

Scope reports how far the checkpoint failure reaches. See ErrorScope.

func (*CheckpointError) Unwrap

func (e *CheckpointError) Unwrap() error

Unwrap exposes the original cause to errors.Is and errors.As.

type CheckpointInput

type CheckpointInput struct {
	// ExecutionArn identifies the durable execution.
	ExecutionArn string

	// CheckpointToken is the caller's current checkpoint token. The
	// backend rejects stale tokens, which serializes checkpoint writers.
	CheckpointToken string

	// Updates are the operation updates to apply atomically.
	Updates []OperationUpdate
}

CheckpointInput applies a batch of operation updates to an execution.

type CheckpointOutput

type CheckpointOutput struct {
	// CheckpointToken is the rotated token to use for subsequent calls.
	CheckpointToken string

	// NewExecutionState carries the updated operations from the backend,
	// including backend-assigned fields (such as callback IDs).
	NewExecutionState []Operation
}

CheckpointOutput is the result of a successful checkpoint call.

type ChildContextError

type ChildContextError struct {
	// Name is the child context's name.
	Name string

	// ErrorType is the wire ErrorType of the error that escaped the child.
	ErrorType string

	// Message is the escaping error's recorded message.
	Message string

	// ErrorData is the payload attached with [WithErrorData], if any.
	ErrorData string

	// StackTrace holds recorded stack trace lines, when captured.
	StackTrace []string

	// Err is the stand-in for the error that escaped the child.
	Err error
}

ChildContextError indicates that a child-context function failed.

ErrorType names the error that escaped the child body: "StepError" when a step inside the child failed, "ChildContextError" for a nested child, or the body's own error type. Err is a stand-in rebuilt from the record, the same on the first invocation and on replay. When ErrorType names an SDK error type, Err is that type rebuilt with its OperationError fields, so errors.As against the inner type (for example *StepError) succeeds; the inner type's own detail fields, such as StepError.Attempts, are zero. When ErrorType names a caller's type, Err is a leaf stand-in that errors.As against that type does not match; match on ErrorType. ErrorData attached anywhere in the child's failure chain propagates through every nesting level. See OperationError.

func (*ChildContextError) As

func (e *ChildContextError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*ChildContextError) Error

func (e *ChildContextError) Error() string

func (*ChildContextError) Unwrap

func (e *ChildContextError) Unwrap() error

type ChildOption

type ChildOption interface {
	// contains filtered or unexported methods
}

ChildOption configures a single child-context operation.

func WithChildErrorMapper

func WithChildErrorMapper(mapper func(err *ChildContextError) error) ChildOption

WithChildErrorMapper supplies a function that maps a child context's failure before RunInChildContext, RunInChildContextAsync, or Go returns it. Without a mapper a failed child returns a *ChildContextError. With a mapper, that same *ChildContextError is passed to mapper and mapper's result is returned instead. A nil result is ignored and the *ChildContextError is returned unchanged, so a failure cannot become a success by mistake.

Mapper runs on every invocation that reaches the failed child: on the first invocation after the body fails, and on each replay, where the body does not run. Its input is the same each time. The *ChildContextError is built from the recorded failure, never from the live error value, so the ErrorType, Message, ErrorData, and StackTrace mapper sees on the first invocation are the ones it sees on replay. Mapper must therefore be deterministic: the same input yields the same output, with no dependence on time, randomness, or state outside the error. A deterministic mapper reproduces the mapped error on replay:

durable.WithChildErrorMapper(func(err *durable.ChildContextError) error {
	if err.ErrorType == "StepError" {
		return &PaymentError{Reason: err.Message}
	}
	return err
})

The checkpoint records the failure that escaped the child body, which is mapper's input, not mapper's result. A result of the handler's own type cannot be rebuilt from a record, so recording the input and mapping it again is what reproduces the mapped error on replay. A mapper that alters the fields of the *ChildContextError it receives, or returns a new one, changes what the handler sees but not what is recorded. The execution history therefore shows the original failure.

func WithChildSerdes

func WithChildSerdes(s Serdes) ChildOption

WithChildSerdes overrides the serializer for the child context's result.

func WithChildSubType

func WithChildSubType(subType string) ChildOption

WithChildSubType sets the operation subtype recorded for a RunInChildContext, RunInChildContextAsync, or Go operation. Without it the operation records OperationSubTypeRunInChildContext. The subtype is written to the checkpoint and reported in OperationHookInfo.SubType, so a plugin or a reader of the execution history can tell one kind of caller-defined grouping from another:

durable.RunInChildContext(ctx, "order-42", processOrder,
	durable.WithChildSubType("OrderSaga"))

subType must be 1 to 32 characters from the set A-Z, a-z, 0-9, hyphen, and underscore; an empty subType selects the default. The subtypes the SDK records for its own operations, the OperationSubType constants other than OperationSubTypeRunInChildContext, are reserved: a child context cannot be labelled as a Step, a Map iteration, or any other SDK operation, because plugins and tooling identify those operations by subtype alone. A value outside these rules is a configuration error the operation returns before it claims an operation ID.

The subtype is part of the operation's identity on replay. Every invocation of the execution must supply the same subtype for the same operation; an invocation that finds a different subtype in the checkpoint returns a *NonDeterministicReplayError. A subtype must therefore not depend on the input, on time, or on any other value that can differ between invocations, and changing it in a deployment breaks the executions that are in flight.

func WithChildSummary

func WithChildSummary[O any](fn func(result O) string) ChildOption

WithChildSummary supplies a summary function for the result of a RunInChildContext, RunInChildContextAsync, or Go operation. O is the operation's result type; a mismatch is a configuration error the operation returns before it claims an operation ID.

The SDK checkpoints the child's result when it is at most 256 KiB serialized. A larger result is not stored: the checkpoint records that the child's operations are kept, and replay re-executes the child body to rebuild the value. Without a summary that checkpoint carries no payload, so inspecting the execution history shows nothing about what the child produced. With a summary, the SDK calls fn with the result and stores the returned string as the checkpoint payload instead:

durable.RunInChildContext(ctx, "import", importRows,
	durable.WithChildSummary(func(rows []Row) string {
		return fmt.Sprintf("%d rows imported", len(rows))
	}))

fn runs only when the serialized result exceeds the limit, and only on the invocation that produced the result. The summary is advisory: the SDK never reads it back, and replay correctness never depends on it. The summary must itself fit the checkpoint limit. A summary longer than 256 KiB is truncated on a UTF-8 boundary to that size; an empty summary leaves the payload absent. A panic in fn fails the operation with an error naming the child. fn must be deterministic and free of side effects: it runs at most once per execution, so a summary that varies between runs is a defect a reader of the history cannot detect.

func WithChildVirtual

func WithChildVirtual() ChildOption

WithChildVirtual makes a RunInChildContext, RunInChildContextAsync, or Go child context virtual. A virtual child context groups durable operations and scopes their names and log records like a checkpointed child context, but it is not an operation itself: nothing is checkpointed for the wrapper, so the execution history holds no ContextStarted, ContextSucceeded, or ContextFailed event for it. The operations inside it are checkpointed where the enclosing context's own operations are: they record the nearest checkpointed ancestor as their parent, and their IDs are numbered under the virtual child's position, so adding or removing the option around existing operations changes their identity on replay.

durable.RunInChildContext(ctx, "enrich", enrichOrder, durable.WithChildVirtual())

This is the same mechanism NestingFlat applies to the items of a Map or Parallel: a flat item is a virtual child context that the batch creates for each item, and WithChildVirtual creates one standalone. Where WithNesting chooses per batch, WithChildVirtual chooses per child context.

Because the wrapper leaves no record, replay re-runs the child body on every invocation that reaches it; the operations inside replay from their own checkpoints as usual, and a suspending operation inside it resumes in a later invocation exactly as it would in a checkpointed child. The body must therefore be deterministic in the same way a handler is. The child starts in the replay state of its parent, and like its parent it switches to live execution at its first operation that has no checkpoint. The result is round-tripped through the child's Serdes on every run, so the caller sees the same value live and on replay, and it has no size limit because it is never stored. A failure of the body is returned as a *ChildContextError, or the error a WithChildErrorMapper mapper derives from it, rebuilt from the same failure on every run.

Plugins observe a virtual child context as they observe a checkpointed one: an operation start is dispatched before Plugin.WrapChildContextFn wraps the body, and an operation end once the body has an outcome, with the WithChildSubType subtype and the enclosing context's ParentID. A plugin that counts runs sees one start and one end per invocation that reaches the child. Because nothing is recorded, no event of the child records a checkpoint: the start and the end report IsReplay true on every invocation. WrapChildContextFn receives the start's info with IsReplay false when the child executes live and true when it replays the operations inside it. The operations inside it report the enclosing context's ParentID, not the child's, so the child adds no level to the depth WithPluginChildOperationsDepth counts. A WithChildSummary function is never called.

A virtual child context cannot hold another virtual child context: the inner one returns a configuration error before it claims an operation ID. Make one of the two a checkpointed child context instead. A virtual child context inside a NestingFlat item, and a flat batch inside a virtual child context, are both supported.

type ClientError

type ClientError struct {
	// Scope states how far the failure reaches.
	Scope ErrorScope

	// Err is the underlying failure, kept for diagnostics.
	Err error
}

ClientError is the error an ExecutionClient returns to state the scope of a failure directly. The default client infers the scope from the AWS SDK's error shape. A custom client that does not produce AWS-shaped errors has no other way to express the distinction: without it, every failure is treated as transient, so a permanent failure is retried until the execution times out.

The SDK honors the scope wherever the error surfaces. A ClientError returned from ExecutionClient.Checkpoint is wrapped in a CheckpointError carrying the same scope. A ClientError returned from ExecutionClient.GetExecutionState with ErrorScopeExecution fails the execution instead of the invocation. A ClientError that handler or plugin code returns is acted on like a CheckpointError of the same scope: ErrorScopeInvocation ends the invocation with an error so the execution resumes later, and ErrorScopeExecution fails the execution.

A Scope that is neither ErrorScopeInvocation nor ErrorScopeExecution, including the zero value, is treated as ErrorScopeInvocation. Assuming a failure is transient is the safe default: the execution gets another attempt rather than being failed on the strength of an error the SDK does not understand.

func (c *httpClient) Checkpoint(ctx context.Context, in durable.CheckpointInput) (durable.CheckpointOutput, error) {
	resp, err := c.post(ctx, in)
	if err != nil {
		return durable.CheckpointOutput{}, &durable.ClientError{Scope: durable.ErrorScopeInvocation, Err: err}
	}
	if resp.StatusCode >= 400 && resp.StatusCode < 500 {
		return durable.CheckpointOutput{}, &durable.ClientError{Scope: durable.ErrorScopeExecution, Err: errors.New(resp.Status)}
	}
	...
}

func (*ClientError) Error

func (e *ClientError) Error() string

func (*ClientError) Unwrap

func (e *ClientError) Unwrap() error

Unwrap exposes the underlying failure to errors.Is and errors.As.

type CombinatorError

type CombinatorError struct {
	// Name is the combinator operation's name.
	Name string

	// Errors contains the individual future errors.
	Errors []error
	// contains filtered or unexported fields
}

CombinatorError indicates that a future combinator failed. For Any, this wraps all individual future errors when no future succeeded.

The combinator runs in a child context, so the error a caller receives is a ChildContextError whose cause is a CombinatorError rebuilt from the recorded failure: Errors then holds one stand-in carrying the recorded message, on the first invocation and on replay alike. Match on the type and on ChildContextError.ErrorType. See OperationError.

A value rebuilt from a checkpoint record keeps the recorded Error() text, so the count it reports is the count at the time of failure.

func (*CombinatorError) As

func (e *CombinatorError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*CombinatorError) Error

func (e *CombinatorError) Error() string

func (*CombinatorError) Unwrap

func (e *CombinatorError) Unwrap() []error

Unwrap returns the individual errors for use with errors.Is and errors.As.

type CompletionConfig

type CompletionConfig struct {

	// MinSuccessful completes the batch early once this many items
	// succeed. Zero leaves the threshold unset.
	MinSuccessful int

	// ToleratedFailureCount fails the batch once more than this many
	// items fail. Nil leaves the threshold unset; an explicit zero fails
	// the batch on the first failure.
	ToleratedFailureCount *int

	// ToleratedFailurePercentage fails the batch once the failure
	// percentage, computed against the total item count, strictly
	// exceeds this value. Nil leaves the threshold unset; an explicit
	// zero fails the batch on the first failure. The comparison is
	// exact: with three items and a threshold of 33, one failure
	// (33.3%) exceeds the threshold.
	ToleratedFailurePercentage *int

	// ShouldComplete decides completion programmatically. It is called
	// after each item reaches a terminal state, with a [BatchProgress]
	// snapshot taken at that moment, and returns [ContinueBatch] to keep
	// going or [CompleteBatch] to complete the batch now. Completing early
	// leaves the items still in flight with status [BatchItemStarted] and
	// omits the items that never started, exactly as a threshold does.
	// The batch's [CompletionReason] is then [CompletionCustomSucceeded]
	// or [CompletionCustomFailed], by the decision's [CompletionOutcome];
	// that outcome alone decides [BatchResult.Status] and whether Map or
	// Parallel returns a [BatchError].
	//
	// The callback must be deterministic: it must depend only on its
	// argument, and it must return the same decision for the same
	// progress every time it is called. The decision it makes is
	// checkpointed with the batch, so replaying a completed batch reads
	// the recorded reason and does not call the callback again. A
	// callback that reads state outside its argument can decide
	// differently on one invocation than the checkpoint of an earlier
	// invocation recorded, and the execution then diverges from its
	// checkpoint log.
	//
	// Items can finish between two evaluations, so the batch may complete
	// with more terminal items than the snapshot that decided it showed.
	// A callback that panics, or that completes the batch with an outcome
	// that is not a defined [CompletionOutcome], fails the batch operation
	// with an error that is not a BatchError.
	ShouldComplete func(BatchProgress) CompletionDecision
	// contains filtered or unexported fields
}

CompletionConfig is a batch completion policy for Map and Parallel.

The default is fail-fast. When no threshold is set (no WithCompletion option, or a zero CompletionConfig), the first item failure completes the batch with CompletionFailureToleranceExceeded; items not yet started are omitted from the result. Tolerating failures requires setting ToleratedFailureCount or ToleratedFailurePercentage explicitly. Setting only MinSuccessful also disables fail-fast: every item runs unless the MinSuccessful threshold completes the batch first.

Thresholds may be combined. The first threshold to fire decides the CompletionReason:

The completion reason describes why scheduling stopped. Whether the batch is returned as an error is decided separately: Map and Parallel return a BatchError whenever at least one item failed, whatever the reason. See Map.

Custom completion

ShouldComplete replaces the thresholds with a callback that decides completion from the batch's progress. It is mutually exclusive with the threshold fields: a CompletionConfig that sets ShouldComplete together with MinSuccessful, ToleratedFailureCount, or ToleratedFailurePercentage is rejected, and Map or Parallel returns an error before any item runs. With ShouldComplete set there is no fail-fast: an item failure by itself never stops the batch. The callback decides, and the batch completes with CompletionAllCompleted once every item has finished if the callback never completed it.

type CompletionDecision

type CompletionDecision struct {
	// contains filtered or unexported fields
}

CompletionDecision is the value a CompletionConfig.ShouldComplete callback returns. Build it with ContinueBatch or CompleteBatch. The zero value is the decision ContinueBatch returns: it keeps the batch running.

func CompleteBatch

func CompleteBatch(outcome CompletionOutcome) CompletionDecision

CompleteBatch returns the decision to complete the batch now with the given outcome. The batch stops admitting items and stops awaiting the items in flight. outcome must be CompletionOutcomeSucceeded or CompletionOutcomeFailed; any other value fails the batch operation when the decision is applied.

func ContinueBatch

func ContinueBatch() CompletionDecision

ContinueBatch returns the decision to keep the batch running. It is the zero CompletionDecision.

func (CompletionDecision) Complete

func (d CompletionDecision) Complete() bool

Complete reports whether the decision completes the batch.

func (CompletionDecision) Outcome

Outcome returns the outcome of a completing decision. It is zero for a decision built by ContinueBatch.

type CompletionOutcome

type CompletionOutcome int

CompletionOutcome is the outcome a custom completion decision assigns to the batch it completes.

const (
	// CompletionOutcomeSucceeded completes the batch as succeeded, with
	// [CompletionCustomSucceeded], even if items failed.
	CompletionOutcomeSucceeded CompletionOutcome = 1

	// CompletionOutcomeFailed completes the batch as failed, with
	// [CompletionCustomFailed], even if no item failed. Map and Parallel
	// then return a [BatchError] whose Errors holds the failed items'
	// errors, which may be empty.
	CompletionOutcomeFailed CompletionOutcome = 2
)

Completion outcomes.

func (CompletionOutcome) String

func (o CompletionOutcome) String() string

String returns the name of the outcome.

type CompletionReason

type CompletionReason int

CompletionReason records why a batch operation completed.

const (
	// CompletionAllCompleted indicates every item ran to completion.
	CompletionAllCompleted CompletionReason = 1

	// CompletionMinSuccessfulReached indicates the batch completed early
	// because the MinSuccessful threshold was met.
	CompletionMinSuccessfulReached CompletionReason = 2

	// CompletionFailureToleranceExceeded indicates the batch failed early
	// because more items failed than the tolerance allows.
	CompletionFailureToleranceExceeded CompletionReason = 3

	// CompletionCustomSucceeded indicates a custom completion decision
	// completed the batch early as succeeded.
	CompletionCustomSucceeded CompletionReason = 4

	// CompletionCustomFailed indicates a custom completion decision
	// completed the batch early as failed.
	CompletionCustomFailed CompletionReason = 5
)

Batch completion reasons. Values are persisted in checkpoints, so they are pinned explicitly rather than derived from iota ordering.

func (CompletionReason) String

func (r CompletionReason) String() string

String returns the wire representation of the completion reason. A zero or unrecognized value returns "UNKNOWN".

type ConditionConfig

type ConditionConfig[S any] struct {

	// InitialState is the state passed to the first check.
	InitialState S

	// WaitStrategy decides, after each check, whether to keep waiting and
	// for how long. attempt is the 1-based number of completed checks.
	// The strategy must be a deterministic function of its arguments,
	// except for randomized jitter in the returned delay.
	//
	// Build one from declarative configuration with [NewWaitStrategy] or
	// [MustNewWaitStrategy], or write a function literal. The field keeps
	// its unnamed function type so that a [WaitStrategy] value, a function
	// literal, and a caller-defined function type all assign to it.
	//
	// If nil, the strategy that WaitConfig[S]{} builds is used: keep
	// polling with exponential backoff — a 5 second initial delay
	// multiplied by 1.5 after each attempt, capped at 300 seconds, with
	// full jitter — and fail the operation once 60 attempts have been
	// made. That default has no condition predicate, so it never reports
	// the condition met; set WaitStrategy to make the wait succeed.
	WaitStrategy func(state S, attempt int) WaitDecision

	// Serdes overrides the serializer for the condition state.
	Serdes Serdes
	// contains filtered or unexported fields
}

ConditionConfig configures a WaitForCondition operation.

Unlike other operations, which take variadic Option arguments, WaitForCondition takes this struct positionally: two of its fields (InitialState and WaitStrategy) depend on the state type S, and Go's non-generic option interfaces cannot carry a type parameter.

type Context

type Context interface {
	context.Context

	// ExecutionArn returns the ARN of the current durable execution.
	ExecutionArn() string

	// RequestID returns the AWS request ID of the current Lambda
	// invocation. It is empty outside a Lambda invocation (such as under
	// the [durabletest] local runner).
	//
	// The request ID varies across invocations of one execution, so using
	// it in handler logic outside a [Step] introduces non-determinism on
	// replay. If the raw aws-lambda-go invocation context is genuinely
	// needed, it remains reachable from the embedded [context.Context] via
	// lambdacontext.FromContext(ctx).
	RequestID() string

	// InvokedFunctionARN returns the ARN used to invoke the current
	// Lambda function. It is empty outside a Lambda invocation (such as
	// under the [durabletest] local runner).
	InvokedFunctionARN() string

	// Logger returns the logger for this context. Its records carry the
	// request ID and execution ARN as structured attributes, and the
	// tenant ID when the invocation has one, whichever [slog.Handler] is
	// installed with [WithLogHandler] or [ConfigureLogging]. Inside a child
	// context (from [RunInChildContext], [Go], [Map], [Parallel], or
	// [WaitForCallback]) the records also carry the child operation's ID
	// as operationId and its name as operationName. While this context is
	// replaying, log output is suppressed so that replayed code does not
	// duplicate log lines, unless [ReplayLogModeEmit] is in effect, in
	// which case replayed records are emitted with the attribute
	// replay=true. Suppression is decided per branch: each context (root,
	// child context, and each concurrent branch from [Go], [Map], or
	// [Parallel]) consults its own replay state, so a branch that is still
	// replaying stays suppressed even after a sibling branch has reached
	// live execution.
	Logger() *slog.Logger

	// IsReplaying reports whether the execution is currently replaying
	// previously checkpointed operations.
	IsReplaying() bool
	// contains filtered or unexported methods
}

Context is the durable execution context passed to handler functions and child-context functions. It carries the execution's identity and replay state, and it is the required first argument of every durable operation.

Context implements context.Context, so it can be passed directly to AWS SDK calls and other context-aware APIs.

A Context is owned by the goroutine it was created on. Durable operations invoked on a Context from any other goroutine fail with ErrWrongGoroutine before they claim an operation ID. Use Go to run durable work concurrently; it gives the new goroutine a Context of its own. See the package documentation section "Goroutine Ownership".

Context is sealed: only the SDK can implement it. External types that embed or imitate this interface will fail to compile because of the unexported method.

type ContextDetails

type ContextDetails struct {
	// Result is the child context's serialized result, set on success.
	Result *string

	// ReplayChildren reports whether the completed context's child
	// operations are included in replay state.
	ReplayChildren *bool

	// Error carries failure details, set on failure.
	Error *ErrorObject
}

ContextDetails carries CONTEXT operation state.

type ContextOptions

type ContextOptions struct {
	// ReplayChildren requests that the completed context's child
	// operations be included in replay state.
	ReplayChildren *bool
}

ContextOptions configures a CONTEXT operation update.

type Deserializer

type Deserializer interface {
	Unmarshal(data []byte, v any) error
}

Deserializer deserializes callback payloads submitted by external systems. It is set for the whole handler with WithCallbackDeserializer. Without one, callbacks decode payloads with the handler-level Serdes, which defaults to encoding/json; see CreateCallback for the full precedence and for how this differs from the other Durable Execution SDKs.

type ErrorMatcher

type ErrorMatcher func(err error) bool

ErrorMatcher reports whether a failed attempt's error is retryable. It is used in RetryConfig.RetryableErrors and LinearRetryConfig.RetryableErrors. ErrorIs, ErrorAs, ErrorContains, and ErrorMatches build matchers for the common cases; any func(error) bool is a matcher.

A matcher must be a deterministic function of the error it receives, for the same reason a RetryStrategy must be.

func ErrorAs

func ErrorAs[T error]() ErrorMatcher

ErrorAs returns a matcher that reports whether an error matches type T under errors.As, so wrapped errors match. T is the type a caller would pass a pointer to when calling errors.As directly: a pointer type for errors with pointer receivers, or an interface type.

durable.ErrorAs[*TransientError]()
durable.ErrorAs[net.Error]()

func ErrorContains

func ErrorContains(substr string) ErrorMatcher

ErrorContains returns a matcher that reports whether an error's message, the value of its Error method, contains substr. The empty string matches every error.

func ErrorIs

func ErrorIs(target error) ErrorMatcher

ErrorIs returns a matcher that reports whether an error matches target under errors.Is, so wrapped errors match. It is intended for sentinel errors such as io.EOF.

ErrorIs(nil) returns a nil matcher, which NewRetryStrategy and LinearBackoff reject.

func ErrorMatches

func ErrorMatches(re *regexp.Regexp) ErrorMatcher

ErrorMatches returns a matcher that reports whether an error's message, the value of its Error method, contains a match of re.

ErrorMatches(nil) returns a nil matcher, which NewRetryStrategy and LinearBackoff reject.

type ErrorObject

type ErrorObject struct {
	// ErrorType is the error's type name.
	ErrorType *string

	// ErrorMessage is the human-readable error message.
	ErrorMessage *string

	// ErrorData is machine-readable error data.
	ErrorData *string

	// StackTrace is optional stack trace information.
	StackTrace []string
}

ErrorObject carries structured error information for a failed operation.

type ErrorScope

type ErrorScope string

ErrorScope states how far an ExecutionClient failure reaches: whether it ends only the current invocation or the whole execution.

The SDK decides, for every failed client call, between two outcomes. An invocation-scoped failure ends the current invocation with an error, and the execution resumes in a later invocation from its last checkpoint. An execution-scoped failure ends the execution: the invocation responds FAILED and no later invocation follows.

const (
	// ErrorScopeInvocation means the current invocation cannot continue but
	// the execution can resume in a new one. It fits transient conditions:
	// timeouts, throttling, connection failures, and server-side errors.
	// The SDK retries a checkpoint call that fails with this scope before
	// giving up on the invocation, except for a stale checkpoint token,
	// which a newer invocation has superseded and no retry can revive.
	ErrorScopeInvocation ErrorScope = "INVOCATION"

	// ErrorScopeExecution means the execution cannot proceed and must fail.
	// It fits conditions a retry cannot resolve: a rejected request,
	// missing permissions, an unknown or finished execution, or
	// misconfiguration. The SDK does not retry a call that fails with this
	// scope.
	ErrorScopeExecution ErrorScope = "EXECUTION"
)

type ExecutionClient

type ExecutionClient interface {
	// GetExecutionState returns one page of the execution's checkpointed
	// operation log. Set [GetExecutionStateInput.Marker] from a previous
	// page's NextMarker to continue pagination.
	GetExecutionState(ctx context.Context, in GetExecutionStateInput) (GetExecutionStateOutput, error)

	// Checkpoint atomically applies operation updates and rotates the
	// checkpoint token. The returned state carries the updated operations,
	// including any backend-assigned fields (such as callback IDs).
	Checkpoint(ctx context.Context, in CheckpointInput) (CheckpointOutput, error)
}

ExecutionClient persists and retrieves durable execution state. The SDK calls it to load the checkpointed operation log at the start of an invocation and to record operation updates as the handler runs.

The default implementation calls the AWS Lambda service. The [durabletest] package provides an in-memory implementation for local testing without AWS infrastructure. Custom implementations are injected with WithExecutionClient.

All types in the interface are owned by this SDK, so implementing it requires no dependency on any AWS SDK module.

type ExecutionDetails

type ExecutionDetails struct {
	// InputPayload is the original input to the durable execution.
	InputPayload *string
}

ExecutionDetails carries EXECUTION operation state.

type FieldMatchMode

type FieldMatchMode int

FieldMatchMode controls how a PreviewField is compared with the dot-separated path of a field in the value being previewed.

const (
	// FieldMatchAnywhere matches a field whose name equals
	// [PreviewField.Name] at any depth. The selector "email" matches the
	// paths "email", "customer.email", and "orders.customer.email". A
	// dotted selector never matches in this mode, because it is compared
	// with single path segments. This is the default.
	FieldMatchAnywhere FieldMatchMode = iota

	// FieldMatchPath matches only the field whose full dot-separated path
	// from the root equals [PreviewField.Name]. The selector "email"
	// matches only a top-level field named email; "customer.email" matches
	// only the email field directly inside the top-level customer field.
	FieldMatchPath
)

type FileSystemPathEncoding

type FileSystemPathEncoding int

FileSystemPathEncoding controls how the durable execution ARN and the operation ID are turned into the directory and file names of an offloaded value under the base path.

const (
	// FileSystemPathEncodingURI is the readable layout and the default. The
	// per-execution directory is
	// <functionName>/<executionName>/<invocationId>, taken from the
	// execution ARN, and the file name is the operation ID followed by
	// ".json". Every segment is percent-encoded: bytes outside the
	// unreserved set (letters, digits, "-", "_", ".", "~") become %XX, and
	// a segment that would be "." or ".." has its dots encoded. So no
	// identifier can name a path outside its directory or produce a
	// filename with a separator in it. An ARN that does not have the
	// durable-execution shape is percent-encoded whole into a single
	// directory segment. A very long operation ID can exceed the
	// filesystem's per-name limit (commonly 255 bytes); use
	// FileSystemPathEncodingHash when that is a risk.
	FileSystemPathEncodingURI FileSystemPathEncoding = iota

	// FileSystemPathEncodingHash is the hashed layout. The directory is the
	// hex encoding of the first 16 bytes of the SHA-256 digest of the
	// execution ARN, and the file name is the operation ID followed by
	// ".json". The directory name has a fixed length and is filesystem-safe
	// whatever the ARN contains, but it cannot be read back to an execution
	// by browsing the mount. This is the layout earlier releases always
	// used, unchanged, so files written by them sit where this layout puts
	// them.
	FileSystemPathEncodingHash
)

type FileSystemSerdesConfig

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.
	//
	// The checkpoint envelope stores the full path of each file, so a
	// value is read back correctly whatever layout was in effect when it
	// was written. Changing this setting affects only where new files are
	// written.
	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. The
	// operation log then shows the preview without reading the file. It is
	// not called for a value stored inline in
	// [FileSystemSerdesModeOverflow], since that value is already visible.
	//
	// Use [BuildPreview] with a [PreviewConfig] to select and mask fields:
	//
	//	GeneratePreview: func(v any) map[string]any {
	//		return durable.BuildPreview(v, durable.PreviewConfig{
	//			Mode:    durable.PreviewExcludeAll,
	//			Include: []durable.PreviewField{{Name: "id"}},
	//			Mask:    []durable.PreviewField{{Name: "email"}},
	//		})
	//	}
	//
	// The preview is advisory metadata. Masking a field in the preview does
	// not redact it from the file, which holds the value in full. An
	// envelope written without a preview reads back unchanged, so this
	// setting can be added to or removed from a deployed function without
	// affecting executions that are already running.
	GeneratePreview func(value any) map[string]any
	// contains filtered or unexported fields
}

FileSystemSerdesConfig configures a [FileSystemSerdes].

type FileSystemSerdesMode

type FileSystemSerdesMode int

FileSystemSerdesMode controls when data is written to the filesystem.

const (
	// FileSystemSerdesModeAlways writes every value to a file; the
	// checkpoint stores only a reference envelope.
	FileSystemSerdesModeAlways FileSystemSerdesMode = iota

	// FileSystemSerdesModeOverflow writes data inline (as JSON) unless it
	// exceeds the overflow threshold, in which case it overflows to a file.
	FileSystemSerdesModeOverflow
)

type Future

type Future[O any] struct {
	// contains filtered or unexported fields
}

Future is the result of an asynchronous durable operation. It settles exactly once, with either a value or an error, and can be read any number of times from any goroutine.

The only ways to wait on a Future are Future.Result and the combinators All, AllSettled, Any, Race, and Join. Each of these checkpoints its outcome, so the value observed on first execution is the value observed on replay. A Future deliberately exposes no channel: a select over several futures picks whichever settles first in the current invocation, and that choice is not checkpointed. On replay every future may already be settled, so the select could pick a different case, and any code that branches on the winner would diverge from the first execution.

func Go

func Go[O any](ctx Context, name string, fn func(Context) (O, error), opts ...ChildOption) *Future[O]

Go runs fn concurrently in its own child context and returns a future for its result. It is the replay-safe substitute for the go statement inside durable functions, and shorthand for RunInChildContextAsync: opts are forwarded unchanged, so WithChildSerdes applies to the child result, WithChildSummary to its checkpoint when the result is oversized, WithChildSubType to its recorded subtype, and WithChildErrorMapper to its failure.

The child's operation identity is claimed before Go returns, so consecutive Go calls from one goroutine are replay-deterministic. Inside fn, the provided Context is owned by fn's goroutine, and all durable operations on it are safe, including nested Go calls.

func InvokeAsync

func InvokeAsync[O, I any](ctx Context, name, functionID string, input I, opts ...InvokeOption) *Future[O]

InvokeAsync is Invoke, except that the result is delivered through the returned future.

The operation's identity is claimed before InvokeAsync returns, so consecutive InvokeAsync calls from one goroutine are replay-deterministic. On invocation suspension, the returned future is settled with errSuspendExecution so goroutines blocked on Future.Result unwind.

func RunInChildContextAsync

func RunInChildContextAsync[O any](ctx Context, name string, fn func(Context) (O, error), opts ...ChildOption) *Future[O]

RunInChildContextAsync is RunInChildContext, except that fn runs concurrently on its own goroutine and the result is delivered through the returned future.

The child's operation identity is claimed synchronously before RunInChildContextAsync returns, preserving deterministic program order. Inside fn, the provided Context is owned by fn's goroutine, and all durable operations on it are safe, including nested Go calls. If fn fails, the future settles with a *ChildContextError, or the error a WithChildErrorMapper mapper derives from it.

On invocation suspension, the returned future is settled with errSuspendExecution so goroutines blocked on Future.Result unwind.

func StepAsync

func StepAsync[O any](ctx Context, name string, fn func(StepContext) (O, error), opts ...StepOption) *Future[O]

StepAsync is Step, except that fn runs concurrently and the result is delivered through the returned future. The operation's identity is claimed before StepAsync returns, so consecutive StepAsync calls from one goroutine are replay-deterministic.

On invocation suspension, the returned future is settled with errSuspendExecution so goroutines blocked on Future.Result unwind.

func WaitAsync

func WaitAsync(ctx Context, name string, d time.Duration, opts ...WaitOption) *Future[Void]

WaitAsync is Wait, except that the wait completes through the returned future, allowing other durable operations to proceed concurrently.

On invocation suspension, the returned future is settled with errSuspendExecution so goroutines blocked on Future.Result unwind.

func (*Future[O]) Result

func (f *Future[O]) Result() (O, error)

Result blocks until the operation settles, then returns its outcome. Once the future has settled, Result returns immediately.

type GetExecutionStateInput

type GetExecutionStateInput struct {
	// ExecutionArn identifies the durable execution.
	ExecutionArn string

	// CheckpointToken is the caller's current checkpoint token.
	CheckpointToken string

	// Marker continues pagination from a previous page's NextMarker.
	// Empty requests the first page.
	Marker string
}

GetExecutionStateInput requests one page of an execution's checkpointed operation log.

type GetExecutionStateOutput

type GetExecutionStateOutput struct {
	// Operations are the checkpointed operations on this page.
	Operations []Operation

	// NextMarker is non-empty when more pages remain. Pass it as the next
	// request's Marker.
	NextMarker string
}

GetExecutionStateOutput is one page of an execution's checkpointed operation log.

type Handler

type Handler[I, O any] func(ctx Context, event I) (O, error)

Handler is a durable function handler. It receives the deserialized invocation event and a Context in place of the standard Lambda context.

type HandlerOption

type HandlerOption interface {
	// contains filtered or unexported methods
}

HandlerOption configures the durable execution handler at construction time.

func WithCallbackDeserializer

func WithCallbackDeserializer(d Deserializer) HandlerOption

WithCallbackDeserializer sets the default deserializer for callback payloads submitted by external systems. This is used when deserializing the result of a SUCCEEDED callback during replay. Per-operation WithCallbackSerdes takes precedence. Without it, callbacks use the handler-level Serdes set with WithSerdes (default: JSONSerdes), not a raw-string passthrough; see CreateCallback. To replace the default from inside the handler, see ConfigureSerdes.

func WithExecutionClient

func WithExecutionClient(client ExecutionClient) HandlerOption

WithExecutionClient sets the execution client for the durable handler. The default client calls the AWS Lambda service and is built lazily from the default AWS config.

Use this option to inject a custom ExecutionClient implementation, such as the in-memory client provided by the [durabletest] package for local testing.

func WithLogHandler

func WithLogHandler(h slog.Handler) HandlerOption

WithLogHandler sets the slog.Handler behind Context.Logger and StepContext.Logger. A nil handler selects the default.

The default handler writes one JSON object per record to stderr, Lambda's log channel, with these fields: timestamp (ISO 8601 UTC with millisecond precision and a Z suffix), level (DEBUG, INFO, WARN, or ERROR), message, requestId, executionArn, tenantId (when the invocation has one), operationId, operationName, and attempt (inside a step body, condition check, or callback submitter), plus any attributes the call site adds. An attribute whose value is an error is expanded into errorType and errorMessage, and stackTrace when the error carries recorded frames. The default handler's minimum level is read from the AWS_LAMBDA_LOG_LEVEL environment variable (TRACE, DEBUG, INFO, WARN, ERROR, or FATAL, case insensitive); unset or unrecognised selects INFO. A supplied handler applies its own level.

The SDK adds the execution and operation attributes through the handler's WithAttrs method, so a supplied handler receives them as structured attributes, and it wraps the handler with per-branch replay suppression: while a context replays checkpointed operations, its records are dropped before they reach the handler, unless WithReplayLogMode selects ReplayLogModeEmit. Fields a plugin returns from Plugin.EnrichLogContext arrive as attributes of the record; that field documents their precedence. To replace the handler from inside the handler body, see ConfigureLogging.

func WithPluginChildOperationsDepth

func WithPluginChildOperationsDepth(depth int) HandlerOption

WithPluginChildOperationsDepth bounds the depth in the operation tree of the operations reported to plugins. Operations deeper than depth are omitted from every operation-level notification: OnOperationStart, OnOperationEnd, OnOperationAttemptStart, OnOperationAttemptEnd, the WrapOperationAttemptFn and WrapChildContextFn wrap hooks, which then run the wrapped work directly, and the Operations and UpdatedOperations maps of the invocation hooks. Omission affects notifications only: an omitted operation runs, retries, and checkpoints exactly as a reported one.

Depth counts the operations between an operation and the root of the tree. An operation claimed on the handler's Context has depth 0. An operation claimed inside a child context has the depth of that context's operation plus one. A Map or Parallel item has the depth of its batch plus one, and the operations inside the item one more. An item under NestingFlat has no operation of its own, so the operations inside it have the depth of the batch plus one; likewise a child context under WithChildVirtual adds no level, so the operations inside it have the depth of the operations claimed on its parent. depth is the deepest depth reported: 0 reports only the operations claimed on the handler's Context, 1 also reports their direct children, and so on.

An operation at depth is reported with ChildrenOmitted set on its OperationHookInfo, so a consumer can tell that the operations inside it were withheld rather than absent. Every reported operation's parent is also reported, so ParentID chains stay complete; see OperationHookInfo.ParentID.

The default reports every depth. depth must not be negative; Wrap and Start panic on a negative value.

EXPERIMENTAL: this function is experimental and may be changed or removed in future releases.

func WithPlugins

func WithPlugins(plugins ...Plugin) HandlerOption

WithPlugins registers instrumentation plugins with the handler. Plugins are appended in call order, and the order decides how the wrap hooks compose: the first registered plugin is outermost. See Plugin for the dispatch and concurrency contract.

EXPERIMENTAL: this function is experimental and may be changed or removed in future releases.

func WithReplayLogMode

func WithReplayLogMode(mode ReplayLogMode) HandlerOption

WithReplayLogMode sets what happens to log records emitted while a context replays checkpointed operations. The default, ReplayLogModeSuppress, drops them so replayed code does not duplicate the lines it wrote when it first ran. ReplayLogModeEmit emits them with the attribute replay=true, for diagnosing a replay problem; expect every line written before a suspension to appear again on each later invocation. ReplayLogModeUnchanged selects the default. To change the mode from inside the handler body, see ConfigureLogging.

func WithSerdes

func WithSerdes(s Serdes) HandlerOption

WithSerdes sets the default serializer for operation results. It applies to steps, child contexts, invokes, and condition state. Per-operation serdes options take precedence. The default is JSONSerdes. To replace the default from inside the handler, see ConfigureSerdes.

func WithStackTraces

func WithStackTraces(enabled bool) HandlerOption

WithStackTraces controls whether the SDK records a stack trace when user code fails. Capture is enabled by default.

User code is the handler and every callback a durable operation runs: a step body, a child context body, a Map or Parallel item function, and a WaitForCondition check or wait strategy. The trace is taken when that code hands the failure to the SDK, so the first frame names the user function that produced it:

  • For a returned error, the first frame is the failing function itself and the frames after it run outward from the SDK's call into that function through the code that invoked the operation.
  • For a panic, the trace is taken inside the SDK's recovery while the panicking frames are still on the stack, so it starts at the panicking function.
  • An error that supplies its own trace through a "StackTrace() []string" method anywhere in its chain is recorded with that trace instead.

Each frame is one string of the form "function file:line", innermost frame first, and at most MaxStackTraceFrames frames are kept. The frames are recorded in the operation's checkpoint and in the FAILED invocation response, and the typed operation errors expose them as StackTrace. An operation error that already carries a trace keeps it when a later operation or the handler returns it, so the recorded trace always points at the failure that happened first.

Pass false to record no stack traces. Failures then carry an empty StackTrace on every path. Disable capture when checkpoints must stay as small as possible or when file paths from the build must not leave the function.

type InvocationEndHookInfo

type InvocationEndHookInfo struct {
	ExecutionArn string
	Status       PluginInvocationStatus

	// ExecutionResult is the handler's return value when the invocation
	// succeeded. Nil on failure or suspension.
	ExecutionResult any

	// ExecutionError is the error that caused invocation failure. Nil on
	// success or suspension.
	ExecutionError error
}

InvocationEndHookInfo carries context for the OnInvocationEnd hook. A minor release may add fields; construct it with keyed fields.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

type InvocationHookInfo

type InvocationHookInfo struct {
	ExecutionArn      string
	IsFirstInvocation bool

	// ExecutionInput is the deserialized customer event for the execution.
	// It is the raw unmarshaled value (typically a map or struct).
	ExecutionInput any

	// ExecutionStartTimestamp is the time the execution was first created,
	// sourced from the execution operation's StartTimestamp in the wire
	// payload. Zero when unavailable (e.g. payload lacks timestamp data).
	ExecutionStartTimestamp time.Time

	// UpdatedOperations contains operations whose status changed
	// externally between invocations, keyed by operation ID. This
	// embeds the same data as OnOperationChange to allow plugins that
	// need both to avoid state ordering dependencies. It is a subset of
	// Operations.
	UpdatedOperations map[string]OperationHookInfo

	// Operations contains every operation known at the start of the
	// invocation, keyed by operation ID, including the root execution
	// operation and operations that did not change since the previous
	// invocation. UpdatedOperations is a subset of it. On the first
	// invocation it holds the execution operation alone. Each entry
	// carries the checkpointed status, timestamps, result, and error, with
	// IsReplay set, as an OnOperationEnd hook would report the operation.
	//
	// The map is built before the invocation hooks run and is not
	// modified afterwards, so a plugin may read it at any time. It is nil
	// when no registered plugin implements OnInvocationStart or
	// WrapInvocation, the hooks that receive it.
	Operations map[string]OperationHookInfo
}

InvocationHookInfo carries context for invocation-level hooks. A minor release may add fields; construct it with keyed fields.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

type InvokeError

type InvokeError struct {
	// Name is the invoke operation's name.
	Name string

	// FunctionID is the function name or ARN that was invoked.
	FunctionID string

	// Status is the terminal operation status that caused the failure
	// (Failed, TimedOut, Stopped, or Cancelled). It enables callers to
	// distinguish the reason for failure without sentinel error matching.
	Status OperationStatus

	// ErrorType is the wire ErrorType the invoked function recorded.
	ErrorType string

	// Message is the invoked function's recorded error message.
	Message string

	// ErrorData is the invoked function's recorded ErrorData, if any.
	ErrorData string

	// StackTrace holds recorded stack trace lines, when captured.
	StackTrace []string

	// Err is the stand-in for the invoked function's failure.
	Err error
}

InvokeError indicates that an invoked function failed or its execution timed out.

Err is a stand-in rebuilt from ErrorType and Message, the same on the first invocation and on replay. It unwraps to ErrInvokeTimedOut, ErrExecutionStopped, or ErrExecutionCancelled when Status is the matching terminal status. Match on ErrorType or Status. See OperationError.

func (*InvokeError) As

func (e *InvokeError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*InvokeError) Error

func (e *InvokeError) Error() string

func (*InvokeError) Unwrap

func (e *InvokeError) Unwrap() error

type InvokeOption

type InvokeOption interface {
	// contains filtered or unexported methods
}

InvokeOption configures a single invoke operation.

func WithInvokePayloadSerdes

func WithInvokePayloadSerdes(s Serdes) InvokeOption

WithInvokePayloadSerdes overrides the serializer for the invoke's input payload.

func WithInvokeResultSerdes

func WithInvokeResultSerdes(s Serdes) InvokeOption

WithInvokeResultSerdes overrides the serializer for the invoke's result.

func WithTenantID

func WithTenantID(tenantID string) InvokeOption

WithTenantID sets the tenant identifier for a tenant-isolated invocation.

type JitterStrategy

type JitterStrategy string

JitterStrategy randomizes retry delays to avoid thundering herds.

const (
	// JitterFull randomizes the delay between zero and the computed
	// delay. This is the default.
	JitterFull JitterStrategy = "FULL"

	// JitterHalf randomizes the delay between half the computed delay
	// and the computed delay.
	JitterHalf JitterStrategy = "HALF"

	// JitterNone applies the computed delay unchanged.
	JitterNone JitterStrategy = "NONE"
)

Jitter strategies for retry delays.

type LinearRetryConfig

type LinearRetryConfig struct {

	// MaxAttempts is the maximum number of total attempts, including the
	// first. The default is 6. It must not be negative.
	MaxAttempts int

	// InitialDelay is the delay before the first retry. The default is
	// 1 second. When set, it must be at least 1 second.
	InitialDelay time.Duration

	// Increment is added to the delay before each retry after the first.
	// The default is 1 second. It must not be negative. For a fixed
	// interval between attempts, use [NewRetryStrategy] with a
	// BackoffRate of 1 instead.
	Increment time.Duration

	// MaxDelay caps the delay between retries. The default is 5 minutes.
	// When set, it must be at least 1 second.
	MaxDelay time.Duration

	// Jitter is the jitter strategy applied to computed delays. The
	// default is [JitterNone], so the default sequence is exact. When
	// set, it must be one of the defined [JitterStrategy] constants.
	Jitter JitterStrategy

	// RetryableErrors restricts retries to errors that at least one
	// matcher reports as retryable. When empty, every error is retryable.
	// Entries must not be nil. See [RetryConfig.RetryableErrors].
	RetryableErrors []ErrorMatcher
	// contains filtered or unexported fields
}

LinearRetryConfig configures a linear backoff retry strategy created with LinearBackoff or MustLinearBackoff. The zero value of each field selects its documented default. The zero value of LinearRetryConfig produces the default linear strategy: 6 total attempts with delays of 1 s, 2 s, 3 s, 4 s, and 5 s, without jitter.

type LogConfig

type LogConfig struct {

	// Handler replaces the [slog.Handler] behind [Context.Logger] and
	// [StepContext.Logger]. It has the same role as [WithLogHandler]. The
	// SDK attaches the execution attributes (requestId, executionArn, and
	// tenantId when present) and each scope's operation attributes to the
	// new handler through its WithAttrs method, wraps it with replay
	// suppression, and adds the fields plugins return from
	// [Plugin.EnrichLogContext], exactly as it does for the handler given
	// at construction. nil keeps the current handler.
	Handler slog.Handler

	// ReplayLogMode replaces the treatment of records emitted during
	// replay. It has the same role as [WithReplayLogMode].
	// [ReplayLogModeUnchanged], the zero value, keeps the current mode.
	ReplayLogMode ReplayLogMode
	// contains filtered or unexported fields
}

LogConfig names the logging settings that ConfigureLogging replaces. A zero field keeps the value currently in effect, so a config that sets only one field leaves the other unchanged.

type NestingMode

type NestingMode int

NestingMode controls whether batch items run in real or virtual child contexts.

const (
	// NestingNormal (default) runs each item in its own child context,
	// producing per-item ContextStarted/ContextSucceeded events. Each item
	// is a checkpointed operation whose parent is the batch, and each
	// dispatches its own operation lifecycle hooks to plugins with the
	// batch as ParentID; see [Plugin].
	NestingNormal NestingMode = iota

	// NestingFlat runs each item in a virtual context: operations inside
	// the item are checkpointed directly under the parent batch context,
	// with no per-item context events. A flat item is not an operation,
	// so it dispatches no operation lifecycle hooks: plugins observe the
	// batch's own start and end and the operations inside each item,
	// which name the batch as ParentID. A flat item is the virtual child
	// context [WithChildVirtual] creates standalone, made by the batch
	// for each item, with one difference: a standalone virtual child
	// context dispatches lifecycle hooks of its own and a flat item does
	// not. See that option for how a virtual context replays.
	NestingFlat
)

Nesting modes.

type NonDeterministicReplayError

type NonDeterministicReplayError struct {
	// Name is the operation name the current code passed.
	Name string

	// StepID is the positional ID where the mismatch was detected.
	StepID string

	// ExpectedType is the operation type the current code expects.
	ExpectedType string

	// ExpectedSubType is the sub-type the current code expects (may be
	// empty for operations that don't distinguish by sub-type).
	ExpectedSubType string

	// ExpectedName is the operation name the current code expects (may be
	// empty for unnamed operations).
	ExpectedName string

	// ActualType is the checkpointed operation's type.
	ActualType string

	// ActualSubType is the checkpointed operation's sub-type.
	ActualSubType string

	// ActualName is the checkpointed operation's name.
	ActualName string
	// contains filtered or unexported fields
}

NonDeterministicReplayError indicates that replay diverged from the recorded execution. Two cases produce it:

  • A checkpointed operation's type, sub-type, or name does not match what the current code expects at the same position. The handler code changed between deployments in a way that breaks replay determinism.
  • The current code awaits an operation inside a child context whose result is already recorded, but the operation had not completed when that result was recorded. Such an operation never runs again during replay, so the await could never settle; the SDK reports it after a bounded wait instead of stalling until the invocation times out.

A value rebuilt from a checkpoint record (for example the Err of a rejected Settled) carries Name and the recorded Error() text; the detail fields are zero.

func (*NonDeterministicReplayError) As

func (e *NonDeterministicReplayError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*NonDeterministicReplayError) Error

type Operation

type Operation struct {
	// Id is the operation's unique identifier.
	Id *string

	// Status is the operation's current status.
	Status OperationStatus

	// Type is the operation's kind.
	Type OperationType

	// SubType further qualifies the operation's kind.
	SubType *string

	// Name is the caller-supplied operation name.
	Name *string

	// ParentId identifies the parent operation for operations inside a
	// child context.
	ParentId *string

	// StartTimestamp is when the operation started.
	StartTimestamp *time.Time

	// EndTimestamp is when the operation reached a terminal status.
	EndTimestamp *time.Time

	// ExecutionDetails is set for EXECUTION operations.
	ExecutionDetails *ExecutionDetails

	// StepDetails is set for STEP operations.
	StepDetails *StepDetails

	// WaitDetails is set for WAIT operations.
	WaitDetails *WaitDetails

	// CallbackDetails is set for CALLBACK operations.
	CallbackDetails *CallbackDetails

	// ChainedInvokeDetails is set for CHAINED_INVOKE operations.
	ChainedInvokeDetails *ChainedInvokeDetails

	// ContextDetails is set for CONTEXT operations.
	ContextDetails *ContextDetails
}

Operation is one checkpointed durable operation record. Exactly one of the details fields is set, matching Type.

Optional string fields are pointers so that an absent value is distinguishable from an empty one, mirroring the checkpoint wire contract.

type OperationAction

type OperationAction string

OperationAction is the state transition a checkpoint update applies to an operation.

const (
	OperationActionStart   OperationAction = "START"
	OperationActionSucceed OperationAction = "SUCCEED"
	OperationActionFail    OperationAction = "FAIL"
	OperationActionRetry   OperationAction = "RETRY"
	OperationActionCancel  OperationAction = "CANCEL"
)

Operation actions.

type OperationChangeHookInfo

type OperationChangeHookInfo struct {
	ExecutionArn      string
	UpdatedOperations map[string]OperationHookInfo
}

OperationChangeHookInfo carries context for OnOperationChange. A minor release may add fields; construct it with keyed fields.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

type OperationError

type OperationError struct {
	// Name is the operation's name as passed by the caller.
	Name string

	// ErrorType is the wire ErrorType of the escaping error, for example
	// "PaymentDeclinedError". It is empty for errors that record no cause.
	ErrorType string

	// Message is the escaping error's recorded message.
	Message string

	// ErrorData is the optional structured payload attached with
	// [WithErrorData]. It round-trips through checkpoints unchanged.
	ErrorData string

	// StackTrace holds the stack trace lines recorded for the failure, when
	// the recorder captured one. It is nil otherwise.
	StackTrace []string

	// Err is the stand-in for the recorded failure, built from ErrorType
	// and Message. It is the same value on the first invocation and on
	// replay. It is nil for errors that record no cause.
	Err error
}

OperationError is the common shape of every failure a durable operation reports. Callers match it with a single errors.As call to determine that any durable operation produced the error, without checking each concrete type:

var opErr *durable.OperationError
if errors.As(err, &opErr) {
    log.Printf("operation %q failed with %s: %s", opErr.Name, opErr.ErrorType, opErr.Message)
}

Every typed operation error (StepError, InvokeError, CallbackError and its subtypes, ChildContextError, WaitForConditionError, RetryError, CombinatorError, StepInterruptedError, BatchError, NonDeterministicReplayError, ResultTooLargeError) is matchable this way. The typed errors that wrap a recorded failure expose the same fields directly.

The wrapped cause is a stand-in

A failure is recorded in the checkpoint as four fields: the escaping error's type name (ErrorType), its message, optional ErrorData, and an optional stack trace. Err is rebuilt from those fields on both the first invocation and on replay; it never holds the original Go error value. So errors.As against a caller's own error type is false on every invocation, not just on replay. Match on ErrorType instead:

var stepErr *durable.StepError
if errors.As(err, &stepErr) && stepErr.ErrorType == "PaymentDeclinedError" {
    // handle the decline
}

ErrorType is the escaping error's Go type name, as derived when the failure was recorded: the concrete type's name, or "Error" for unnamed types such as those from errors.New and fmt.Errorf. The SDK's own error types use fixed names shared with the other Durable Execution SDKs, so a StepError escaping a child context is recorded as "StepError".

Where the SDK defines a sentinel (ErrCallbackTimedOut, ErrInvokeTimedOut, ErrExecutionStopped, ErrExecutionCancelled), the stand-in unwraps to it, so errors.Is against the sentinel is true on both the first invocation and on replay.

func (*OperationError) Error

func (e *OperationError) Error() string

func (*OperationError) Unwrap

func (e *OperationError) Unwrap() error

type OperationHookInfo

type OperationHookInfo struct {
	ExecutionArn string
	ID           string
	Name         string
	Type         string
	SubType      string
	Status       PluginOperationStatus
	Attempt      int
	IsReplay     bool

	// ParentID is the ID of the parent context operation, if any. Empty
	// for top-level (root context) operations. A consumer that builds the
	// operation tree follows it.
	//
	// Every reported operation's parent is itself reported, so a consumer
	// can follow ParentID from any reported operation up to the root. Under
	// [WithPluginChildOperationsDepth] the operations nested inside an
	// operation at the configured depth are not reported; that operation
	// carries ChildrenOmitted so the consumer can tell the subtree was
	// truncated rather than absent.
	ParentID string

	// ChildrenOmitted reports that the operations nested inside this one
	// are omitted from plugin notifications: the operation lies at the
	// depth set by [WithPluginChildOperationsDepth], so no hook fires for
	// anything it contains, and no reported operation names it as
	// ParentID. False when no depth is set or the operation lies above it.
	// A consumer that finds no children for an operation with this field
	// set must treat the subtree as unreported, not as empty.
	ChildrenOmitted bool

	// StartTimestamp is when this operation began. Set on OnOperationStart
	// and OnOperationEnd; zero on hooks where not yet known.
	StartTimestamp time.Time

	// EndTimestamp is when this operation reached a terminal state. Set on
	// OnOperationEnd; zero on OnOperationStart.
	EndTimestamp time.Time

	// Result is the operation's serialized result (raw wire form), if any.
	// Set on OnOperationEnd for succeeded operations; empty otherwise.
	Result string

	// Error is the error the operation failed with, if any. Set on
	// OnOperationEnd for failed operations; nil otherwise.
	Error error
}

OperationHookInfo carries context for operation-level hooks. A minor release may add fields; construct it with keyed fields.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

type OperationStatus

type OperationStatus string

OperationStatus is the status of a durable operation. It covers the in-progress statuses (STARTED, PENDING, READY), the successful terminal status (SUCCEEDED), and the failure terminal statuses (FAILED, TIMED_OUT, STOPPED, CANCELLED).

It appears in two places. Operation.Status carries it in checkpoint records exchanged with the service. InvokeError.Status carries a failure terminal status to indicate why an invoked function failed.

const (
	// OperationStatusStarted indicates the operation is in progress.
	OperationStatusStarted OperationStatus = "STARTED"

	// OperationStatusPending indicates the operation is awaiting a timer
	// (such as a step retry delay).
	OperationStatusPending OperationStatus = "PENDING"

	// OperationStatusReady indicates the operation's timer elapsed and it
	// is ready to run again.
	OperationStatusReady OperationStatus = "READY"

	// OperationStatusSucceeded indicates the operation completed with a
	// result.
	OperationStatusSucceeded OperationStatus = "SUCCEEDED"
)

In-progress operation statuses, plus the successful terminal status. The failure terminal statuses are declared in errors.go alongside InvokeError, which reports them.

const (
	// OperationStatusFailed indicates the operation's function returned an error.
	OperationStatusFailed OperationStatus = "FAILED"

	// OperationStatusTimedOut indicates the operation exceeded its timeout.
	OperationStatusTimedOut OperationStatus = "TIMED_OUT"

	// OperationStatusStopped indicates the execution was explicitly stopped.
	OperationStatusStopped OperationStatus = "STOPPED"

	// OperationStatusCancelled indicates the execution was cancelled.
	OperationStatusCancelled OperationStatus = "CANCELLED"
)

Failure terminal operation statuses, reported by InvokeError. The in-progress statuses and SUCCEEDED are declared in client.go alongside Operation.

type OperationType

type OperationType string

OperationType identifies the kind of a durable operation.

const (
	OperationTypeExecution     OperationType = "EXECUTION"
	OperationTypeContext       OperationType = "CONTEXT"
	OperationTypeStep          OperationType = "STEP"
	OperationTypeWait          OperationType = "WAIT"
	OperationTypeCallback      OperationType = "CALLBACK"
	OperationTypeChainedInvoke OperationType = "CHAINED_INVOKE"
)

Operation types.

type OperationUpdate

type OperationUpdate struct {
	// Id is the operation's unique identifier.
	Id *string

	// Type is the operation's kind.
	Type OperationType

	// Action is the state transition to apply.
	Action OperationAction

	// SubType further qualifies the operation's kind.
	SubType *string

	// Name is the caller-supplied operation name.
	Name *string

	// ParentId identifies the parent operation for operations inside a
	// child context.
	ParentId *string

	// Payload is the operation result for SUCCEED actions.
	Payload *string

	// Error carries failure details for FAIL and RETRY actions.
	Error *ErrorObject

	// StepOptions configures STEP operations.
	StepOptions *StepOptions

	// WaitOptions configures WAIT operations.
	WaitOptions *WaitOptions

	// CallbackOptions configures CALLBACK operations.
	CallbackOptions *CallbackOptions

	// ChainedInvokeOptions configures CHAINED_INVOKE operations.
	ChainedInvokeOptions *ChainedInvokeOptions

	// ContextOptions configures CONTEXT operations.
	ContextOptions *ContextOptions
}

OperationUpdate is one state transition to apply to an operation in a checkpoint call.

type Plugin

type Plugin struct {
	// OnInvocationStart is called once at the start of each Lambda
	// invocation of the execution, before the user handler runs. It is
	// the first hook of the invocation: it fires before OnOperationChange
	// and before WrapInvocation. It fires on the first invocation and on
	// every later one; IsFirstInvocation on the info tells them apart.
	// The info's Operations map is a snapshot of every checkpointed
	// operation as the invocation begins. It is dispatched from the
	// goroutine that received the invocation, in the sense the Dispatch
	// section of [Plugin] defines: on that goroutine with one plugin
	// registered, on a goroutine the dispatch joins with several. It
	// never overlaps another invocation-level hook of the same plugin.
	OnInvocationStart func(ctx context.Context, info InvocationHookInfo)

	// OnInvocationEnd is called once when the invocation ends, with the
	// invocation's status: Succeeded or Failed when the execution has
	// finished, Pending when the invocation suspended on an operation that
	// completes later, and Retrying when the invocation returned an error
	// to Lambda and the service will invoke the execution again. See the
	// [PluginInvocationStatus] constants. It is the last hook of the
	// invocation: WrapInvocation and the hooks of every operation the
	// handler awaited have returned before it fires. On Succeeded it
	// fires only after the result has been recorded, so a plugin never
	// reports a success the execution has not committed. It fires on
	// every invocation, whether the handler completed, suspended, or
	// failed. It is dispatched from the goroutine that received the
	// invocation: on that goroutine with one plugin registered, on a
	// goroutine the dispatch joins with several.
	OnInvocationEnd func(ctx context.Context, info InvocationEndHookInfo)

	// OnOperationStart is called when a durable operation begins in this
	// invocation, before the operation's body, attempt hooks, and wrap
	// hooks, and before any hook of an operation nested inside it. Each
	// operation dispatches at most one start per invocation. An
	// operation that runs live reports [PluginOperationStarted] with
	// IsReplay false. An operation that is re-entered before it settled
	// reports its checkpointed status with IsReplay true. A [Step]
	// replayed from a terminal checkpoint reports that status with
	// IsReplay true and then dispatches OnOperationEnd with no hook in
	// between. For a succeeded Step the SDK decodes the checkpointed
	// result between the two; if decoding fails, the Step returns the
	// decoding error to its caller and dispatches no end. Every other
	// operation replayed from a terminal checkpoint dispatches only the
	// end, because its start was dispatched by the invocation that
	// recorded it. A Step whose retry is scheduled and not yet due
	// dispatches no hook until the attempt runs. Each operation's own
	// documentation states any further detail of its replayed events. It
	// is dispatched from the goroutine that runs the operation: on that
	// goroutine with one plugin registered, on a goroutine the dispatch
	// joins with several. Starts of concurrent operations may run in
	// parallel.
	OnOperationStart func(ctx context.Context, info OperationHookInfo)

	// OnOperationEnd is called when a durable operation reaches a
	// terminal status: Succeeded, Failed, TimedOut, Stopped, or
	// Cancelled. Each operation dispatches at most one end per
	// invocation, after OnOperationAttemptEnd of its final attempt and,
	// for a context operation, after its body has returned. A live operation
	// dispatches its end after its terminal checkpoint is recorded, with
	// IsReplay false. An operation that suspends the invocation has no
	// outcome yet and dispatches no end; the later invocation that
	// observes its terminal checkpoint dispatches the end with IsReplay
	// true and the checkpointed timestamps, result, and error. An
	// operation replayed from a terminal checkpoint dispatches the end
	// with IsReplay true on every invocation that replays it, with one
	// exception: a succeeded [Step] whose checkpointed result fails to
	// decode dispatches no end on that invocation (see OnOperationStart).
	// It is dispatched from the goroutine that runs the operation: on
	// that goroutine with one plugin registered, on a goroutine the
	// dispatch joins with several.
	OnOperationEnd func(ctx context.Context, info OperationHookInfo)

	// OnOperationAttemptStart is called before each attempt of a
	// retryable operation runs its body: a [Step] body or a
	// [WaitForCondition] check. It fires after OnOperationStart of the
	// operation and before WrapOperationAttemptFn. Attempts are numbered
	// from 1 and the numbering continues across invocations. Only a live
	// attempt fires it: an attempt whose outcome is checkpointed is not
	// run again, so a replayed operation dispatches no attempt hooks. It
	// is dispatched from the goroutine that runs the operation: on that
	// goroutine with one plugin registered, on a goroutine the dispatch
	// joins with several.
	OnOperationAttemptStart func(ctx context.Context, info AttemptHookInfo)

	// OnOperationAttemptEnd is called after each attempt of a retryable
	// operation has an outcome. With [PluginAttemptSucceeded] it fires
	// after the attempt's result is checkpointed and before
	// OnOperationEnd. With [PluginAttemptFailed] it fires when the body
	// returns an error, or when the result cannot be serialized or is
	// too large, before the retry decision: a retry that follows suspends
	// the invocation and the next attempt fires OnOperationAttemptStart
	// in a later invocation; a final failure fires OnOperationEnd. Like
	// OnOperationAttemptStart it fires only for live attempts. It is
	// dispatched from the goroutine that runs the operation: on that
	// goroutine with one plugin registered, on a goroutine the dispatch
	// joins with several.
	OnOperationAttemptEnd func(ctx context.Context, info AttemptEndHookInfo)

	// OnOperationChange is called at most once per invocation, after
	// OnInvocationStart and before WrapInvocation, when at least one
	// operation changed status externally between the previous invocation
	// and this one: a wait that elapsed, a callback that was resolved, a
	// chained invoke that finished. It does not fire on an invocation
	// with no such change, including the first. The info's
	// UpdatedOperations is the same map [InvocationHookInfo] carries
	// under that name; every entry has IsReplay set. It is dispatched
	// from the goroutine that received the invocation: on that goroutine
	// with one plugin registered, on a goroutine the dispatch joins with
	// several.
	OnOperationChange func(ctx context.Context, info OperationChangeHookInfo)

	// WrapInvocation wraps the user handler invocation. It fires once per
	// invocation, after OnInvocationStart and OnOperationChange and
	// before every operation-level hook; OnInvocationEnd fires after it
	// returns. The outer plugin (index 0) wraps first. fn must be called
	// exactly once; the context passed to fn is the parent of the
	// handler's [Context]. fn returns the handler's result and error,
	// which the hook must return unchanged; see the wrap-hook contract in
	// the [Plugin] documentation. It runs on the goroutine that received
	// the invocation however many plugins are registered.
	WrapInvocation func(ctx context.Context, info InvocationHookInfo, fn func(ctx context.Context) (any, error)) (any, error)

	// WrapOperationAttemptFn wraps the execution of an operation attempt
	// body: a [Step] body or a [WaitForCondition] check. It fires for
	// each live attempt, after OnOperationAttemptStart and before
	// OnOperationAttemptEnd, and never for a replayed operation. fn must
	// be called exactly once; the context passed to fn is the parent of
	// the body's [StepContext]. See the wrap-hook contract in the
	// [Plugin] documentation. It runs on the goroutine that runs the
	// operation however many plugins are registered; hooks of concurrent
	// operations may run in parallel.
	WrapOperationAttemptFn func(ctx context.Context, info AttemptHookInfo, fn func(ctx context.Context) (any, error)) (any, error)

	// WrapChildContextFn wraps the execution of a child-context function:
	// the body passed to [RunInChildContext], [RunInChildContextAsync],
	// or [Go], including a child under [WithChildVirtual]. It fires after
	// OnOperationStart of the child and before its OnOperationEnd, with
	// IsReplay false for a live child and IsReplay true for a child
	// re-entered before it settled or a virtual child replaying the
	// operations inside it. A child replayed from a terminal checkpoint
	// dispatches only its end and, in general, does not run its body, so
	// the hook does not fire. One such child does run its body: a
	// succeeded child whose result was too large to checkpoint runs its
	// body again to rebuild the result. That run is not wrapped, so the
	// hook does not fire for it either; its OnOperationEnd, with the
	// checkpointed timestamps, is dispatched before the body runs. [Map]
	// and [Parallel] items and [WaitForCallback] are context operations
	// too but do not fire it. fn must be called exactly once; the
	// context passed to fn is the parent of the child [Context]. See the
	// wrap-hook contract in the [Plugin] documentation. It runs on the
	// goroutine that runs the child body however many plugins are
	// registered.
	WrapChildContextFn func(ctx context.Context, info OperationHookInfo, fn func(ctx context.Context) (any, error)) (any, error)

	// EnrichLogContext returns additional key-value pairs to merge into
	// every log record emitted through [Context.Logger] and
	// [StepContext.Logger]. It is called once per record that the
	// logger emits, after replay suppression has been decided. Under
	// [ReplayLogModeSuppress], the default, a record written while the
	// context replays is dropped and the hook is not called for it. Under
	// [ReplayLogModeEmit] such a record is emitted with the attribute
	// replay=true, and the hook is called for it as for a live record.
	// ctx is the record's context: the one passed to a *Context logging
	// method such as [slog.Logger.InfoContext], else context.Background().
	// It runs on the goroutine that logs however many plugins are
	// registered, so it may run concurrently with itself and with every
	// other hook.
	//
	// Each returned entry becomes an attribute of the record unless its
	// key is already taken. Precedence, highest first: the SDK's own
	// fields (timestamp, level, message, requestId, executionArn,
	// tenantId, operationId, operationName, attempt, replay), then attributes the
	// user supplied with the record or through [slog.Logger.With], then
	// plugin fields. A plugin field under a taken key is dropped, so a
	// plugin cannot overwrite the SDK's identifiers. Keys are compared by
	// qualified path: a plugin field lands under the groups the logger has
	// opened with [slog.Logger.WithGroup], and collides only with an
	// attribute at that same path, with the children of an empty-key group
	// counting at the enclosing path. The SDK's own fields are top-level,
	// so under an open group a plugin field may use their names. A group
	// the logger opens after an attribute was attached at that same path
	// is also taken: the plugin fields would form a second object under
	// that key beside the attached one, so none are added to records
	// logged through that logger. When several plugins implement the
	// hook, their maps are merged in registration order and a later
	// plugin's value replaces an earlier one's under the same key. Fields
	// are added in key order. A hook that panics contributes no fields
	// and does not fail the log call or the invocation. When no plugin
	// implements the hook, no per-record work is done.
	EnrichLogContext func(ctx context.Context) map[string]any
}

Plugin configures an EXPERIMENTAL instrumentation plugin that observes durable execution lifecycle events. Plugins implement only the hooks they need by setting non-nil function fields; nil fields are skipped with zero overhead. Register plugins with WithPlugins. Construct a Plugin with keyed fields: a minor release may add hook fields.

Experimental

The plugin API is experimental. It comprises this type, WithPlugins, WithPluginChildOperationsDepth, the hook info types (InvocationHookInfo, InvocationEndHookInfo, OperationHookInfo, AttemptHookInfo, AttemptEndHookInfo, OperationChangeHookInfo), and the PluginInvocationStatus, PluginOperationStatus, and PluginAttemptOutcome constants. Any of it may change or be removed in a later release without notice. Construct Plugin and the hook info types with keyed fields, because fields may be added, and tolerate status and outcome values you do not know. The release notes record each change to the plugin API.

Dispatch

The notification hooks (the On* fields) are dispatched synchronously at the lifecycle point they report. The dispatching goroutine is the one that reached the point: the goroutine that received the invocation for the invocation-level hooks, and the goroutine that runs the operation for the operation-level hooks. It calls every registered plugin's hook and waits for all of them to return before it proceeds. With one registered plugin the hook runs on the dispatching goroutine itself. With several, each plugin's hook runs on a goroutine started for that dispatch, and the dispatching goroutine joins them all before it proceeds; the hooks of different plugins for one notification may therefore run in parallel, and no order is defined among them. In this documentation, a hook that is "dispatched from" a goroutine runs on that goroutine when one plugin is registered and on a goroutine that the dispatch joins when several are. Either way the dispatching goroutine waits, so a slow hook delays execution for as long as it runs. A hook receives a copy of its info, so it cannot alter the operation it observes. A notification hook that panics is recovered and the panic discarded; the hook never affects execution.

The wrap hooks (the Wrap* fields) run on the dispatching goroutine however many plugins are registered. They compose in registration order: the plugin at index 0 is outermost, and each hook receives the next as fn. See the wrap-hook contract below.

Hooks fire in this order within one invocation: OnInvocationStart, then OnOperationChange when any operation changed externally since the previous invocation, then WrapInvocation around the handler. The operation-level hooks fire inside the handler as the operations run. OnInvocationEnd fires last, once the invocation's outcome is decided and recorded. Exactly one OnInvocationStart and exactly one OnInvocationEnd fire per invocation. Every operation-level hook receives an OperationHookInfo whose IsReplay reports whether the operation is replayed from a checkpoint; each hook's documentation states when it fires on replay. An operation beyond the depth set by WithPluginChildOperationsDepth fires no operation-level hook.

Concurrency

Hook dispatches from concurrent branches of one execution run in parallel. The items of a Map or Parallel with a concurrency above one, the bodies started by Go, StepAsync, RunInChildContextAsync, and the other Async variants, and the operations nested inside them each dispatch their operation-level hooks from their own goroutine, and the SDK does not serialize those dispatches. EnrichLogContext runs on the goroutine that logs, which may be any goroutine holding a Context or StepContext. Every field of a Plugin must therefore be safe for concurrent use from multiple goroutines: a hook that mutates state shared with other hooks or with user code must guard it with its own synchronization. Under this contract the SDK is free of data races; the package tests exercise every hook from concurrent branches under the race detector.

The invocation-level hooks (OnInvocationStart, OnOperationChange, WrapInvocation, OnInvocationEnd) are dispatched in sequence from the goroutine that received the invocation: each dispatch completes before the next begins. So within one invocation, one plugin's invocation-level hooks never overlap one another. The hooks of one operation are dispatched in sequence from the goroutine that runs the operation, in the order start, attempt hooks, wrap hook, end, each completing before the next is dispatched. So one plugin's hooks for one operation never overlap one another either. Both guarantees hold per plugin: with several plugins registered, the hooks of different plugins for the same notification run in parallel, as the Dispatch section states. Neither guarantee extends across operations or across invocations: a hook for one operation may run concurrently with a hook for another operation on a concurrent branch, and EnrichLogContext may run concurrently with any hook.

Wrap hooks

WrapInvocation, WrapOperationAttemptFn, and WrapChildContextFn receive the wrapped work as fn and must call fn exactly once, returning its result. fn takes a context.Context. The context a hook passes to fn becomes the parent of the context the wrapped user code observes: the handler's Context for WrapInvocation, the StepContext of a step body or condition check for WrapOperationAttemptFn, and the child Context for WrapChildContextFn. A hook that attaches a value to the ctx it received and passes the derived context to fn makes that value readable inside the user code, and inside every hook nested further in. A hook that passes the ctx it received unchanged causes no behavior change. A nil context is treated as the ctx the hook received.

The SDK preserves the cancellation, the deadline, and the values of the context it gave the outermost hook. When a hook passes fn any context other than the one it received, the user code observes a context that is cancelled when either context is cancelled, reports the earlier of the two deadlines, and falls back to the SDK's context for values the hook's context lacks. This holds whether or not the hook's context descends from the SDK's. A hook therefore cannot detach the wrapped work from the invocation's cancellation or deadline. That merged context is cancelled once fn returns; user code must not keep using it after the step body, condition check, child context function, or handler it was given to has returned.

The SDK runs the wrapped work at most once regardless of what the hook does. A hook that calls fn again receives the first call's result; the context passed to the later call is ignored. A hook that panics is contained: if it panics before calling fn, the SDK runs fn once with the ctx the hook received and uses that result; if it panics after calling fn, the SDK uses the result fn already produced. If fn itself panics, that panic reaches the SDK as it would without the hook, even if the hook recovers it and returns normally or calls fn again. A hook that panics while fn is still running on another goroutine fails the wrapped work with an error; fn is still not run again.

The error fn returns may be the SDK's internal signal that the invocation is suspending. A wrap hook must return fn's result and error unchanged; a hook that replaces the error breaks suspension and replay.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

type PluginAttemptOutcome

type PluginAttemptOutcome string

PluginAttemptOutcome is the result of an operation attempt.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

const (
	PluginAttemptSucceeded PluginAttemptOutcome = "SUCCEEDED"
	PluginAttemptFailed    PluginAttemptOutcome = "FAILED"
)

Attempt outcome constants for plugin hooks.

type PluginInvocationStatus

type PluginInvocationStatus string

PluginInvocationStatus is the invocation outcome visible to plugins.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

const (
	// PluginInvocationSucceeded reports that the handler returned a result
	// and the execution has succeeded. ExecutionResult carries the result.
	PluginInvocationSucceeded PluginInvocationStatus = "SUCCEEDED"

	// PluginInvocationFailed reports that the execution has failed: the
	// handler returned an error that is not scoped to the invocation, or
	// the SDK could not record the result and the failure is scoped to the
	// execution. ExecutionError carries the error. The execution is not
	// invoked again.
	PluginInvocationFailed PluginInvocationStatus = "FAILED"

	// PluginInvocationPending reports that the invocation suspended
	// normally: the handler is blocked on an operation that completes
	// later, such as a wait, a callback, or a chained invoke, or the
	// service stopped accepting this invocation's checkpoints. The
	// execution resumes in a later invocation once that operation
	// completes. ExecutionResult and ExecutionError are nil.
	PluginInvocationPending PluginInvocationStatus = "PENDING"

	// PluginInvocationRetrying reports that the invocation ended by
	// returning an error to Lambda instead of reporting an outcome for the
	// execution: the handler returned an error scoped to the invocation, or
	// the SDK could not serialize or record the handler's result and that
	// failure is not scoped to the execution. The service invokes the
	// execution again from its last checkpoint. ExecutionError carries the
	// error. A plugin that opens a span per execution should leave it open,
	// as for Pending.
	PluginInvocationRetrying PluginInvocationStatus = "RETRYING"
)

Invocation status constants for plugin hooks.

Succeeded and Failed are terminal: the execution has finished and no further invocation follows. Pending and Retrying both mean the execution continues in a later invocation; they differ in why this one ended.

type PluginOperationStatus

type PluginOperationStatus string

PluginOperationStatus is an operation's lifecycle status visible to plugins.

EXPERIMENTAL: this type is experimental and may be changed or removed in future releases.

const (
	PluginOperationStarted   PluginOperationStatus = "STARTED"
	PluginOperationReady     PluginOperationStatus = "READY"
	PluginOperationPending   PluginOperationStatus = "PENDING"
	PluginOperationSucceeded PluginOperationStatus = "SUCCEEDED"
	PluginOperationFailed    PluginOperationStatus = "FAILED"
	PluginOperationTimedOut  PluginOperationStatus = "TIMED_OUT"
	PluginOperationStopped   PluginOperationStatus = "STOPPED"
	PluginOperationCancelled PluginOperationStatus = "CANCELLED"
)

Operation status constants for plugin hooks. A later release may add constants; a plugin that switches on the status should tolerate values it does not know.

type PreviewConfig

type PreviewConfig struct {

	// Mode is the base visibility. The zero value is [PreviewIncludeAll].
	Mode PreviewMode

	// Include lists fields shown under [PreviewExcludeAll]. It has no
	// effect under [PreviewIncludeAll], where every field is already
	// visible.
	Include []PreviewField

	// Exclude lists fields that are never shown. Exclude wins over
	// Include and over Mask, and nothing below an excluded field is
	// traversed.
	Exclude []PreviewField

	// Mask lists fields shown with their value replaced by MaskString. A
	// masked field is visible under either mode unless it is also
	// excluded. When the masked field holds an object or a slice, the whole
	// value is replaced by MaskString.
	Mask []PreviewField

	// MaskString replaces the value of masked fields. Empty means
	// [DefaultPreviewMaskString].
	MaskString string

	// MaxPreviewBytes caps the JSON size of the returned preview. Fields
	// are added in traversal order until the next one would exceed the
	// cap. Zero or negative means [DefaultPreviewMaxBytes].
	MaxPreviewBytes int

	// MaxDepth bounds how many nested objects and slices are traversed. The
	// root value is level 0, so with MaxDepth 1 a top-level field holding an
	// object or slice is omitted while scalar top-level fields are kept. A
	// masked field beyond the limit is still shown as MaskString. Zero or
	// negative means [DefaultPreviewMaxDepth].
	MaxDepth int
	// contains filtered or unexported fields
}

PreviewConfig configures BuildPreview.

type PreviewField

type PreviewField struct {

	// Name is the field name, or the dot-separated path from the root when
	// Match is [FieldMatchPath].
	Name string

	// Match is how Name is compared with a field's path. The zero value is
	// [FieldMatchAnywhere].
	Match FieldMatchMode
	// contains filtered or unexported fields
}

PreviewField selects fields in the include, exclude, and mask lists of a PreviewConfig.

A field is addressed by the JSON name it is serialized under, so a struct field with a `json:"user_id"` tag is selected by "user_id". Dots separate path segments, so a field whose JSON name contains a dot cannot be selected and is left out of every preview.

type PreviewMode

type PreviewMode int

PreviewMode selects which fields a preview built by BuildPreview shows before the include, exclude, and mask lists are applied.

const (
	// PreviewIncludeAll starts with every field visible. Fields in
	// [PreviewConfig.Exclude] are then hidden and fields in
	// [PreviewConfig.Mask] are shown redacted. This is the default.
	PreviewIncludeAll PreviewMode = iota

	// PreviewExcludeAll starts with no field visible. Fields in
	// [PreviewConfig.Include] are then shown and fields in
	// [PreviewConfig.Mask] are shown redacted.
	PreviewExcludeAll
)

type ReplayLogMode

type ReplayLogMode int

ReplayLogMode selects what happens to a log record emitted while the emitting context is replaying checkpointed operations. Set it for the whole handler with WithReplayLogMode or for the rest of an invocation with ConfigureLogging.

const (
	// ReplayLogModeUnchanged keeps the mode currently in effect. It is the
	// zero value, so a [LogConfig] that leaves the field unset does not
	// change the mode. [WithReplayLogMode] treats it as
	// [ReplayLogModeSuppress].
	ReplayLogModeUnchanged ReplayLogMode = iota

	// ReplayLogModeSuppress drops every record a context emits while it is
	// replaying, before the record is built, so replayed code does not
	// duplicate the lines it wrote when it first ran. This is the default.
	ReplayLogModeSuppress

	// ReplayLogModeEmit emits the records a context writes while it is
	// replaying, each with the attribute replay=true; a live record carries
	// no replay attribute. The mode is for diagnosing a replay problem: the
	// output shows what the replayed code did up to the point where the
	// problem appeared. Every line written before the execution suspended
	// appears again on each later invocation, once per invocation that
	// replays it, so expect duplicate lines. Records emitted in this mode
	// still pass through the handler's level filter and, for the default
	// handler, AWS_LAMBDA_LOG_LEVEL.
	//
	// The top-level replay key belongs to the SDK in every mode. An
	// attribute named replay that a handler body adds at the top level,
	// through [slog.Logger.With] or with a record, is dropped so the key
	// stays single-valued; the same name inside a group the logger opened
	// with [slog.Logger.WithGroup] is kept.
	ReplayLogModeEmit
)

type ResultTooLargeError

type ResultTooLargeError struct {
	// Name is the operation's name.
	Name string

	// SizeBytes is the serialized result's size.
	SizeBytes int

	// LimitBytes is the threshold that was exceeded.
	LimitBytes int
	// contains filtered or unexported fields
}

ResultTooLargeError indicates that a single operation's serialized result exceeds the checkpoint payload limit. The caller should restructure the operation to return a reference (e.g., an S3 key) instead of the full payload, or supply a custom Serdes that offloads to external storage.

A value rebuilt from a checkpoint record (for example the Err of a rejected Settled) carries Name and the recorded Error() text; SizeBytes and LimitBytes are zero.

func (*ResultTooLargeError) As

func (e *ResultTooLargeError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*ResultTooLargeError) Error

func (e *ResultTooLargeError) Error() string

type RetryAttempt

type RetryAttempt struct {

	// Err is the error from the attempt that just failed.
	Err error

	// Attempt is the 1-based number of the attempt that just failed,
	// inclusive of the first attempt.
	Attempt int

	// Elapsed is the time since the first attempt began. It is zero when
	// the SDK does not track it.
	Elapsed time.Duration
	// contains filtered or unexported fields
}

RetryAttempt describes a failed attempt to a RetryStrategy.

type RetryConfig

type RetryConfig struct {

	// MaxAttempts is the maximum number of total attempts, including the
	// first. The default is 3. It must not be negative.
	MaxAttempts int

	// InitialDelay is the delay before the first retry. The default is
	// 5 seconds. When set, it must be at least 1 second.
	InitialDelay time.Duration

	// MaxDelay caps the delay between retries. The default is 5 minutes.
	// When set, it must be at least 1 second.
	MaxDelay time.Duration

	// BackoffRate multiplies the delay after each attempt. The default
	// is 2. It must be a finite value and must not be negative.
	BackoffRate float64

	// Jitter is the jitter strategy applied to computed delays. The
	// default is [JitterFull]. When set, it must be one of the defined
	// [JitterStrategy] constants.
	Jitter JitterStrategy

	// RetryableErrors restricts retries to errors that at least one
	// matcher reports as retryable. When empty, every error is retryable.
	// A non-matching error is not retried: the step fails on that attempt
	// with the attempts made so far. Entries must not be nil.
	//
	// Build matchers with [ErrorIs] for sentinel errors, [ErrorAs] for
	// error types, and [ErrorContains] or [ErrorMatches] for message
	// patterns:
	//
	//	durable.RetryConfig{
	//		RetryableErrors: []durable.ErrorMatcher{
	//			durable.ErrorAs[*TransientError](),
	//			durable.ErrorIs(io.ErrUnexpectedEOF),
	//			durable.ErrorContains("throttl"),
	//		},
	//	}
	//
	// RetryableErrors applies to the strategy [NewRetryStrategy] builds
	// from this config. A hand-written [RetryStrategy] is not filtered; it
	// sees every failed attempt. To combine the two, have the hand-written
	// strategy delegate to the configured one, which then applies the
	// matchers, or apply an [ErrorMatcher] directly to [RetryAttempt.Err].
	RetryableErrors []ErrorMatcher
	// contains filtered or unexported fields
}

RetryConfig configures an exponential backoff retry strategy created with NewRetryStrategy or MustNewRetryStrategy. The zero value of each field selects its documented default.

The zero value of RetryConfig is not the strategy ExponentialBackoff returns. RetryConfig{} produces 3 total attempts with delays of 5 s and 10 s before jitter, capped at 5 minutes. ExponentialBackoff produces 6 total attempts with delays of 5 s, 10 s, 20 s, 40 s, and 60 s before jitter, capped at 60 seconds. Both apply full jitter.

type RetryDecision

type RetryDecision struct {

	// Retry indicates whether the operation should be attempted again.
	Retry bool

	// Delay is how long to wait before the next attempt. It is ignored
	// when Retry is false.
	//
	// The SDK sends the delay as a whole number of seconds, rounding a
	// fractional delay up. A zero Delay selects [DefaultRetryDelay], so a
	// strategy that returns RetryDecision{Retry: true} without setting
	// Delay waits one second. A negative Delay is an error that fails the
	// step.
	Delay time.Duration
	// contains filtered or unexported fields
}

RetryDecision is a retry strategy's verdict for a failed attempt.

type RetryError

type RetryError struct {
	// Name is the retry group's name, or the empty string for an unnamed
	// group.
	Name string

	// Attempts is the number of times fn ran.
	Attempts int

	// ErrorType is the wire ErrorType of the final attempt's error.
	ErrorType string

	// Message is the final attempt's recorded error message.
	Message string

	// ErrorData is the payload attached with [WithErrorData], if any.
	ErrorData string

	// StackTrace holds recorded stack trace lines, when captured.
	StackTrace []string

	// Err is the final attempt's error.
	Err error
}

RetryError indicates that a Retry group failed: its retry strategy stopped retrying after the final attempt failed.

ErrorType and Message describe the error that escaped fn on the final attempt. Err is that attempt's error as Retry observed it: a *ChildContextError naming the attempt when attempts run in child contexts (the default), or the error fn returned when they do not. A value rebuilt from a checkpoint record carries a stand-in as Err and a zero Attempts. Match on ErrorType. See OperationError.

func (*RetryError) As

func (e *RetryError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*RetryError) Error

func (e *RetryError) Error() string

func (*RetryError) Unwrap

func (e *RetryError) Unwrap() error

type RetryOption

type RetryOption interface {
	// contains filtered or unexported methods
}

RetryOption configures a Retry operation.

func WithAttemptChildContext

func WithAttemptChildContext(enabled bool) RetryOption

WithAttemptChildContext sets whether Retry runs each attempt in its own child context. The default is true. See Retry for what changes when attempts run directly in the caller's context instead.

func WithAttemptChildOptions

func WithAttemptChildOptions(opts ...ChildOption) RetryOption

WithAttemptChildOptions supplies ChildOption values for the child context Retry creates for each attempt. WithChildSerdes selects the serializer for the attempt's result, WithChildSummary the summary of an oversized result, and WithChildErrorMapper the mapping of a failed attempt's *ChildContextError before the retry strategy sees it. The options are ignored when WithAttemptChildContext is false.

type RetryStrategy

type RetryStrategy func(RetryAttempt) RetryDecision

RetryStrategy decides whether and when to retry a failed step attempt. Strategies must be deterministic functions of the RetryAttempt they receive, except for randomized jitter in the returned delay.

To retry only specific errors, set RetryConfig.RetryableErrors or LinearRetryConfig.RetryableErrors on a configured strategy. A hand-written strategy receives every failed attempt and decides for itself; it can inspect RetryAttempt.Err with errors.Is or errors.As, or apply an ErrorMatcher, before delegating to a configured strategy:

transientOnly := func(a durable.RetryAttempt) durable.RetryDecision {
	var te *TransientError
	if !errors.As(a.Err, &te) {
		return durable.RetryDecision{}
	}
	return durable.ExponentialBackoff()(a)
}

func ExponentialBackoff

func ExponentialBackoff() RetryStrategy

ExponentialBackoff returns the default retry strategy: 6 total attempts with exponentially increasing delays, starting at 5 seconds, doubling each attempt, capped at 60 seconds, with full jitter.

func LinearBackoff

func LinearBackoff(cfg LinearRetryConfig) (RetryStrategy, error)

LinearBackoff returns a linear backoff retry strategy: the delay before retry n is InitialDelay + Increment × (n-1), capped at MaxDelay, with jitter applied, rounded to a whole number of seconds no less than one.

The zero value of cfg produces 6 total attempts with delays of 1 s, 2 s, 3 s, 4 s, and 5 s, without jitter. As a worked example, LinearRetryConfig{InitialDelay: 2 * time.Second, Increment: 3 * time.Second, MaxDelay: 10 * time.Second} produces delays of 2 s, 5 s, 8 s, 10 s, and 10 s: the fourth and fifth retries would be 11 s and 14 s but are capped.

It returns an error if cfg is invalid; see LinearRetryConfig for the constraints on each field. Zero-value fields are always valid and select their documented defaults.

func MustLinearBackoff

func MustLinearBackoff(cfg LinearRetryConfig) RetryStrategy

MustLinearBackoff is like LinearBackoff but panics if cfg is invalid. It is intended for initialization with hard-coded configurations, where invalid values are programming errors. MustLinearBackoff(LinearRetryConfig{}) produces 6 total attempts with delays of 1 s, 2 s, 3 s, 4 s, and 5 s, without jitter.

func MustNewRetryStrategy

func MustNewRetryStrategy(cfg RetryConfig) RetryStrategy

MustNewRetryStrategy is like NewRetryStrategy but panics if cfg is invalid. It is intended for initialization with hard-coded configurations, where invalid values are programming errors.

func NewRetryStrategy

func NewRetryStrategy(cfg RetryConfig) (RetryStrategy, error)

NewRetryStrategy returns an exponential backoff retry strategy: the delay before retry n is InitialDelay × BackoffRate^(n-1), capped at MaxDelay, with jitter applied, rounded to a whole number of seconds no less than one.

It returns an error if cfg is invalid; see RetryConfig for the constraints on each field. Zero-value fields are always valid and select their documented defaults.

func NoRetry

func NoRetry() RetryStrategy

NoRetry returns a strategy that never retries.

type Serdes

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

Serdes serializes and deserializes operation inputs and results for checkpoint storage. The default Serdes is JSONSerdes, which uses encoding/json.

Implementations receive the invocation's context.Context for cancellation and deadline propagation (e.g., when offloading payloads to external storage), and a SerdesContext with the operation's identity and the execution ARN, enabling context-aware serialization strategies such as filesystem offloading keyed by operation.

The interface is untyped: Marshal takes any and Unmarshal fills a pointer passed as any. It has to be, because one Serdes value can serve every operation result type in a handler. The handler-wide default set with WithSerdes and the payload-offloading serdes from NewFileSystemSerdes both do exactly that, so neither can carry a single type parameter. For a serdes written for one result type, use SerdesOf, which performs the type assertion once and hands typed values to your marshal and unmarshal functions.

func NewFileSystemSerdes

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

NewFileSystemSerdes creates a Serdes that offloads values to files under basePath and stores a reference envelope in the checkpoint.

basePath must be a durable, shared mount (EFS or S3 Files) — NOT Lambda's /tmp. On replay, a different execution environment may service the invocation, so /tmp files from a prior invocation are unavailable.

By default files are laid out under basePath as <functionName>/<executionName>/<invocationId>/<operationID>.json, with each segment percent-encoded (FileSystemPathEncodingURI). Set FileSystemSerdesConfig.PathEncoding to FileSystemPathEncodingHash for the hashed layout instead. The envelope records the full file path, so either layout reads back files written under the other.

File writes are atomic from a reader's perspective: each value is written to a temporary file in the target directory, synced, and renamed over the final path, so a concurrent reader sees either the previous complete file or the new complete file, never a partial one. This guarantee relies on the mount supporting atomic rename within a directory.

func SerdesOf

func SerdesOf[T any](
	marshal func(ctx context.Context, meta SerdesContext, v T) ([]byte, error),
	unmarshal func(ctx context.Context, meta SerdesContext, data []byte) (T, error),
) Serdes

SerdesOf adapts typed marshal and unmarshal functions to the Serdes interface. The returned Serdes rejects values of any other type with an error that names both the expected and the actual type.

Use SerdesOf for a serdes written for one result type. It performs the type assertion once so that marshal and unmarshal receive and return T directly:

masked := durable.SerdesOf(
	func(_ context.Context, _ durable.SerdesContext, r Receipt) ([]byte, error) {
		r.Card = "****" + r.Card[len(r.Card)-4:]
		return json.Marshal(r)
	},
	func(_ context.Context, _ durable.SerdesContext, b []byte) (Receipt, error) {
		var r Receipt
		return r, json.Unmarshal(b, &r)
	},
)
receipt, err := durable.Step(ctx, "charge", chargeCard, durable.WithStepSerdes(masked))

T is inferred from the function arguments. The returned Serdes works with every With*Serdes option. When attached handler-wide with WithSerdes it serves every operation result in the handler, so an operation whose result is not T fails at runtime with a SerdesError. That is the intended behaviour: a handler-wide serdes must accept every result type it is asked to serialize, and SerdesOf accepts exactly one.

type SerdesConfig

type SerdesConfig struct {

	// Serdes replaces the default serializer for operation results: steps,
	// child contexts, invokes, and condition state. It has the same role as
	// [WithSerdes].
	Serdes Serdes

	// CallbackDeserializer replaces the default deserializer for callback
	// payloads submitted by external systems. It has the same role as
	// [WithCallbackDeserializer]. Once set it cannot be cleared: to return
	// callbacks to the standard serdes, set Serdes and pass a
	// CallbackDeserializer that delegates to it.
	CallbackDeserializer Deserializer
	// contains filtered or unexported fields
}

SerdesConfig names the handler-level serializer defaults that ConfigureSerdes replaces. A nil field keeps the value currently in effect, so a config that sets only one field leaves the other unchanged.

type SerdesContext

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
	// contains filtered or unexported fields
}

SerdesContext provides contextual information to a Serdes implementation, enabling context-aware serialization strategies (e.g., using the execution ARN or operation ID in file paths for a filesystem-backed serdes).

type SerdesError

type SerdesError struct {
	// Operation is the name of the durable operation whose serdes failed.
	Operation string

	// Direction is "marshal" or "unmarshal", indicating whether
	// serialization or deserialization failed.
	Direction string

	// Err is the underlying serdes failure.
	Err error
}

SerdesError indicates that a serialization or deserialization operation failed. It wraps the underlying serdes failure with context about which operation and direction (marshal/unmarshal) triggered it.

A SerdesError returned directly by an operation holds the serdes's own error as Err. A SerdesError reached through a typed operation error (for example a StepError whose ErrorType is "SerdesError") is rebuilt from the recorded failure, and its Err is a stand-in carrying the recorded message; match on the type and the operation error's ErrorType.

func (*SerdesError) Error

func (e *SerdesError) Error() string

func (*SerdesError) Unwrap

func (e *SerdesError) Unwrap() error

type Settled

type Settled[O any] struct {

	// Value is the future's result. It is the zero value when Err is
	// non-nil.
	Value O

	// Err is the future's error, or nil if the future succeeded.
	//
	// After a checkpoint round trip (AllSettled runs in a child context),
	// Err is rebuilt from the serialized outcome: an SDK error type is
	// rebuilt as that type with its [OperationError] fields, so
	// [errors.As] matches it and a timed-out callback still matches
	// [ErrCallbackTimedOut]; any other error is a stand-in whose Error()
	// is "<ErrorType>: <message>". Fields outside [OperationError], such
	// as [StepError.Attempts], are zero after the round trip.
	Err error
	// contains filtered or unexported fields
}

Settled is the per-future outcome returned by AllSettled.

func AllSettled

func AllSettled[O any](ctx Context, name string, fs []*Future[O], opts ...ChildOption) ([]Settled[O], error)

AllSettled records a combinator operation and waits for every future to settle, returning each outcome in input order regardless of success or failure. If any future suspends, AllSettled awaits all remaining futures and then propagates the suspension; suspension takes precedence over terminal outcomes because the suspended branch completes only on a later invocation.

AllSettled uses RunInChildContext internally, so the aggregate result is checkpointed. On replay, the stored outcomes are returned without re-awaiting the futures. opts configure that child-context operation; WithChildSerdes selects the serializer for the aggregate result.

Empty input returns an empty slice immediately.

func (Settled[O]) MarshalJSON

func (s Settled[O]) MarshalJSON() ([]byte, error)

MarshalJSON serializes a Settled value. A rejected outcome records the error's message, its wire ErrorType, and, for an SDK operation error, the OperationError fields, so the SDK type is rebuilt on deserialization.

The message is taken from [recordOf], not from Error(). A stand-in's Error() is "<ErrorType>: <message>"; storing that text would prefix the type name again on every further round trip. [recordOf] yields the stand-in's raw message, so a value that is serialized, deserialized, and serialized again keeps the same message.

func (*Settled[O]) UnmarshalJSON

func (s *Settled[O]) UnmarshalJSON(data []byte) error

UnmarshalJSON deserializes a Settled value. A rejected outcome that names an SDK error type is rebuilt as that type, so errors.As matches it and errors.Is matches its sentinel; fields outside OperationError are zero. An unknown name yields a stand-in whose Error() is "<ErrorType>: <message>". A value in the older message-only form yields a stand-in with ErrorType "Error".

type StepContext

type StepContext interface {
	context.Context

	// Logger returns the logger for the current step body, condition
	// check, or callback submitter. Its records carry the execution
	// attributes of the enclosing context, this operation's ID as
	// operationId, its name as operationName when it has one, and the
	// attempt number. The enclosing context's own operation attributes are
	// replaced, not repeated: a step inside a child context reports the
	// step, not the child.
	Logger() *slog.Logger

	// Attempt returns the 1-based attempt number of the current
	// execution of the user function. The first attempt is 1. It applies
	// to step bodies, to condition checks (where it is the poll attempt
	// number, the same value the wait strategy receives), and to callback
	// submitters (where it is the submitter's retry attempt).
	Attempt() int
	// contains filtered or unexported methods
}

StepContext is the context passed to step bodies, condition checks, and callback submitters. It implements context.Context, so it can be passed directly to AWS SDK calls made inside the step.

StepContext deliberately exposes no durable operations: a step is a single atomic unit of work. To group durable operations, use RunInChildContext or Go.

StepContext is sealed: only the SDK can implement it. External types that embed or imitate this interface will fail to compile because of the unexported method. Sealing lets the SDK add methods to StepContext without breaking user code.

type StepDetails

type StepDetails struct {
	// Attempt is the current attempt number.
	Attempt int32

	// Result is the serialized step result, set on success.
	Result *string

	// Error carries failure details, set on failure or pending retry.
	Error *ErrorObject

	// NextAttemptTimestamp is when the next retry attempt is scheduled.
	// Set only while the step is pending.
	NextAttemptTimestamp *time.Time
}

StepDetails carries STEP operation state.

type StepError

type StepError struct {
	// Name is the step's name, or the empty string for unnamed steps.
	Name string

	// Attempts is the number of times the step body executed.
	Attempts int

	// ErrorType is the wire ErrorType of the final attempt's error.
	ErrorType string

	// Message is the final attempt's recorded error message.
	Message string

	// ErrorData is the payload attached with [WithErrorData], if any.
	ErrorData string

	// StackTrace holds recorded stack trace lines, when captured.
	StackTrace []string

	// Err is the stand-in for the final attempt's error.
	Err error
}

StepError indicates that a step failed after exhausting its retry strategy.

Err is a stand-in rebuilt from ErrorType and Message; it is the same on the first invocation and on replay and never holds the step body's original error value. Match on ErrorType rather than with errors.As against the body's error type. See OperationError.

func (*StepError) As

func (e *StepError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*StepError) Error

func (e *StepError) Error() string

func (*StepError) Unwrap

func (e *StepError) Unwrap() error

type StepInterruptedError

type StepInterruptedError struct {
	// Name is the step's name, or the empty string for unnamed steps.
	Name string
	// contains filtered or unexported fields
}

StepInterruptedError indicates that a step with at-most-once-per-retry semantics was interrupted before recording an outcome, and was not re-executed. It is passed to the step's retry strategy as the failed attempt's error. It records no cause, so the OperationError it reaches has an empty ErrorType and a nil Err.

A value rebuilt from a checkpoint record (see ErrorFromObject) keeps the recorded Error() text; Name is empty.

func (*StepInterruptedError) As

func (e *StepInterruptedError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*StepInterruptedError) Error

func (e *StepInterruptedError) Error() string

type StepOption

type StepOption interface {
	// contains filtered or unexported methods
}

StepOption configures a single step operation.

func WithRetry

func WithRetry(s RetryStrategy) StepOption

WithRetry sets the step's retry strategy. The default is ExponentialBackoff.

func WithSemantics

func WithSemantics(s StepSemantics) StepOption

WithSemantics sets the step's execution guarantee. The default is AtLeastOncePerRetry.

func WithStepSerdes

func WithStepSerdes(s Serdes) StepOption

WithStepSerdes overrides the serializer for this step's result.

type StepOptions

type StepOptions struct {
	// NextAttemptDelaySeconds is the delay before the next retry attempt.
	NextAttemptDelaySeconds *int32
}

StepOptions configures a STEP operation update.

type StepSemantics

type StepSemantics int

StepSemantics selects a step's execution guarantee across retries.

const (
	// AtLeastOncePerRetry re-executes a step whose previous invocation
	// was interrupted before recording an outcome. The step body may run
	// more than once per retry attempt. This is the default.
	AtLeastOncePerRetry StepSemantics = iota

	// AtMostOncePerRetry never re-executes an interrupted attempt.
	// Interruption is treated as a failed attempt and consumes one retry
	// from the step's retry strategy.
	AtMostOncePerRetry
)

Step execution guarantees.

type Void

type Void struct{}

Void is the result type of operations that produce no value, such as WaitAsync.

type WaitConfig

type WaitConfig[S any] struct {

	// MaxAttempts is the maximum number of checks, including the first.
	// Reaching it with the condition still unmet fails the operation with
	// a [*WaitForConditionError]. The default is 60. It must not be
	// negative.
	MaxAttempts int

	// InitialDelay is the delay before the second check. The default is
	// 5 seconds. When set, it must be at least 1 second.
	InitialDelay time.Duration

	// MaxDelay caps the delay between checks. The default is 5 minutes.
	// When set, it must be at least 1 second.
	MaxDelay time.Duration

	// BackoffRate multiplies the delay after each check. The default is
	// 1.5. It must be a finite value and must not be negative.
	BackoffRate float64

	// Jitter is the jitter strategy applied to computed delays. The
	// default is [JitterFull]. When set, it must be one of the defined
	// [JitterStrategy] constants.
	Jitter JitterStrategy

	// ShouldContinue reports whether to keep polling given the state the
	// latest check returned. When it returns false the condition is met:
	// the operation succeeds with that state, even on the final attempt.
	//
	// When nil, every check continues polling, so the operation can only
	// end by reaching MaxAttempts. Set it to make the wait succeed.
	ShouldContinue func(state S) bool
	// contains filtered or unexported fields
}

WaitConfig configures an exponential backoff wait strategy created with NewWaitStrategy or MustNewWaitStrategy. The zero value of each field selects its documented default. Field names match RetryConfig where the meaning is the same; only the defaults differ.

WaitConfig[S]{} builds the strategy WaitForCondition uses when ConditionConfig.WaitStrategy is nil: poll with a 5 second initial delay multiplied by 1.5 after each attempt, capped at 5 minutes, with full jitter, and fail once 60 attempts have been made.

type WaitDecision

type WaitDecision struct {

	// Continue indicates whether to keep waiting and check again.
	Continue bool

	// Delay is how long to suspend before the next check. It is ignored
	// when Continue is false.
	Delay time.Duration

	// Err, when non-nil and Continue is false, signals that the
	// operation should fail with this error rather than succeed. Use it
	// to implement max-attempts or timeout strategies.
	Err error
	// contains filtered or unexported fields
}

WaitDecision is a condition wait strategy's verdict after a check.

A wait strategy returns one of three outcomes:

  • Continue: keep polling (Continue true, Delay set).
  • Stop: the condition is met, return the state (Continue false, Err nil).
  • Fail: an operational limit (such as max attempts) was exceeded (Continue false, Err non-nil). The error is checkpointed and returned as a *WaitForConditionError.

type WaitDetails

type WaitDetails struct {
	// ScheduledEndTimestamp is when the wait completes.
	ScheduledEndTimestamp *time.Time
}

WaitDetails carries WAIT operation state.

type WaitForCallbackOption

type WaitForCallbackOption interface {
	// contains filtered or unexported methods
}

WaitForCallbackOption configures a WaitForCallback operation. It accepts every CallbackOption plus options that only apply to the submitter step, such as WithSubmitterRetry.

func WithSubmitterRetry

func WithSubmitterRetry(s RetryStrategy) WaitForCallbackOption

WithSubmitterRetry configures a retry strategy for the submitter step in WaitForCallback. The submitter function re-executes on failure according to this strategy.

WithSubmitterRetry is a WaitForCallbackOption only. CreateCallback has no submitter step, so passing this option to it is a compile error.

type WaitForConditionError

type WaitForConditionError struct {
	// Name is the operation's name.
	Name string

	// Attempts is the number of times the check function was called.
	Attempts int

	// ErrorType is the wire ErrorType of the check or strategy error.
	ErrorType string

	// Message is the recorded error message.
	Message string

	// ErrorData is the payload attached with [WithErrorData], if any.
	ErrorData string

	// StackTrace holds recorded stack trace lines, when captured.
	StackTrace []string

	// Err is the stand-in for the failure from the check function or wait
	// strategy.
	Err error
}

WaitForConditionError indicates that a wait-for-condition operation failed: either the check function returned an error or the wait strategy decided to stop with an error.

Err is a stand-in rebuilt from ErrorType and Message, the same on the first invocation and on replay. Match on ErrorType. See OperationError.

func (*WaitForConditionError) As

func (e *WaitForConditionError) As(target any) bool

As supports errors.As matching against *OperationError.

func (*WaitForConditionError) Error

func (e *WaitForConditionError) Error() string

func (*WaitForConditionError) Unwrap

func (e *WaitForConditionError) Unwrap() error

type WaitOption

type WaitOption interface {
	// contains filtered or unexported methods
}

WaitOption configures a single Wait or WaitAsync operation.

The interface is sealed: only this package can implement it. No option constructors exist yet. The parameter is present so that options can be added later without changing the signatures of Wait and WaitAsync.

type WaitOptions

type WaitOptions struct {
	// WaitSeconds is the wait duration in seconds.
	WaitSeconds *int32
}

WaitOptions configures a WAIT operation update.

type WaitStrategy

type WaitStrategy[S any] func(state S, attempt int) WaitDecision

WaitStrategy decides, after each check of a WaitForCondition, whether to keep waiting and for how long. state is the value the check returned, round-tripped through the configured Serdes. attempt is the 1-based number of completed checks.

A strategy must be a deterministic function of its arguments, except for randomized jitter in the returned delay. Build one from declarative configuration with NewWaitStrategy or MustNewWaitStrategy, or write one by hand. ConditionConfig.WaitStrategy has this type's underlying function type, so a WaitStrategy assigns to it directly.

func MustNewWaitStrategy

func MustNewWaitStrategy[S any](cfg WaitConfig[S]) WaitStrategy[S]

MustNewWaitStrategy is like NewWaitStrategy but panics if cfg is invalid. It is intended for initialization with hard-coded configurations, where invalid values are programming errors.

func NewWaitStrategy

func NewWaitStrategy[S any](cfg WaitConfig[S]) (WaitStrategy[S], error)

NewWaitStrategy returns an exponential backoff wait strategy for WaitForCondition. After each check the strategy consults cfg.ShouldContinue; when it reports the condition met, the strategy stops and the operation succeeds. Otherwise, once cfg.MaxAttempts checks have been made, the strategy fails the operation. Otherwise the delay before check n+1 is InitialDelay × BackoffRate^(n-1), capped at MaxDelay, with jitter applied, rounded to a whole number of seconds no less than one.

It returns an error if cfg is invalid; see WaitConfig for the constraints on each field. Zero-value fields are always valid and select their documented defaults.

Directories

Path Synopsis
Package durabletest provides an in-memory local testing runner for durable handler functions.
Package durabletest provides an in-memory local testing runner for durable handler functions.
internal
wire
Package wire defines the JSON shapes exchanged between a durable function invocation and the durable execution service: the invocation input with its embedded operation log page, and the invocation response.
Package wire defines the JSON shapes exchanged between a durable function invocation and the durable execution service: the invocation input with its embedded operation log page, and the invocation response.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL