Post

#PowerPlatformTip 49: 'Do-Until'

#PowerPlatformTip 49: 'Do-Until'

TL;DR: A Do Until loop checks its condition after every pass. Put the check action, a Set variable and a Delay inside the loop, let the condition read that variable, and keep the Count and Timeout limits as your safety net. When a limit stops the loop, the loop still reports Succeeded, so check the variable after the loop, not the loop status.

💡 Challenge

You need to wait for something that is not ready yet: a file that is still locked, a job that is still running, a record that has not changed its status. The naive approach is a fixed Delay and hope. Too short and the flow fails, too long and every run wastes minutes. A Do Until loop solves this, but only if you know three things: what the condition can read, what happens when the Count or Timeout limit is hit, and what a failing action inside the loop does to the loop.

✅ Solution

Do Until runs its actions first and evaluates the condition afterwards, so the body always runs at least once. Inside the loop you place the action that checks the status, a Set variable that stores the result, and a Delay. The condition compares the variable with the value you are waiting for, for example equals(variables('Status'), 'done'). The Count and Timeout parameters of the loop cap the number of passes and the total time. After the loop, one Condition on the variable decides between “it worked” and “it never happened”.

🔧 How It’s Done

The tested pattern, built with Compose, Variables and Delay only:

  1. Initialize two variables before the loop. 🔸 Attempt (Integer, 0) to count the passes.

    🔸 Status (String, pending) to hold the last result of the check.

  2. Add the “Do until” control and set its condition. 🔸 Switch the condition to advanced mode and enter equals(variables('Status'), 'done').

    🔸 The condition can also read an action inside the loop directly. equals(outputs('Check_status'), 'done') works and ended the loop after the same three passes in the test. The variable version is still the better choice, because it also lets you store an error result from a run-after branch (step 4).

  3. Put these actions inside the loop, in this order. 🔸 “Increment variable” on Attempt by 1.

    🔸 The check action, named Check_status. In the test this was a Compose with if(greaterOrEquals(variables('Attempt'), 3), 'done', 'pending'), which simulates a job that is done on the third look. In your flow this is your Get file, Get a row or HTTP call.

    🔸 “Set variable” Status to outputs('Check_status'), or to the status property of your real action.

    🔸 “Delay” with the interval you want between two looks (the test used 2 seconds). Without the Delay the loop fires its checks back to back: 60 passes ran in 15 seconds.

  4. Decide what a failed check should do. 🔸 An action that fails inside the loop does not stop the loop. In the test, Check_status failed on pass 2 and the loop simply continued with pass 3 and 4. The action after it, configured with run after “is successful”, was skipped in that pass only.

    🔸 To record the failure, add a second “Set variable” Status = error with run after set to “has failed” and “has timed out” on the check action. Give the Delay run after “is successful” on the first Set variable and “is successful” plus “is skipped” on the second, so every pass finishes the same way.

    🔸 The loop takes the status of its last pass. If the check fails on the final pass, the loop ends as Failed and the whole run fails with “An action failed. No dependent actions succeeded.” If it fails on an earlier pass and the last pass succeeds, the loop reports Succeeded.

    🔸 “Terminate” cannot be placed inside the loop. Saving the flow is refused with “The workflow run action ‘Terminate’ has type ‘Terminate’ that is not allowed to be nested under an action of type ‘until’.” Set the variable instead and terminate after the loop.

  5. Set the limits. 🔸 Count and Timeout sit in the loop’s own parameters, with the defaults Count 60 and Timeout PT1H. Timeout uses ISO 8601 duration format: PT8S, PT10M, PT1H. The field also accepts an expression: concat('PT', string(8), 'S') was saved and applied as an 8-second timeout.

    🔸 Count is a hard cap on passes. With the condition never true and Count 60, the loop ran exactly 60 passes and then stopped.

    🔸 Timeout is checked between passes. With Timeout PT8S and a 3-second Delay inside, the loop ran 3 passes and ended after about 10 seconds. The pass that is running when the timeout is reached still completes.

    🔸 In both cases the loop reports Succeeded. Actions after the loop with run after “has failed” or “has timed out” were skipped, the one with “is successful” ran. The limit is not an error from the loop’s point of view.

  6. Evaluate the result after the loop. 🔸 Add a Condition: variables('Status') is equal to done. The Yes branch continues your process. The No branch handles the case that the loop gave up (status still pending or error) - send a notification, terminate the run with status Failed, or both.

🎉 Result

The flow polls the status in fixed intervals, exits the moment the status matches, and never runs longer than the Count and Timeout limits allow. A failed check does not kill the loop. The variable after the loop tells you unambiguously whether the wait succeeded or hit a limit.

🌟 Key Advantages

🔸 Exits as soon as the condition is true: three passes in the test, then straight on to the next action.

🔸 Two independent safety nets (Count and Timeout) that cannot be forgotten, because the loop always has them.

🔸 A single variable after the loop replaces guesswork about the loop status.

🔸 Works with any action that returns a status, and the whole pattern needs no connector to test.

🛠️ FAQ

1. Does the loop fail when the Count or Timeout limit is reached?

No. In both tests the loop ended with status Succeeded, the action with run after “is successful” ran, and the actions with “has failed” or “has timed out” were skipped. That is why the Condition on the variable after the loop is mandatory.

2. What happens when an action inside the loop fails?

The loop continues with the next pass. Only the actions in that pass that depend on the failed action with run after “is successful” are skipped. The loop’s own status is the status of its last pass: a failure in the last pass makes the loop and the run fail, a failure in an earlier pass with a successful last pass leaves the loop at Succeeded.

3. Can the condition reference an action inside the loop?

Yes. equals(outputs('Check_status'), 'done') with Check_status inside the loop ended the loop after the same three passes as the variable version. The variable is still preferable, because the run-after branch for a failed check can write error into it.

4. Can I stop the loop early with Terminate?

No, the flow cannot be saved with Terminate inside a Do Until. Set the variable to the value the condition is waiting for (or to error) and let the condition end the loop, then handle the result after the loop.

5. Can I set the Timeout dynamically?

Yes. The Timeout parameter accepts an expression that returns an ISO 8601 duration string, for example concat('PT', string(variables('Seconds')), 'S'). An 8-second timeout set this way ended the loop after about 10 seconds with a 3-second Delay per pass.

🔸 #PowerPlatformTip 144: ‘Infinite Approvals’ - another loop-and-retry pattern, this time working around Power Automate’s 30-day approval timeout.

🔸 #PowerPlatformTip 36: ‘Static Result’ - mock the flaky action’s output while you test different retry and timeout settings.

🔸 #PowerPlatformTip 91: ‘If Function in Formulas’ - the same If()-based condition logic, applied to Power Apps formulas instead of a flow’s Do Until.

This tip is by Marcel Lehmann, Microsoft MVP (Business Applications) and founder of the Power Platform Academy – live Power Platform workshops, free or CHF 49. Join the workshop waitlist to get notified about new dates. For companies that want this implemented for them, see KMUpower – Power Platform consulting & automation for SMEs (DACH).

This post is licensed under CC BY 4.0 by the author.

Microsoft and the Power Platform product names (for example Power Apps, Power Automate, Copilot Studio) are trademarks of the Microsoft group of companies. This is an independent website. It is not operated, sponsored or endorsed by Microsoft.