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
apiVersion: kdeps.io/v1
kind: Resource
metadata:
actionId: countToFive
name: Count to Five
run:
loop:
while: "loop.index() < 5"
maxIterations: 1000 # safety cap (default: 1000)
expr:
- "{{ 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:
| Callable | Description |
|---|---|
loop.index() | Current iteration index (0-based) |
loop.count() | Current iteration count (1-based) |
loop.results() | Results accumulated from all prior iterations |
Method Syntax
run:
expr:
- "{{ set('idx', loop.index()) }}"
- "{{ set('cnt', loop.count()) }}"
- "{{ set('prev', loop.results()) }}"Comparison: loop vs item
| Loop | Items equivalent | Description |
|---|---|---|
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:
run:
loop:
while: "default(get('step', 'loop'), 0) < 3"
maxIterations: 10
expr:
- "{{ 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:
run:
loop:
while: "len(loop.results()) < 3"
maxIterations: 10
expr:
- "{{ 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.
run:
loop:
while: "loop.index() < 3"
maxIterations: 10
expr:
- "{{ 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
run:
loop:
while: "true"
maxIterations: 50000 # allow up to 50k iterationsevery: — 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).
run:
loop:
while: "loop.index() < 10"
every: "5s" # wait 5 seconds between each iteration
maxIterations: 100
expr:
- "{{ 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
run:
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"
expr:
- "{{ get('execResource').exitCode == 0 ? set('done', true) : set('noop', 0) }}"Duration format
| Example | Meaning |
|---|---|
"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.
run:
loop:
while: "loop.index() < 2"
maxIterations: 10
at:
- "2026-03-15T10:00:00Z" # RFC3339 absolute timestamp
- "2026-03-15T14:30:00Z"
expr:
- "{{ set('tick', loop.count()) }}"
apiResponse:
success: true
response:
tick: "{{ get('tick') }}"Supported date/time formats
| Format | Example | Behaviour |
|---|---|---|
| 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
run:
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:
# 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:
| Primitive | How kdeps provides it |
|---|---|
| Unbounded iteration | loop.while with configurable maxIterations |
| Mutable state | set() / get() across iterations |
| Conditional branching | Arbitrary 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)
run:
loop:
while: "loop.index() < 4"
maxIterations: 100
expr:
- "{{ set('sum', int(default(get('sum'), 0)) + loop.count()) }}"
apiResponse:
success: true
response:
partial_sum: "{{ get('sum') }}"State-Machine Phase Transition
run:
loop:
while: "int(default(get('phase'), 0)) < 3"
maxIterations: 10
expr:
- "{{ set('phase', int(default(get('phase'), 0)) + 1) }}"
apiResponse:
success: true
response:
phase: "{{ get('phase') }}"Conditional Early Exit (flag-based)
run:
loop:
while: "get('done') == nil"
maxIterations: 100
exec:
command: "check-condition.sh"
expr:
- "{{ get('execResource').exitCode == 0 ? set('done', true) : set('noop', 0) }}"
apiResponse:
success: true
response:
iterations: "{{ loop.count() }}"Collect N Results
run:
loop:
while: "len(loop.results()) < 5"
maxIterations: 50
chat:
model: llama3.2:1b
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:
# resources/compute.yaml
metadata:
actionId: compute
run:
loop:
while: "loop.index() < 3"
maxIterations: 10
expr:
- "{{ set('computed', loop.count()) }}"
---
# resources/respond.yaml
metadata:
actionId: respond
requires: [compute]
run:
apiResponse:
success: true
response:
value: "{{ get('computed') }}" # reads final value set by the loopWhen to Use loop vs items
Use loop when… | Use items when… |
|---|---|
| Number of iterations is not known in advance | You have a fixed list to process |
| Termination depends on runtime state | You want to iterate over a pre-computed array |
| You need mutable accumulation across iterations | Each item is independent |
| Implementing search / retry / polling patterns | Batch 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 |
Next Steps
- Items Iteration — Fixed-list iteration
- Expressions — Expression syntax
- Expression Functions Reference — Complete function list
- Validation and Control Flow — Skip conditions, preflight checks