Loops
Iterate over data, repeat numeric ranges, and poll until conditions are met.
Loop modifiers
Dokkimi provides three loop types: forEach iterates over arrays, for counts through numeric ranges, and repeat runs a fixed number of times with optional early exit. Each can be added at the test, step, action, assertion-block, or UI sub-step level.
forEach
Iterate over an array of items. Each iteration binds the current item to a variable you specify with as.
| Field | Type | Required | Description |
|---|---|---|---|
items | array or string | Yes | Inline array, {{variable}} reference, or $.response.body.path |
as | string | Yes | Variable name for the current item |
name | string | No | Exposes loop metadata: {{name.index}}, {{name.items}}, {{name.iterations}}, {{name.completed}} |
delayMs | integer | No | Milliseconds to wait between iterations |
Step-level forEach example:
{
"name": "Verify user {{user.name}}",
"forEach": {
"items": [
{ "name": "Alice", "email": "alice@test.com" },
{ "name": "Bob", "email": "bob@test.com" }
],
"as": "user"
},
"action": {
"type": "httpRequest",
"method": "GET",
"url": "api-gateway/api/users?email={{user.email}}"
},
"assertions": [
{
"assertions": [
{ "path": "$.response.body.name", "operator": "eq", "value": "{{user.name}}" }
]
}
]
}Loop name and metadata
The as field gives you the current item. The name field gives you everything else — the iteration index, the total items, and (after the loop finishes) how many iterations ran and whether the loop completed. Without name, you can only access the current item.
Use name when you need to:
- Number iterations —
{{name.index}}is the 0-based iteration counter - Build dynamic extract keys — e.g.
"userId_{{userLoop.index}}"createsuserId_0,userId_1, etc. - Reference the full items array —
{{name.items}} - Check loop results in a later step —
{{name.completed}}and{{name.iterations}}
Example — extracting a different variable per iteration using the index:
{
"name": "Create user {{user.name}}",
"forEach": {
"items": [
{ "name": "Alice", "email": "alice@test.com" },
{ "name": "Bob", "email": "bob@test.com" }
],
"as": "user",
"name": "userLoop"
},
"action": {
"type": "httpRequest",
"method": "POST",
"url": "api-gateway/api/users",
"body": { "name": "{{user.name}}", "email": "{{user.email}}" }
},
"extract": {
"userId_{{userLoop.index}}": "$.response.body.id"
}
}After this loop, {{userId_0}} and {{userId_1}} hold the IDs of Alice and Bob respectively.
You can also reference a variable instead of an inline array:
"forEach": {
"items": "{{users}}",
"as": "user"
}for
Count through a numeric range. The range is inclusive of both from and to.
| Field | Type | Required | Description |
|---|---|---|---|
from | integer | Yes | Start value |
to | integer | Yes | End value (inclusive) |
step | integer | No | Increment per iteration (defaults to 1). Use negative step for descending ranges. |
as | string | Yes | Variable name for the current value |
name | string | No | Exposes loop metadata: {{name.index}}, {{name.iterations}}, {{name.completed}} |
delayMs | integer | No | Milliseconds to wait between iterations |
{
"name": "Seed user {{i}}",
"for": { "from": 1, "to": 5, "as": "i" },
"action": {
"type": "httpRequest",
"method": "POST",
"url": "api-gateway/api/users",
"body": { "name": "user-{{i}}" }
}
}If from is greater than to, you must provide a negative step explicitly (e.g. "step": -1). Omitting step when from > to is a validation error.
repeat
Run a fixed number of times with an optional early-exit condition. Useful for polling until a condition is met.
| Field | Type | Required | Description |
|---|---|---|---|
count | integer | Yes | Maximum number of iterations |
as | string | Yes | Variable name for the iteration counter (0-based) |
name | string | No | Exposes loop metadata: {{name.index}}, {{name.iterations}}, {{name.completed}} |
delayMs | integer | No | Milliseconds to wait between iterations |
until | array of assertions | No | Checked after each iteration; all must pass to stop early |
Polling example:
{
"name": "Wait for job to complete (attempt {{attempt}})",
"repeat": {
"count": 10,
"as": "attempt",
"delayMs": 500,
"until": [
{ "path": "$.response.body.status", "operator": "eq", "value": "done" }
]
},
"action": {
"type": "httpRequest",
"method": "GET",
"url": "api-gateway/api/jobs/{{jobId}}"
}
}The loop always executes at least once. The until assertions are evaluated after each iteration, so the action runs before the exit condition is checked.
Loop result variables
When a loop has a name, result metadata is set on the named variable after the loop completes:
| Variable | Type | Description |
|---|---|---|
{{name.completed}} | boolean | true if the loop finished normally or until triggered; false if a repeat exhausted all iterations without until matching |
{{name.iterations}} | number | How many iterations actually ran |
Use these in a subsequent step to verify polling behavior:
{
"name": "Poll until ready",
"repeat": {
"count": 10, "as": "attempt", "name": "pollLoop",
"delayMs": 500,
"until": [{ "path": "$.response.body.status", "operator": "eq", "value": "done" }]
},
"action": { "type": "httpRequest", "method": "GET", "url": "api/jobs/{{jobId}}" }
},
{
"name": "Verify polling completed",
"action": { "type": "wait", "durationMs": 10 },
"assertions": [
{
"assertions": [
{ "path": "$.variables.pollLoop.completed", "operator": "eq", "value": true }
]
}
]
}stopOnFailure interaction
By default, a failed assertion in any loop iteration stops the loop immediately. Set "stopOnFailure": false on the step to collect all iteration failures without halting:
{
"name": "Verify all users (collect failures)",
"stopOnFailure": false,
"forEach": {
"items": "{{users}}",
"as": "user"
},
"action": { "type": "httpRequest", "method": "GET", "url": "api/users/{{user.id}}" },
"assertions": [
{ "assertions": [{ "path": "$.response.status", "operator": "eq", "value": 200 }] }
]
}Where loops can be used
Loops can be applied at multiple levels of a test definition:
| Level | Where | Effect |
|---|---|---|
| Test-level | forEach/for/repeat on a test | All steps repeat per iteration |
| Step-level | forEach/for/repeat on a step | Action + extract + assertions repeat per iteration |
| Action-level | forEach/for/repeat inside an action | Only the action repeats; extract + assertions run once after |
| Assertion-block | forEach on an assertion block | Assertions nested inside the loop body run once per item |
| UI sub-step group | forEach/for/repeat with nested steps inside a UI action | Sub-steps repeat per iteration |
Test-level loop
{
"name": "Verify order {{order.id}}",
"forEach": {
"items": "{{orders}}",
"as": "order"
},
"steps": [
{ "name": "Check status", "action": { "..." : "..." } },
{ "name": "Verify in DB", "action": { "..." : "..." } }
]
}Action-level loop
{
"name": "Seed users",
"action": {
"type": "httpRequest",
"method": "POST",
"url": "api-gateway/api/users",
"body": { "name": "user-{{i}}" },
"for": { "from": 1, "to": 5, "as": "i" }
},
"extract": {
"lastUserId": "$.response.body.id"
}
}Assertion-block loop
When a forEach is used inside an assertion block, the assertions are nested inside the loop object:
{
"assertions": [
{
"forEach": {
"items": "$.response.body.users",
"as": "user",
"assertions": [
{ "path": "$.variables.user.email", "operator": "exists" },
{ "path": "$.variables.user.active", "operator": "eq", "value": true }
]
}
}
]
}UI sub-step group
{
"forEach": {
"items": ["bad@", "", "missing-dot"],
"as": "email"
},
"steps": [
{ "type": { "selector": "#email", "text": "{{email}}" } },
{ "click": "#submit" },
{ "waitFor": "[data-testid='error']" }
]
}Extract transform
You can convert objects to arrays for iteration using transform in extract rules:
{
"extract": {
"configKeys": {
"path": "$.response.body.settings",
"transform": "keys"
}
}
}Three transform types are available:
| Transform | Output |
|---|---|
keys | Array of key strings |
values | Array of values |
entries | Array of { key, value } objects |
You can also use from instead of path to transform a variable reference:
{
"extract": {
"fieldNames": {
"from": "{{userTemplate}}",
"transform": "keys"
}
}
}