DEV Community

Cover image for Why Wait Steps Behave Differently Inside While Loops
The Unmeshed Team
The Unmeshed Team

Posted on Originally published at unmeshed.io

Why Wait Steps Behave Differently Inside While Loops

Using Wait Steps in Unmeshed: Handling Delays Inside While Loops

Waiting—whether for a webhook, a human approval, or simply a fixed pause—shows up in almost every orchestration scenario. In Unmeshed, you model these pauses with the Wait step, which accepts a JavaScript function that returns a Unix epoch timestamp (waitUntil) indicating when the step should complete.

Most of the time that's all you need. But as soon as you nest a Wait step inside a While loop, an easy-to-miss nuance can trip you up and make the wait look "broken." This post explains that behavior and shows a clean pattern for making loops and waits play nicely together.

Recap: How a Wait Step Works

When the Wait step starts, Unmeshed passes the step's execution context to your JavaScript function. A common implementation adds a fixed offset to the step's start time:

(steps, context) => {
  const startDate = new Date(steps.__self.start); // epoch millis when the step first began

  return {
    waitUntil: startDate.getTime() + 3_000, // wait 3 seconds
  };
};
Enter fullscreen mode Exit fullscreen mode

steps.__self.start never changes for that step instance, so the first time through everything is perfect: the engine sleeps for three seconds and continues.

The Gotcha: Wait Inside While

A While step re-executes its inner graph as long as its condition remains true. Each inner step (including Wait) keeps its original start timestamp across iterations. That means:

  • Iteration 0 – Wait schedules start + 3,000 ms and pauses. ✅
  • Iteration 1 – The same Wait step runs again, but start is still the original value. Because that timestamp is already in the past, Unmeshed sees that waitUntil is satisfied and continues immediately. ⚠️

From the outside it looks like the engine ignored your wait—but it's really doing exactly what you asked.

A Robust Pattern: Per-Iteration Timestamps

The fix is to track a separate target time for every loop iteration. The snippet below pushes a new timestamp into an iterations array the first time each iteration executes, then reuses it on retries, restarts, or subsequent checks.

(steps, context) => {
  // Existing array (or []) pulled from the step's previous output
  const iterations = steps.__self.output.result?.iterations || [];
  const currentIter = steps.loop.output.iteration;

  // If we haven't stored a target time for this iteration yet, compute & save one
  if (iterations.length <= currentIter) {
    iterations.push(Date.now() + 3_000); // wait 3 seconds from *now*
  }

  return {
    waitUntil: iterations[currentIter],
    iterations, // persist the array for next pass
    loop: currentIter, // optional: surface the index for debugging
  };
};
Enter fullscreen mode Exit fullscreen mode

Why This Works

  • Idempotent and restart-safe. If the engine restarts mid-wait, the stored timestamp is still there.
  • True per-iteration delay. Each loop cycle gets a fresh waitUntil computed relative to when that cycle began—not when the Wait step was first created.

Alternative Approaches

Approach When to Use
Context-based key (for example, storing waitUntil in processContext) Useful when multiple steps need to reference a shared schedule.
Dynamic While condition (moving the timer logic into the While condition itself) Handy for simple polling loops where you can express "stop when 3 seconds have passed" directly.
Separate Wait step outside While Best when you only need one delay before the loop begins.

Best Practices Checklist

  • ✅ Treat Wait steps like any other stateful computation—make them idempotent and restart-safe.
  • ✅ When looping, compute targets relative to now, not to the original start time.
  • ✅ Always surface debugging information (such as the current iteration index) in the step output. It makes diagnosing timing issues much easier.
  • ❌ A Wait step inside a loop does not behave like sleep() in synchronous code. Remember that the orchestration engine re-evaluates the same node instance on every iteration.

Top comments (0)