Skip to content

While-Loop Iteration

The loop block enables conditional, unbounded iteration — making kdeps workflows Turing complete. Unlike items (which iterates over a fixed list), loop repeats a resource body while an optional expression is true (or for a fixed count when while: is omitted), with full access to mutable state via set()/get(). Add every: to turn the loop into a repeated scheduled task that pauses for a fixed duration between iterations.

Basic Usage

yaml
# resources/count.yaml

actionId: countToFive
name: Count to Five
loop:
  while: "loop.index() < 5"
  maxIterations: 1000   # safety cap (default: 1000)
after:
  - "{{ set('result', loop.count()) }}"
apiResponse:
  success: true
  response:
    count: "{{ get('result') }}"

loop.index() < 5 runs the body for index 0–4, producing 5 iterations. Each iteration's apiResponse becomes one element of the streaming response.

Loop Context

Inside the loop body, special callables are available:

CallableDescription
loop.index()Current iteration index (0-based)
loop.count()Current iteration count (1-based)
loop.results()Results accumulated from all prior iterations

Method Syntax

yaml
# resources/example.yaml
after:
  - "{{ set('idx', loop.index()) }}"
  - "{{ set('cnt', loop.count()) }}"
  - "{{ set('prev', loop.results()) }}"

Comparison: loop vs item

LoopItems equivalentDescription
loop.index()item.index()Current index (0-based)
loop.count()item.count()Current count (1-based)
loop.results()item.values()All prior results
set('key', val, 'loop')set('key', val, 'item')Loop-scoped storage
get('key', 'loop')get('key', 'item')Read loop-scoped value

Loop-Scoped Storage

Use 'loop' as a storage type hint to scope variables to the loop context, mirroring the 'item' type for items iteration:

yaml
# resources/example.yaml
loop:
  while: "default(get('step', 'loop'), 0) < 3"
  maxIterations: 10
after:
  - "{{ set('step', loop.count(), 'loop') }}"

loop.results() — Self-Referential Termination

loop.results() returns a slice of all results from previous iterations. This enables patterns where the termination condition depends on what the loop has already produced:

yaml
# resources/example.yaml
loop:
  while: "len(loop.results()) < 3"
  maxIterations: 10
after:
  - "{{ set('n', loop.count()) }}"

The loop runs until 3 results have been collected, regardless of how many iterations that takes — a key pattern for mu-recursion and unbounded search.

Streaming Response

When apiResponse is present, every iteration produces one response map. Multiple per-iteration responses constitute a streaming response — a slice returned to the caller. This mirrors how items with apiResponse works.

yaml
# resources/example.yaml
loop:
  while: "loop.index() < 3"
  maxIterations: 10
after:
  - "{{ set('tick', loop.count()) }}"
apiResponse:
  success: true
  response:
    tick: "{{ get('tick') }}"

Three iterations → three apiResponse maps → streaming slice of length 3.

No iterations → empty slice.

maxIterations Safety Cap

maxIterations is a configurable upper bound on the number of iterations. It prevents accidental infinite loops in production while preserving Turing completeness — users can set it to any positive integer.

  • Default: 1000
  • Set to any positive integer for tighter or looser control
  • Turing completeness is preserved because the cap is configurable, not fixed
yaml
# resources/example.yaml
loop:
  while: "true"
  maxIterations: 50000   # allow up to 50k iterations

every: — Repeated Scheduled Tasks

Add every: to pause the loop for a fixed duration between iterations, turning it into a repeated scheduled task (ticker pattern). Supported units: ms (milliseconds), s (seconds), m (minutes), h (hours).

yaml
# resources/example.yaml
loop:
  while: "loop.index() < 10"
  every: "5s"           # wait 5 seconds between each iteration
  maxIterations: 100
after:
  - "{{ set('tick', loop.count()) }}"
apiResponse:
  success: true
  response:
    tick: "{{ get('tick') }}"
    at:   "{{ loop.count() }}"

The sleep is skipped after the last iteration — the caller receives results without an unnecessary trailing delay.

Combining while: "true" with every: for infinite polling

yaml
# resources/example.yaml
loop:
  while: "true"          # run until maxIterations
  every: "30s"           # poll every 30 seconds
  maxIterations: 1440    # up to 12 hours (1440 × 30 s)
exec:
  command: "poll-service.sh"
after:
  - "{{ get('execResource').exitCode == 0 ? set('done', true) : set('noop', 0) }}"

Duration format

ExampleMeaning
"500ms"500 milliseconds
"1s"1 second
"2m"2 minutes
"1h"1 hour

An invalid every: value (e.g. "not-a-duration") is rejected at validation time.

at: — Specific Dates and Times

Use at: to fire the loop body at a list of specific dates and/or times, in order. The engine sleeps until each scheduled time before executing the body for that iteration.

every: and at: are mutually exclusive — set only one per loop block.

yaml
# resources/example.yaml
loop:
  while: "loop.index() < 2"
  maxIterations: 10
  at:
    - "2026-03-15T10:00:00Z"   # RFC3339 absolute timestamp
    - "2026-03-15T14:30:00Z"
after:
  - "{{ set('tick', loop.count()) }}"
apiResponse:
  success: true
  response:
    tick: "{{ get('tick') }}"

Supported date/time formats

FormatExampleBehaviour
RFC3339 (UTC)"2026-03-15T10:00:00Z"Fire at that exact instant
RFC3339 (offset)"2026-03-15T10:00:00+02:00"Fire at that exact instant
Local datetime"2026-03-15T10:00:00"Treated as local time
Time of day"10:00" or "10:00:00"Next occurrence of that time today; tomorrow if already past
Date only"2026-03-15"Midnight (00:00:00) of that date, local time

Daily recurring example

yaml
# resources/example.yaml
loop:
  while: "true"
  maxIterations: 30  # run for 30 days
  at:
    - "08:00"   # fire at 08:00 every morning (next occurrence)
    - "20:00"   # fire at 20:00 every evening
exec:
  command: "daily-report.sh"

If a time entry is already in the past the engine fires immediately (no sleep).
An invalid entry (e.g. "not-a-date") causes an error before any iterations run.

Condition Syntax

The while: field is optional. When omitted, the loop runs until maxIterations (default 1000) or until all at: entries are consumed.

When provided, the expression is evaluated using expr-lang — any boolean expression is valid:

yaml
# Counter
while: "loop.index() < 10"

# Data-driven termination
while: "get('done') == nil"

# Prior-results driven
while: "len(loop.results()) < 5"

# Mutable state
while: "int(default(get('phase'), 0)) < 3"

# Mathematical search (mu-recursion)
while: "int(loop.count()) * int(loop.count() + 1) / 2 <= 20"

The while field is a plain expr-lang boolean expression string.

Turing Completeness

The three primitives of Turing completeness are:

PrimitiveHow kdeps provides it
Unbounded iterationloop.while with configurable maxIterations
Mutable stateset() / get() across iterations
Conditional branchingArbitrary boolean while expression + validations.skip

Together with loop.results() feeding back into the while condition, the system can simulate any computable function — including mu-recursion (search until an unpredictable condition is met).

Examples

Accumulator (sum 1+2+3+4 = 10)

yaml
# resources/example.yaml
loop:
  while: "loop.index() < 4"
  maxIterations: 100
after:
  - "{{ set('sum', int(default(get('sum'), 0)) + loop.count()) }}"
apiResponse:
  success: true
  response:
    partial_sum: "{{ get('sum') }}"

State-Machine Phase Transition

yaml
# resources/example.yaml
loop:
  while: "int(default(get('phase'), 0)) < 3"
  maxIterations: 10
after:
  - "{{ set('phase', int(default(get('phase'), 0)) + 1) }}"
apiResponse:
  success: true
  response:
    phase: "{{ get('phase') }}"

Conditional Early Exit (flag-based)

yaml
# resources/example.yaml
loop:
  while: "get('done') == nil"
  maxIterations: 100
exec:
  command: "check-condition.sh"
after:
  - "{{ get('execResource').exitCode == 0 ? set('done', true) : set('noop', 0) }}"
apiResponse:
  success: true
  response:
    iterations: "{{ loop.count() }}"

Collect N Results

yaml
# resources/example.yaml
loop:
  while: "len(loop.results()) < 5"
  maxIterations: 50
chat:
  prompt: "Generate item {{ loop.count() }}"
apiResponse:
  success: true
  response:
    item: "{{ get('chatResource') }}"

Downstream Resource Reads Loop Output

A resource that runs a loop and a downstream resource that reads the final state:

yaml
# resources/compute.yaml
actionId: compute
loop:
  while: "loop.index() < 3"
  maxIterations: 10
after:
  - "{{ set('computed', loop.count()) }}"

---
# resources/respond.yaml
actionId: respond
requires: [compute]
apiResponse:
  success: true
  response:
    value: "{{ get('computed') }}"   # reads final value set by the loop

When to Use loop vs items

Use loop when…Use items when…
Number of iterations is not known in advanceYou have a fixed list to process
Termination depends on runtime stateYou want to iterate over a pre-computed array
You need mutable accumulation across iterationsEach item is independent
Implementing search / retry / polling patternsBatch processing of a dataset
Running a repeated scheduled task (every:)One-shot batch over a dataset
Firing at specific dates or times (at:)Batch processing of a dataset

See Also

Released under the Apache 2.0 License.