Skip to content

WHILE condition

Repeats the ops nested underneath it for as long as a set of comparisons against the stash holds true. The comparisons are checked before every pass, including the first, so a condition that is already false means the body never runs at all.

Use it to poll: wait for a remote job to report a finished state, drain a queue of items one at a time, keep asking for a lock until you get it. When you want the body to run at least once before the first check, use DO-WHILE condition instead. When you want a single pass with no repetition, use IF var condition THEN, which has the identical condition builder.

check the rows When: all / any / none true run the nested ops the body must move the value the rows read false on to the next op

Fields

When sits at the top. Under it, each comparison gets its own box titled Condition holding the four fields below plus its own Remove button. A new op opens with one Condition box already there and When blank.

When

Decides how the condition rows combine into the single answer that drives the loop.

When The loop keeps going while
All every row is true
Any at least one row is true
None no row is true

None is the opposite of Any, not the opposite of All. One true row ends the loop.

Leave When unset and the answer is false whatever the rows say, so the body never runs and the rule walks straight past. Nothing warns you about it. Pick a value even when you only have one row.

Rows stop being evaluated as soon as one of them settles the answer, but they are not necessarily read in the order the form lists them. Do not write a row that only makes sense after an earlier row has passed. Put that guard in an enclosing IF var condition THEN instead.

Stash Variable

The left side of the comparison, and the value that has to change for the loop to end. Write the name on its own, with no ${} wrapper: last_state, not ${last_state}.

Dots walk into nested data, so topic.status reads a field out of a loaded topic. List positions take brackets: bar_items[0].name, not bar_items.0.name. A path that does not resolve produces an empty value rather than an error, which is why IS EMPTY and NOT EMPTY are the safe way to ask whether something arrived at all.

The form will not let you save a row with this field blank.

Operator

Picks the comparison, listed in full below. Your choice reshapes the rest of the row. IS TRUE, IS FALSE, IS EMPTY and NOT EMPTY hide the value field because they only look at the left side, and the checkboxes appear only for the operators that honour them.

Value

The right side of the comparison. This field does expand ${var} placeholders against the stash, so ${expected_state} and release/${version} both work. The asymmetry trips people up: the left field is a path, the right field is a template.

Expansion happens again on every pass, not once when the loop starts, so a placeholder here tracks the stash the same way the left side does. Either side can be the one the body moves.

For IN and NOT IN, type a comma-separated list. For LIKE (regular expression) and NOT LIKE (regular expression), type a regular expression.

Options

Two checkboxes, shown only for the operators that accept them.

Ignore case lowercases both sides before comparing. On LIKE and NOT LIKE it makes the pattern itself case-insensitive.

Numeric switches from text ordering to number ordering. As text, 9 sorts after 10 and 007 is not 7, so tick it for counters, attempt numbers, sizes, anything you are counting down to zero.

Add condition and Remove

Add condition appends another row. Remove deletes the row it sits in. Every row answers to the same When setting; rows have no weight and no nesting of their own. For mixed and/or logic, nest a second loop or move the test into IF condition THEN.

Operators

Operator True when Value field Options
IS TRUE the variable holds a non-empty, non-zero value; for a list, any element does hidden none
IS FALSE nothing in the variable is truthy hidden none
IS EMPTY the variable is unset, an empty string, an empty list or an empty hash hidden none
NOT EMPTY the variable holds something hidden none
EQUALS = the two sides match exactly text case, numeric
NOT EQUALS != the sides differ text case, numeric
GREATER THAN >, GREATER THAN OR EQUALS TO >=, LESS THAN <, LESS THAN OR EQUALS TO <= the ordering holds text case, numeric
LIKE (regular expression) the expression matches somewhere in the value regex case
NOT LIKE (regular expression) the expression does not match regex case
IN the variable equals one of the comma-separated entries list case
NOT IN the variable matches none of them list case
HAS the variable is a list or hash and the value is one of its entries text case
NOT HAS that list does not contain the value text case

IN and HAS get swapped constantly. IN asks whether one stash value appears in a list you typed into the form. HAS asks whether a value you typed appears in a list sitting in the stash. Draining a list of pending items is HAS or NOT EMPTY, never IN.

IS EMPTY on a list is true as soon as one element is empty, not only when the whole list is. Polling a list that can contain a blank entry ends the loop earlier than you meant. Compare a count instead when that is possible.

An operator the server does not recognise stops the rule with an error instead of evaluating false. The form can only produce the operators above, so this reaches you through a hand-edited rule.

Ending the loop

Nothing here advances anything on your behalf. The body has to change what the rows read, and it has to change it in the stash the loop can see. An op that writes inside STASH LOCAL is invisible to the next check, and the loop spins forever.

There is no iteration cap and no default time limit. A loop whose body never moves the tested value runs until the job is cancelled. Two things bound it:

  • Timeout on the op's Options tab. It arms a clock before the first check and aborts with a timeout error when the seconds run out. The clock covers the whole loop, not a single pass, so a poll expected to take ten passes of five seconds needs a value above fifty, not above five.
  • A counter of your own. Increment a stash variable in the body and add a second row that stops the loop past a limit.

Timeout is weaker than it looks here. Expanding a ${} placeholder anywhere disarms that clock as a side effect, and every service op expands its own configuration before it runs, so a body with any service op in it loses the timeout on the first pass. The counter row is what actually bounds a loop that does real work.

A nested op that fails ends the loop and the job. WHILE does not catch errors. Wrap the body in TRY statement with CATCH statement if a failed pass should be survivable.

The loop writes nothing of its own into the stash, and nothing in the job log marks a pass, so a long poll shows only whatever the body logs. Add LOG Message in the body when you want to see progress. Return Key on the Options tab has no effect on this op.

Combining with other ops

Pair the loop with something that refreshes the value it tests. A service that reloads a record, a Server CODE block that recomputes it, or SET VAR after a check.

Sleep for a number of seconds inside the body keeps a polling loop from hammering whatever it is asking. Without it the loop runs flat out.

Use PUSH VAR to build a list and a NOT EMPTY row to drain it, or skip the loop entirely and use FOREACH CI when you already have the whole list up front.

RETRY is the better choice when you are repeating because something failed, rather than because a value has not changed yet.

Examples

Poll a remote deployment until it reaches a settled state, with a guard against the states that mean it will never settle.

When: All
  last_state   NOT EQUALS   READY
  last_state   NOT EQUALS   ERROR
  last_state   NOT EQUALS   CANCELED
  -> Sleep for a number of seconds   10
  -> Server CODE                     refresh last_state

Keep going while a piece of work is still outstanding, with the body removing one item per pass.

When: Any
  pending_items   NOT EMPTY

Count down a budget of passes, using a numeric comparison so 10 sorts above 9.

When: All
  attempts_left   GREATER THAN   0   (Numeric)