#[non_exhaustive]pub struct LoopConfig {
pub counter: Option<String>,
pub init: i64,
pub increment: i64,
pub max: i64,
pub setup: Vec<Task>,
pub over: Option<Value>,
pub item: Option<String>,
pub scratch: Option<String>,
/* private fields */
}Expand description
Engine-managed for loop over a workflow’s task list.
A workflow carrying a loop runs its task list repeatedly — one sweep
per iteration — rather than once. Per sweep the engine writes the counter
into temp_data (when counter names it), checks
counter < max, then re-evaluates the workflow condition; the sweep runs
only if both hold. Afterwards the counter advances by increment.
The bound is half-open, matching Rollout: init: 0, max: n yields
counter values 0..n-1 — exactly array indices.
Reaching max is normal completion, never an error: max is always
author-supplied, so hitting it is the stated bound rather than a runaway.
To stop mid-body, use a filter task with on_reject: halt — that breaks
the whole loop, not just the current sweep.
§Iterating an array
setup, over, item (as)
and scratch turn the counter into a batch loop: setup
runs once, over is evaluated once after it, and each iteration sees
over[counter] in temp_data.<as> and a fresh {} in
temp_data.<scratch>. A loop carrying none of them behaves exactly as a
plain counter loop always has.
§Memory
Every sweep adds one audit entry per task, and while the message has
capture_changes on — the default — each entry holds a deep copy of the
old and new value of every write. Nothing is released until
process_message returns, so a loop’s memory grows with
sweeps × writes × value size: about 65 bytes per number written, so a body
writing one 10,000-number array per sweep holds roughly 6.5 GB by sweep
10,000. A host that does not read
AuditTrail::changes should build the
message with
MessageBuilder::capture_changes(false);
the entries are still recorded, without values. See the Loops guide,
“Memory in long loops”.
§Example
{
"id": "per_item",
"condition": {"<": [{"var": "temp_data.i"}, {"var": "temp_data.n"}]},
"loop": { "counter": "i", "max": 10000 },
"tasks": [ ... ]
}#[non_exhaustive]: construct through LoopConfig::bounded and assign
the public fields you need. The compiled fields are engine internals a
struct literal would otherwise have to name. Not PartialEq: setup holds
Tasks, which are not comparable, and neither is the compiled over.
Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.counter: Option<String>temp_data field the engine maintains as the induction variable —
"i" means temp_data.i, and dot-paths nest ("cursor.index" →
temp_data.cursor.index).
None still bounds the loop by max; the count is simply not exposed
to conditions or tasks. The engine tracks it either way, so the audit
trail carries it regardless.
The engine owns this field: it is rewritten before every sweep, so a body task writing the same path is overwritten at the next increment.
init: i64First counter value. Defaults to 0.
increment: i64Added to the counter after each sweep. Defaults to 1; must be >= 1,
so the counter strictly increases and the loop cannot stall.
max: i64Required upper bound — sweeps run while counter < max. There is no
default: an unbounded loop is never what the author meant, and the
bound is what makes termination structural rather than a property of
the condition being written correctly.
setup: Vec<Task>Steps run once, before the first iteration, in the normal step
grammar: groups are allowed, and terminal ends the workflow before
any iteration. Their audit entries carry no loop_counter, because
setup is not a sweep. Shares the step id namespace with the workflow’s
tasks.
The workflow condition gates setup: a false result skips setup and
the loop alike.
over: Option<Value>JSONLogic evaluated once, after setup. It must yield an array;
anything else, null included, is a workflow error naming
loop.over. An empty array runs zero iterations.
The counter indexes the array: iteration k holds over[k] in
temp_data.<as>, and the loop stops at max or at the array’s end,
whichever comes first. init is therefore a starting offset and
increment a stride, and init must be >= 0.
An explicit "over": null is kept as Some(Value::Null) rather than
read as absence, so it is refused like any other literal that can never
be an array instead of silently turning the loop into a counter loop.
item: Option<String>temp_data field holding the current element of Self::over. The
JSON key is as. Optional: without it the element is not exposed, but
the loop is still bounded by the array’s length. Requires over.
scratch: Option<String>temp_data field reset to {} at the start of every iteration,
before the condition, so per-item state cannot leak from one element
to the next. Left holding the last iteration’s state after the loop,
like the counter. Independent of over.
Implementations§
Source§impl LoopConfig
impl LoopConfig
Sourcepub fn bounded(max: i64) -> Self
pub fn bounded(max: i64) -> Self
A loop bounded at max, every other field at its default: counter
unnamed, init: 0, increment: 1, no setup, no over.
use dataflow_rs::engine::workflow::LoopConfig;
let mut config = LoopConfig::bounded(10);
config.counter = Some("i".to_string());
assert_eq!((config.init, config.increment, config.max), (0, 1, 10));