Wait for Condition¶
Wait on a condition with polling¶
The Wait for Condition operation polls a check function on a schedule until your code signals it to stop. Behind the scenes, the check condition runs as a durable step, and each polling attempt is a retry, so the SDK checkpoints results automatically and tracks state between attempts. The function suspends between attempts and does not consume Lambda execution time.
It manages the entire check-and-wait cycle for you, including state tracking, backoff, and attempt counting.
Use waitForCondition when you need to poll until a condition is met. For example, if
you need to poll an external system until something changes, like a batch job
completing, a resource becoming available, or a status reaching a terminal state.
If the external system will send a notification or response, use Callbacks instead to suspend the durable function until it receives the response.
Walkthrough¶
Here's a simple example that polls until a job completes:
export const handler = withDurableExecution(
async (event: any, context: DurableContext) => {
// Poll until job completes
const result = await context.waitForCondition(
"wait_for_job",
async (state: { jobId: string; status: string; done: boolean }) => {
const status = await getJobStatus(state.jobId);
return { ...state, status, done: status === "COMPLETED" };
},
{
initialState: { jobId: "job-123", status: "pending", done: false },
waitStrategy: (state, attempt) => {
if (state.done) {
return { shouldContinue: false };
}
return {
shouldContinue: true,
delay: { seconds: Math.min(5 * Math.pow(2, attempt), 300) },
};
},
},
);
return result;
},
);
from aws_durable_execution_sdk_python import DurableContext, durable_execution
from aws_durable_execution_sdk_python.waits import WaitForConditionConfig, WaitForConditionDecision
from aws_durable_execution_sdk_python.config import Duration
def check_job_status(state, check_context):
status = get_job_status(state["job_id"])
return {
"job_id": state["job_id"],
"status": status,
"done": status == "COMPLETED"
}
def wait_strategy(state, attempt):
if state["done"]:
return WaitForConditionDecision.stop_polling()
delay = min(5 * (2 ** attempt), 300)
return WaitForConditionDecision.continue_waiting(Duration.from_seconds(delay))
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
result = context.wait_for_condition(
check=check_job_status,
config=WaitForConditionConfig(
wait_strategy=wait_strategy,
initial_state={"job_id": "job-123", "done": False},
)
)
return result
import java.time.Duration;
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.TypeToken;
import software.amazon.lambda.durable.config.WaitForConditionConfig;
import software.amazon.lambda.durable.model.WaitForConditionResult;
import software.amazon.lambda.durable.retry.JitterStrategy;
import software.amazon.lambda.durable.retry.WaitStrategies;
public class WaitForConditionExample extends DurableHandler<Object, Map<String, Object>> {
@Override
public Map<String, Object> handleRequest(Object input, DurableContext context) {
var strategy = WaitStrategies.<Map<String, Object>>exponentialBackoff(
60, Duration.ofSeconds(5), Duration.ofMinutes(5), 2.0, JitterStrategy.NONE);
var initialState = Map.<String, Object>of(
"jobId", "job-123", "status", "pending", "done", false);
var config = WaitForConditionConfig.<Map<String, Object>>builder()
.initialState(initialState)
.waitStrategy(strategy)
.build();
return context.waitForCondition(
"wait_for_job",
new TypeToken<>() {},
(state, stepCtx) -> {
var status = getJobStatus((String) state.get("jobId"));
var updatedState = Map.<String, Object>of(
"jobId", state.get("jobId"),
"status", status,
"done", "COMPLETED".equals(status));
if (Boolean.TRUE.equals(updatedState.get("done"))) {
return WaitForConditionResult.stopPolling(updatedState);
}
return WaitForConditionResult.continuePolling(updatedState);
},
config);
}
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
public class WaitForConditionExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, JobState>(Workflow, input, context);
private async Task<JobState> Workflow(object input, IDurableContext ctx)
{
// Poll until the job completes.
var result = await ctx.WaitForConditionAsync(
check: async (state, checkCtx, ct) =>
{
var status = await GetJobStatus(state.JobId, ct);
return state with { Status = status, Done = status == "COMPLETED" };
},
config: new WaitForConditionConfig<JobState>
{
InitialState = new JobState("job-123", "pending", Done: false),
WaitStrategy = WaitStrategy.Exponential<JobState>(
maxAttempts: 60,
initialDelay: TimeSpan.FromSeconds(5),
maxDelay: TimeSpan.FromMinutes(5),
backoffRate: 2.0,
jitter: JitterStrategy.None,
isDone: state => state.Done),
},
name: "wait_for_job");
return result;
}
private static Task<string> GetJobStatus(string jobId, CancellationToken ct)
=> Task.FromResult("COMPLETED");
}
public record JobState(string JobId, string Status, bool Done);
Timing and order of operations¶
Each attempt runs the check function first, then applies the wait. The SDK runs the first check immediately, with no delay before it.
An attempt proceeds in this order:
- The SDK runs the check function with the current state. The first attempt receives
the
initialStateyou configured. Each later attempt receives the state the previous check returned. - The SDK evaluates whether the condition is met. If it is,
waitForConditioncheckpoints the final state and returns it. - If the condition is not met, the SDK checkpoints the delay for this attempt, suspends the function, and resumes for the next attempt once the delay elapses.
Where you express the stop-or-continue decision differs by language. See Custom strategy.
graph TD
A[Start with initial state] --> B["check (step)"]
B --> D{wait strategy}
D -->|Continue| E[Suspend for delay]
E -->|step retry| B
D -->|Stop| F[Return final state]
The attempt counter starts at 1 and increments by one on each new attempt. The wait strategy receives this number, so it can grow the delay or cap the total attempts.
Because the check runs before the wait, no delay precedes the first check. With the
strategy builder helper, initialDelay sets the wait after
the first unmet check, before the second attempt. Each later wait grows from there:
attempt n waits initialDelay * backoffRate^(n - 1), capped at maxDelay, with jitter
applied.
Method signature¶
// context.waitForCondition()
waitForCondition<T>(
name: string | undefined,
checkFunc: WaitForConditionCheckFunc<T>,
config: WaitForConditionConfig<T>,
): DurablePromise<T>;
waitForCondition<T>(
checkFunc: WaitForConditionCheckFunc<T>,
config: WaitForConditionConfig<T>,
): DurablePromise<T>;
// Check function
type WaitForConditionCheckFunc<T> = (
state: T,
context: WaitForConditionContext,
) => Promise<T>;
// WaitForConditionContext — provides a logger for use during checks
interface WaitForConditionContext {
logger: DurableLogger;
}
// Config
interface WaitForConditionConfig<T> {
waitStrategy: WaitForConditionWaitStrategyFunc<T>;
initialState: T;
serdes?: Serdes<T>;
}
# context.wait_for_condition()
def wait_for_condition(
self,
check: Callable[[T, WaitForConditionCheckContext], T],
config: WaitForConditionConfig[T],
name: str | None = None,
) -> T
# Check function
Callable[[T, WaitForConditionCheckContext], T]
# WaitForConditionCheckContext — provides a logger for use during checks
class WaitForConditionCheckContext:
logger: LoggerInterface
# Config
@dataclass
class WaitForConditionConfig(Generic[T]):
wait_strategy: Callable[[T, int], WaitForConditionDecision]
initial_state: T
serdes: SerDes | None = None
// context.waitForCondition() — sync
<T> T waitForCondition(
String name, Class<T> resultType,
BiFunction<T, StepContext, WaitForConditionResult<T>> checkFunc);
<T> T waitForCondition(
String name, Class<T> resultType,
BiFunction<T, StepContext, WaitForConditionResult<T>> checkFunc,
WaitForConditionConfig<T> config);
// context.waitForConditionAsync() — async
<T> DurableFuture<T> waitForConditionAsync(
String name, Class<T> resultType,
BiFunction<T, StepContext, WaitForConditionResult<T>> checkFunc,
WaitForConditionConfig<T> config);
// Check function — returns WaitForConditionResult with isDone flag
BiFunction<T, StepContext, WaitForConditionResult<T>>
// WaitForConditionResult
public record WaitForConditionResult<T>(T value, boolean isDone) {
public static <T> WaitForConditionResult<T> stopPolling(T value);
public static <T> WaitForConditionResult<T> continuePolling(T value);
}
// Config
WaitForConditionConfig.<T>builder()
.waitStrategy(strategy) // optional, defaults to exponential backoff
.initialState(initialState) // optional, defaults to null
.serDes(serDes) // optional
.build();
// ctx.WaitForConditionAsync()
Task<TState> WaitForConditionAsync<TState>(
Func<TState, IConditionCheckContext, CancellationToken, Task<TState>> check,
WaitForConditionConfig<TState> config,
string? name = null,
CancellationToken cancellationToken = default);
// Check function
Func<TState, IConditionCheckContext, CancellationToken, Task<TState>>
// IConditionCheckContext — provides a logger and the 1-based attempt number
public interface IConditionCheckContext
{
ILogger Logger { get; }
int AttemptNumber { get; }
}
// Config (required; generic on the state type)
public sealed class WaitForConditionConfig<TState>
{
public required TState InitialState { get; set; }
public required IWaitStrategy<TState> WaitStrategy { get; set; }
}
Parameters:
name(optional) - Only used for display, debugging and testing.check(required) - A function that performs a status check. The check function runs as a durable step and each subsequent polling attempt is a retry, so avoid heavy computation or side effects and keep it focused on querying status.config- A configuration object containing:initialState- The state object passed to the first check invocationwaitStrategy- A Wait strategy to control polling behavior
Returns: The final state object from the last check function invocation.
Wait strategies¶
The wait strategy controls how long to wait between polling attempts and when to stop polling. You can write your own custom strategy from scratch, or you can use the strategy builder helper to create a strategy for you based on configuration values you provide.
Wait strategy signature¶
# Wait strategy
Callable[[T, int], WaitForConditionDecision]
# Decision
@dataclass
class WaitForConditionDecision:
should_continue: bool
delay: Duration
@classmethod
def continue_waiting(cls, delay: Duration) -> WaitForConditionDecision: ...
@classmethod
def stop_polling(cls) -> WaitForConditionDecision: ...
// Wait strategy — decides, per poll, whether to continue and how long to wait
public interface IWaitStrategy<TState>
{
WaitDecision Decide(TState state, int attemptNumber);
}
// Decision
public readonly record struct WaitDecision
{
public bool ShouldContinue { get; }
public TimeSpan Delay { get; }
public static WaitDecision Stop(); // condition met
public static WaitDecision ContinueAfter(TimeSpan delay); // poll again after delay
}
Custom strategy¶
Write your own strategy function for full control over polling behavior. The function receives the current state and attempt number. The strategy function decides whether to continue polling or not.
from aws_durable_execution_sdk_python.waits import WaitForConditionDecision
from aws_durable_execution_sdk_python.config import Duration
def strategy(state, attempt):
if state["status"] == "COMPLETED":
return WaitForConditionDecision.stop_polling()
if attempt >= 10:
raise ValueError("Max attempts exceeded")
return WaitForConditionDecision.continue_waiting(Duration.from_seconds(attempt * 5))
In Java, the stop/continue decision lives in the check function (via
WaitForConditionResult). The strategy only computes the delay.
Implement IWaitStrategy<TState> or wrap a delegate with
WaitStrategy.FromDelegate. The strategy returns a WaitDecision:
WaitDecision.Stop() to stop polling or WaitDecision.ContinueAfter(delay)
to poll again.
using Amazon.Lambda.DurableExecution;
public record JobState(string Status);
public static class CustomWaitStrategy
{
// Full control over polling: the strategy decides whether to continue or
// stop, and how long to wait before the next attempt.
public static IWaitStrategy<JobState> Create() =>
WaitStrategy.FromDelegate<JobState>((state, attempt) =>
{
if (state.Status == "COMPLETED")
{
return WaitDecision.Stop();
}
if (attempt >= 10)
{
throw new WaitForConditionException("Max attempts exceeded");
}
return WaitDecision.ContinueAfter(TimeSpan.FromSeconds(attempt * 5));
});
}
To stop polling and signal an error, throw an exception from the strategy or the check function.
Strategy builder helper¶
Use the ready-made wait strategy helper to generate a strategy function from common parameters like backoff rate, max attempts, and jitter. This is a convenience so you can re-use common wait strategy logic without having to code your own function.
from aws_durable_execution_sdk_python.waits import create_wait_strategy, WaitStrategyConfig, JitterStrategy
from aws_durable_execution_sdk_python.config import Duration
strategy = create_wait_strategy(WaitStrategyConfig(
should_continue_polling=lambda state: state["status"] != "COMPLETED",
max_attempts=10,
initial_delay=Duration.from_seconds(5),
max_delay=Duration.from_minutes(5),
backoff_rate=2.0,
jitter_strategy=JitterStrategy.FULL,
))
import java.time.Duration;
import software.amazon.lambda.durable.retry.JitterStrategy;
import software.amazon.lambda.durable.retry.WaitStrategies;
var strategy = WaitStrategies.<Map<String, String>>exponentialBackoff(
10, // maxAttempts
Duration.ofSeconds(5), // initialDelay
Duration.ofMinutes(5), // maxDelay
2.0, // backoffRate
JitterStrategy.FULL); // jitter
using Amazon.Lambda.DurableExecution;
public record JobState(string Status);
public static class StrategyHelper
{
// Build an exponential-backoff strategy from common parameters. The
// isDone predicate stops polling once the latest state satisfies it.
public static IWaitStrategy<JobState> Create() =>
WaitStrategy.Exponential<JobState>(
maxAttempts: 10,
initialDelay: TimeSpan.FromSeconds(5),
maxDelay: TimeSpan.FromMinutes(5),
backoffRate: 2.0,
jitter: JitterStrategy.Full,
isDone: state => state.Status == "COMPLETED");
}
Helper parameters:
shouldContinuePolling- Predicate that returnstrueto keep polling orfalseto stopmaxAttempts- Maximum polling attempts. Set this to prevent runaway executions. Defaults to 60initialDelay- Delay before the first retry. Defaults to 5 secondsmaxDelay- Maximum delay between attempts. Defaults to 5 minutesbackoffRate- Multiplier applied to the delay after each attempt (e.g.,2.0doubles the delay). Defaults to 1.5jitter- Jitter strategy (FULL,HALF, orNONE). Defaults toFULL
Delays between attempts are approximate. The actual time depends on system scheduling, Lambda cold start time, and current system load.
See also¶
- Wait operations - Simple time-based delays
- Callbacks - Wait for external system responses
- Steps - Execute business logic with automatic checkpointing
- Getting Started - Learn the basics of durable functions
Checkpoint consumption
Durable operations consume checkpoints. To understand how this operation affects your checkpoint usage, see Checkpoint consumption.