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.

FieldTypeRequiredDescription
itemsarray or stringYesInline array, {{variable}} reference, or $.response.body.path
asstringYesVariable name for the current item
namestringNoExposes loop metadata: {{name.index}}, {{name.items}}, {{name.iterations}}, {{name.completed}}
delayMsintegerNoMilliseconds 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:

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.

FieldTypeRequiredDescription
fromintegerYesStart value
tointegerYesEnd value (inclusive)
stepintegerNoIncrement per iteration (defaults to 1). Use negative step for descending ranges.
asstringYesVariable name for the current value
namestringNoExposes loop metadata: {{name.index}}, {{name.iterations}}, {{name.completed}}
delayMsintegerNoMilliseconds 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.

FieldTypeRequiredDescription
countintegerYesMaximum number of iterations
asstringYesVariable name for the iteration counter (0-based)
namestringNoExposes loop metadata: {{name.index}}, {{name.iterations}}, {{name.completed}}
delayMsintegerNoMilliseconds to wait between iterations
untilarray of assertionsNoChecked 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:

VariableTypeDescription
{{name.completed}}booleantrue if the loop finished normally or until triggered; false if a repeat exhausted all iterations without until matching
{{name.iterations}}numberHow 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:

LevelWhereEffect
Test-levelforEach/for/repeat on a testAll steps repeat per iteration
Step-levelforEach/for/repeat on a stepAction + extract + assertions repeat per iteration
Action-levelforEach/for/repeat inside an actionOnly the action repeats; extract + assertions run once after
Assertion-blockforEach on an assertion blockAssertions nested inside the loop body run once per item
UI sub-step groupforEach/for/repeat with nested steps inside a UI actionSub-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:

TransformOutput
keysArray of key strings
valuesArray of values
entriesArray of { key, value } objects

You can also use from instead of path to transform a variable reference:

{
  "extract": {
    "fieldNames": {
      "from": "{{userTemplate}}",
      "transform": "keys"
    }
  }
}