diff --git a/docs/advanced/extensions.md b/docs/advanced/extensions.md
index e165d3e78..19f1974be 100644
--- a/docs/advanced/extensions.md
+++ b/docs/advanced/extensions.md
@@ -74,9 +74,27 @@ New code can use context-free static operations from `software.amazon.lambda.dur
| `DurableWaitForCallbackOperation` | `waitForCallback`, `waitForCallbackAsync` |
| `DurableWaitForConditionOperation` | `waitForCondition`, `waitForConditionAsync` |
| `DurableWithRetryOperation` | `withRetry`, `withRetryAsync` |
+| `DurableReplaySafeValueOperation` | `uuid`, `now`, `random` and asynchronous variants |
The existing `DurableContext` instance methods and callback signatures remain supported for backward compatibility.
+### Replay-safe values
+
+`DurableReplaySafeValueOperation` provides common nondeterministic values through extension-backed STEP operations:
+
+```java
+import static software.amazon.lambda.durable.operation.DurableReplaySafeValueOperation.now;
+import static software.amazon.lambda.durable.operation.DurableReplaySafeValueOperation.random;
+import static software.amazon.lambda.durable.operation.DurableReplaySafeValueOperation.uuid;
+
+var requestId = uuid("request-id");
+var createdAt = now("created-at");
+var sample = random("sample");
+```
+
+Each value is generated once and checkpointed. Replays return the stored `UUID`, `Instant`, or `double` without
+regenerating it. The no-argument overloads use the operation names `uuid`, `now`, and `random`.
+
Each static operation owns its configuration type. For example:
```java
diff --git a/sdk-integration-tests/src/test/java/software/amazon/lambda/durable/ReplaySafeValueIntegrationTest.java b/sdk-integration-tests/src/test/java/software/amazon/lambda/durable/ReplaySafeValueIntegrationTest.java
new file mode 100644
index 000000000..f196bff6e
--- /dev/null
+++ b/sdk-integration-tests/src/test/java/software/amazon/lambda/durable/ReplaySafeValueIntegrationTest.java
@@ -0,0 +1,61 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package software.amazon.lambda.durable;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static software.amazon.lambda.durable.operation.DurableReplaySafeValueOperation.now;
+import static software.amazon.lambda.durable.operation.DurableReplaySafeValueOperation.random;
+import static software.amazon.lambda.durable.operation.DurableReplaySafeValueOperation.uuid;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.services.lambda.model.OperationType;
+import software.amazon.lambda.durable.model.ExecutionStatus;
+import software.amazon.lambda.durable.operation.DurableWaitOperation;
+import software.amazon.lambda.durable.testing.LocalDurableTestRunner;
+
+class ReplaySafeValueIntegrationTest {
+ @Test
+ void generatedValuesAreCheckpointedAndReusedAfterReplay() {
+ var handlerExecutions = new AtomicInteger();
+ var runner = LocalDurableTestRunner.create(String.class, (input, context) -> {
+ handlerExecutions.incrementAndGet();
+ var values = new ReplaySafeValues(uuid(), now(), random());
+ DurableWaitOperation.wait("force-replay", Duration.ofSeconds(1));
+ return values;
+ })
+ .withOutputType(ReplaySafeValues.class);
+
+ var result = runner.runUntilComplete("input");
+
+ assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
+ assertTrue(handlerExecutions.get() >= 2);
+ var values = result.getResult();
+ assertEquals(values.uuid(), result.getOperation("uuid").getStepResult(UUID.class));
+ assertEquals(values.now(), result.getOperation("now").getStepResult(Instant.class));
+ assertEquals(values.random(), result.getOperation("random").getStepResult(Double.class));
+ assertTrue(values.random() >= 0.0);
+ assertTrue(values.random() < 1.0);
+ assertStep(
+ result.getOperation("uuid").getType(),
+ result.getOperation("uuid").getSubtype(),
+ "UUID");
+ assertStep(
+ result.getOperation("now").getType(), result.getOperation("now").getSubtype(), "Now");
+ assertStep(
+ result.getOperation("random").getType(),
+ result.getOperation("random").getSubtype(),
+ "Random");
+ }
+
+ private static void assertStep(OperationType type, String actualSubtype, String expectedSubtype) {
+ assertEquals(OperationType.STEP, type);
+ assertEquals(expectedSubtype, actualSubtype);
+ }
+
+ record ReplaySafeValues(UUID uuid, Instant now, double random) {}
+}
diff --git a/sdk/src/main/java/software/amazon/lambda/durable/operation/DurableReplaySafeValueOperation.java b/sdk/src/main/java/software/amazon/lambda/durable/operation/DurableReplaySafeValueOperation.java
new file mode 100644
index 000000000..ba3b69f53
--- /dev/null
+++ b/sdk/src/main/java/software/amazon/lambda/durable/operation/DurableReplaySafeValueOperation.java
@@ -0,0 +1,119 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package software.amazon.lambda.durable.operation;
+
+import static software.amazon.lambda.durable.model.OperationSubType.STEP;
+
+import java.time.Instant;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Supplier;
+import software.amazon.lambda.durable.DurableFuture;
+import software.amazon.lambda.durable.TypeToken;
+import software.amazon.lambda.durable.extension.ExtensionContext;
+import software.amazon.lambda.durable.extension.ExtensionStepResult;
+import software.amazon.lambda.durable.util.ParameterValidator;
+
+/**
+ * Replay-safe helpers for common nondeterministic values.
+ *
+ *
Each helper checkpoints its generated value as a durable STEP. Replays return the checkpointed value without
+ * generating a new UUID, timestamp, or random number.
+ */
+public final class DurableReplaySafeValueOperation {
+ private static final String UUID_NAME = "uuid";
+ private static final String UUID_SUBTYPE = "UUID";
+ private static final String NOW_NAME = "now";
+ private static final String NOW_SUBTYPE = "Now";
+ private static final String RANDOM_NAME = "random";
+ private static final String RANDOM_SUBTYPE = "Random";
+
+ private DurableReplaySafeValueOperation() {}
+
+ /** Returns a checkpointed random UUID using the default operation name {@code uuid}. */
+ public static UUID uuid() {
+ return uuid(UUID_NAME);
+ }
+
+ /** Returns a checkpointed random UUID. */
+ public static UUID uuid(String name) {
+ return uuidAsync(name).get();
+ }
+
+ /** Returns a future for a checkpointed random UUID using the default operation name {@code uuid}. */
+ public static DurableFuture uuidAsync() {
+ return uuidAsync(UUID_NAME);
+ }
+
+ /** Returns a future for a checkpointed random UUID. */
+ public static DurableFuture uuidAsync(String name) {
+ return checkpointedValueAsync(name, UUID_SUBTYPE, UUID.class, UUID::randomUUID);
+ }
+
+ /** Returns a checkpointed current instant using the default operation name {@code now}. */
+ public static Instant now() {
+ return now(NOW_NAME);
+ }
+
+ /** Returns a checkpointed current instant. */
+ public static Instant now(String name) {
+ return nowAsync(name).get();
+ }
+
+ /** Returns a future for a checkpointed current instant using the default operation name {@code now}. */
+ public static DurableFuture nowAsync() {
+ return nowAsync(NOW_NAME);
+ }
+
+ /** Returns a future for a checkpointed current instant. */
+ public static DurableFuture nowAsync(String name) {
+ return checkpointedValueAsync(name, NOW_SUBTYPE, Instant.class, Instant::now);
+ }
+
+ /**
+ * Returns a checkpointed pseudorandom {@code double} between {@code 0.0} (inclusive) and {@code 1.0} (exclusive)
+ * using the default operation name {@code random}.
+ */
+ public static double random() {
+ return random(RANDOM_NAME);
+ }
+
+ /**
+ * Returns a checkpointed pseudorandom {@code double} between {@code 0.0} (inclusive) and {@code 1.0} (exclusive).
+ */
+ public static double random(String name) {
+ return randomAsync(name).get();
+ }
+
+ /**
+ * Returns a future for a checkpointed pseudorandom {@code double} between {@code 0.0} (inclusive) and {@code 1.0}
+ * (exclusive) using the default operation name {@code random}.
+ */
+ public static DurableFuture randomAsync() {
+ return randomAsync(RANDOM_NAME);
+ }
+
+ /**
+ * Returns a future for a checkpointed pseudorandom {@code double} between {@code 0.0} (inclusive) and {@code 1.0}
+ * (exclusive).
+ */
+ public static DurableFuture randomAsync(String name) {
+ return checkpointedValueAsync(name, RANDOM_SUBTYPE, Double.class, () -> ThreadLocalRandom.current()
+ .nextDouble());
+ }
+
+ private static DurableFuture checkpointedValueAsync(
+ String name, String subType, Class resultType, Supplier supplier) {
+ ParameterValidator.validateOperationName(name);
+ var context = ExtensionContext.getCurrentContext();
+ var result = context.reserve(name)
+ .stepAsync(
+ subType,
+ TypeToken.get(resultType),
+ ignored -> CompletableFuture.completedFuture(ExtensionStepResult.succeed(supplier.get())),
+ DurableStepOperation.extensionConfig(
+ DurableStepOperation.StepConfig.builder().build()));
+ return CompletionStageDurableFuture.from(context, STEP.name(), name, result);
+ }
+}
diff --git a/sdk/src/test/java/software/amazon/lambda/durable/DurableOperationFacadeTest.java b/sdk/src/test/java/software/amazon/lambda/durable/DurableOperationFacadeTest.java
index 4d2cd4319..0b1b5351d 100644
--- a/sdk/src/test/java/software/amazon/lambda/durable/DurableOperationFacadeTest.java
+++ b/sdk/src/test/java/software/amazon/lambda/durable/DurableOperationFacadeTest.java
@@ -71,6 +71,7 @@ void operationFacadesUseSingularClassNames() {
assertFacadeRenamed("DurableWaitForCallbackOperation", "DurableWaitForCallbackOperations");
assertFacadeRenamed("DurableWaitForConditionOperation", "DurableWaitForConditionOperations");
assertFacadeRenamed("DurableWithRetryOperation", "DurableWithRetryOperations");
+ assertFacadeRenamed("DurableReplaySafeValueOperation", "DurableReplaySafeValueOperations");
}
@Test
diff --git a/sdk/src/test/java/software/amazon/lambda/durable/operation/DurableReplaySafeValueOperationTest.java b/sdk/src/test/java/software/amazon/lambda/durable/operation/DurableReplaySafeValueOperationTest.java
new file mode 100644
index 000000000..b2ae4cdba
--- /dev/null
+++ b/sdk/src/test/java/software/amazon/lambda/durable/operation/DurableReplaySafeValueOperationTest.java
@@ -0,0 +1,124 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package software.amazon.lambda.durable.operation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Answers.CALLS_REAL_METHODS;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import java.time.Instant;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import software.amazon.lambda.durable.DurableContext;
+import software.amazon.lambda.durable.TypeToken;
+import software.amazon.lambda.durable.context.BaseContextImpl;
+import software.amazon.lambda.durable.extension.ExtensionContext;
+import software.amazon.lambda.durable.extension.ExtensionOperation;
+import software.amazon.lambda.durable.extension.ExtensionStepConfig;
+import software.amazon.lambda.durable.extension.ExtensionStepFunction;
+import software.amazon.lambda.durable.extension.ExtensionStepResult;
+
+class DurableReplaySafeValueOperationTest {
+ @AfterEach
+ void clearContext() {
+ BaseContextImpl.setCurrentContext(null);
+ }
+
+ @Test
+ void defaultHelpersUseExtensionStepsWithDescriptiveNamesAndSubtypes() {
+ var context = mockDurableContext();
+ var uuidReservation = mock(ExtensionOperation.class);
+ var nowReservation = mock(ExtensionOperation.class);
+ var randomReservation = mock(ExtensionOperation.class);
+ var uuid = UUID.fromString("12345678-1234-5678-1234-567812345678");
+ var now = Instant.parse("2026-09-01T12:00:00Z");
+ BaseContextImpl.setCurrentContext(context);
+ when(context.reserve("uuid")).thenReturn(uuidReservation);
+ when(context.reserve("now")).thenReturn(nowReservation);
+ when(context.reserve("random")).thenReturn(randomReservation);
+ when(uuidReservation.stepAsync(
+ eq("UUID"), eq(TypeToken.get(UUID.class)), any(ExtensionStepFunction.class), any()))
+ .thenReturn(CompletableFuture.completedFuture(uuid));
+ when(nowReservation.stepAsync(
+ eq("Now"), eq(TypeToken.get(Instant.class)), any(ExtensionStepFunction.class), any()))
+ .thenReturn(CompletableFuture.completedFuture(now));
+ when(randomReservation.stepAsync(
+ eq("Random"), eq(TypeToken.get(Double.class)), any(ExtensionStepFunction.class), any()))
+ .thenReturn(CompletableFuture.completedFuture(0.25));
+
+ assertEquals(uuid, DurableReplaySafeValueOperation.uuid());
+ assertEquals(now, DurableReplaySafeValueOperation.now());
+ assertEquals(0.25, DurableReplaySafeValueOperation.random());
+
+ verify(uuidReservation)
+ .stepAsync(eq("UUID"), eq(TypeToken.get(UUID.class)), any(ExtensionStepFunction.class), any());
+ verify(nowReservation)
+ .stepAsync(eq("Now"), eq(TypeToken.get(Instant.class)), any(ExtensionStepFunction.class), any());
+ verify(randomReservation)
+ .stepAsync(eq("Random"), eq(TypeToken.get(Double.class)), any(ExtensionStepFunction.class), any());
+ }
+
+ @Test
+ void namedAsyncHelpersGenerateExpectedValueTypesAndUseDefaultStepRetries() {
+ var context = mockDurableContext();
+ BaseContextImpl.setCurrentContext(context);
+
+ assertGeneratedValue(context, "request-id", "UUID", UUID.class, UUID.class);
+ assertGeneratedValue(context, "created-at", "Now", Instant.class, Instant.class);
+ var random = assertGeneratedValue(context, "sample", "Random", Double.class, Double.class);
+ assertTrue((Double) random >= 0.0);
+ assertTrue((Double) random < 1.0);
+ }
+
+ @Test
+ void helpersRequireAnActiveDurableContext() {
+ assertThrows(IllegalStateException.class, DurableReplaySafeValueOperation::uuid);
+ assertThrows(IllegalStateException.class, DurableReplaySafeValueOperation::now);
+ assertThrows(IllegalStateException.class, DurableReplaySafeValueOperation::random);
+ }
+
+ private Object assertGeneratedValue(
+ ExtensionContext context, String name, String subType, Class resultType, Class> expectedValueType) {
+ var reservation = mock(ExtensionOperation.class);
+ when(context.reserve(name)).thenReturn(reservation);
+ when(reservation.stepAsync(eq(subType), eq(TypeToken.get(resultType)), any(ExtensionStepFunction.class), any()))
+ .thenReturn(new CompletableFuture<>());
+
+ switch (subType) {
+ case "UUID" -> DurableReplaySafeValueOperation.uuidAsync(name);
+ case "Now" -> DurableReplaySafeValueOperation.nowAsync(name);
+ case "Random" -> DurableReplaySafeValueOperation.randomAsync(name);
+ default -> throw new AssertionError("Unexpected subtype: " + subType);
+ }
+
+ @SuppressWarnings("unchecked")
+ var function = (ArgumentCaptor>)
+ (ArgumentCaptor>) ArgumentCaptor.forClass(ExtensionStepFunction.class);
+ var config = ArgumentCaptor.forClass(ExtensionStepConfig.class);
+ verify(reservation).stepAsync(eq(subType), eq(TypeToken.get(resultType)), function.capture(), config.capture());
+ assertNotNull(config.getValue().retryStrategy());
+
+ var outcome = assertInstanceOf(
+ ExtensionStepResult.Succeeded.class,
+ function.getValue().apply(null).toCompletableFuture().join());
+ return assertInstanceOf(expectedValueType, outcome.value());
+ }
+
+ private ExtensionContext mockDurableContext() {
+ return (ExtensionContext) mock(
+ DurableContext.class,
+ withSettings().extraInterfaces(ExtensionContext.class).defaultAnswer(CALLS_REAL_METHODS));
+ }
+}