Unmeshed Logo

Back to Blog

Handling Wait Steps Inside While Loops in Unmeshed

Learn how to configure the Wait step correctly when it’s nested inside a While loop, prevent accidental instant completions, and keep your long-running workflows predictable and robust.

4 min read
June 24, 2025

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 + 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
  };
}

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:

  1. Iteration 0 – Wait schedules start + 3 000 ms and pauses. ✅
  2. 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 re‑uses 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
  };
}

Why this works:

  • Idempotent & 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

ApproachWhen to use
Context‑based key (e.g., store waitUntil in processContext)Useful when multiple steps need to reference a shared schedule.
Dynamic While condition (move the timer logic into the While’s condition itself)Handy for simple polling loops where you can express “stop when 3 seconds have passed” directly.
Separate Wait step outside WhileIf you truly need only 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 info (e.g., the current iteration index) in the step output. It makes diagnosing timing issues much easier.
  • ❌ Wait inside a loop does not behaves like sleep() in synchronous code; remember that the orchestration engine re‑evaluates the same node instance each pass.

Happy orchestrating! 🛠️

Recent Blogs