# Repeat steps in a loop

> Run steps once for each element of a list, several at a time, or again and again until a condition holds.

This guide runs a flow's steps once for each element of a list, several at a time, or again and again until a condition holds.

A `loop` step runs a small flow of its own, its **body**, several times:

- **`foreach`**: once for each element of a list, one or several at a time.
- **`while`**: again and again, until a condition on the last pass holds.

## Before you start

- **A pack with the tools** from [Write a flow](https://docs.kindgi.com/v0.1.9/guides/flows/write-a-flow.md), such as `acme.get-order`, on a runtime such as `kindgi dev`, with Kindgi 0.1's CLI.

## For each element of a list

A `foreach` loop does the same work for every element. Ask your coding agent: "I want a flow that checks the stock of every item in an order, two at a time." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.1.9/start/coding-agents.md)). For reference, the tool and the flow look like this. `acme.check-order-stock` checks every item of an order with a new tool, `acme.check-stock`, two items at a time:

- **TypeScript**

  `tools/check-stock/index.ts`

  ```ts
  import { defineTool } from '@kindgi/sdk/define';
  import type { ToolId } from '@kindgi/sdk/types';
  import { z } from 'zod';

  const STOCK: Record<string, number> = { mug: 10, desk: 0, lamp: 5 };

  const defined = defineTool({
    id: 'acme.check-stock' as ToolId,
    description: 'Checks whether a quantity of one item is in stock.',
    version: '0.1.0',
    input: z.object({ sku: z.string(), quantity: z.number().int().positive() }),
    output: z.object({ sku: z.string(), inStock: z.boolean() }),
    effects: [],
    mutating: false,
    handler: async ({ sku, quantity }) => ({ sku, inStock: (STOCK[sku] ?? 0) >= quantity }),
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

  `flows/check-order-stock/index.ts`

  ```ts
  import { defineFlow } from '@kindgi/sdk/define';

  const defined = defineFlow({
    id: 'acme.check-order-stock',
    version: '0.1.0',
    name: 'Check stock for an order',
    description: 'Checks every item of an order, two at a time.',
    nodes: [
      {
        id: 'order',
        kind: 'tool',
        ref: 'acme.get-order',
        inputMapping: { orderId: { path: 'runInput.orderId' } },
      },
      {
        id: 'each-item',
        kind: 'loop',
        loopKind: 'foreach',
        iterateOver: { path: 'nodeOutputs.order.items' },
        concurrency: 2,
        maxIterations: 100,
        collectAllIterations: true,
        outputSchema: {
          type: 'object',
          properties: { sku: { type: 'string' }, inStock: { type: 'boolean' } },
          required: ['sku', 'inStock'],
        },
        body: {
          nodes: [{ id: 'check', kind: 'tool', ref: 'acme.check-stock' }],
          edges: [
            { id: 'b1', from: '$loop-start', to: 'check' },
            { id: 'b2', from: 'check', to: '$loop-end' },
          ],
        },
      },
    ],
    edges: [
      { id: 'e1', from: '$start', to: 'order' },
      { id: 'e2', from: 'order', to: 'each-item' },
      { id: 'e3', from: 'each-item', to: '$end' },
    ],
    output: {
      mapping: { items: { path: 'nodeOutputs.each-item.outputs' } },
    },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

- **Python**

  `tools/check_stock.py`

  ```python
  from pydantic import BaseModel, Field

  from kindgi import tool

  STOCK = {"mug": 10, "desk": 0, "lamp": 5}


  class StockInput(BaseModel):
      sku: str
      quantity: int = Field(gt=0)


  class Stock(BaseModel):
      sku: str
      in_stock: bool = Field(alias="inStock")


  @tool(id="acme.check-stock", mutating=False)
  def check_stock(input: StockInput) -> Stock:
      """Checks whether a quantity of one item is in stock."""
      return Stock(sku=input.sku, inStock=STOCK.get(input.sku, 0) >= input.quantity)
  ```

  `flows/check_order_stock.py`

  ```python
  from kindgi import Flow

  check_order_stock = Flow(
      id="acme.check-order-stock",
      version="0.1.0",
      name="Check stock for an order",
      description="Checks every item of an order, two at a time.",
      nodes=[
          {
              "id": "order",
              "kind": "tool",
              "ref": "acme.get-order",
              "inputMapping": {"orderId": {"path": "runInput.orderId"}},
          },
          {
              "id": "each-item",
              "kind": "loop",
              "loopKind": "foreach",
              "iterateOver": {"path": "nodeOutputs.order.items"},
              "concurrency": 2,
              "maxIterations": 100,
              "collectAllIterations": True,
              "outputSchema": {
                  "type": "object",
                  "properties": {"sku": {"type": "string"}, "inStock": {"type": "boolean"}},
                  "required": ["sku", "inStock"],
              },
              "body": {
                  "nodes": [{"id": "check", "kind": "tool", "ref": "acme.check-stock"}],
                  "edges": [
                      {"id": "b1", "from": "$loop-start", "to": "check"},
                      {"id": "b2", "from": "check", "to": "$loop-end"},
                  ],
              },
          },
      ],
      edges=[
          {"id": "e1", "from": "$start", "to": "order"},
          {"id": "e2", "from": "order", "to": "each-item"},
          {"id": "e3", "from": "each-item", "to": "$end"},
      ],
      output={"mapping": {"items": {"path": "nodeOutputs.each-item.outputs"}}},
  )
  ```

- **Java**

  `src/main/java/acme/tools/CheckStock.java`

  ```java
  package acme.tools;

  import com.kindgi.pack.Tool;
  import jakarta.validation.constraints.Positive;
  import java.util.Map;

  public final class CheckStock {
    public record Input(String sku, @Positive int quantity) {}

    public record Output(String sku, boolean inStock) {}

    private static final Map<String, Integer> STOCK = Map.of("mug", 10, "desk", 0, "lamp", 5);

    public static final Tool<Input, Output> TOOL = Tool.define("acme.check-stock")
        .description("Checks whether a quantity of one item is in stock.")
        .version("0.1.0")
        .input(Input.class)
        .output(Output.class)
        .mutating(false)
        .handler((input, ctx) ->
            new Output(input.sku(), STOCK.getOrDefault(input.sku(), 0) >= input.quantity()));

    private CheckStock() {}
  }
  ```

  `src/main/java/acme/flows/CheckOrderStock.java`

  ```java
  package acme.flows;

  import com.kindgi.pack.Flow;
  import java.util.List;
  import java.util.Map;

  public final class CheckOrderStock {
    public static final Flow FLOW = Flow.define("acme.check-order-stock")
        .version("0.1.0")
        .set("name", "Check stock for an order")
        .set("description", "Checks every item of an order, two at a time.")
        .node(Map.of("id", "order", "kind", "tool", "ref", "acme.get-order",
            "inputMapping", Map.of("orderId", Map.of("path", "runInput.orderId"))))
        .node(Map.of(
            "id", "each-item",
            "kind", "loop",
            "loopKind", "foreach",
            "iterateOver", Map.of("path", "nodeOutputs.order.items"),
            "concurrency", 2,
            "maxIterations", 100,
            "collectAllIterations", true,
            "outputSchema", Map.of(
                "type", "object",
                "properties", Map.of("sku", Map.of("type", "string"), "inStock", Map.of("type", "boolean")),
                "required", List.of("sku", "inStock")),
            "body", Map.of(
                "nodes", List.of(Map.of("id", "check", "kind", "tool", "ref", "acme.check-stock")),
                "edges", List.of(
                    Map.of("id", "b1", "from", "$loop-start", "to", "check"),
                    Map.of("id", "b2", "from", "check", "to", "$loop-end")))))
        .edge("e1", "$start", "order")
        .edge("e2", "order", "each-item")
        .edge("e3", "each-item", "$end")
        .set("output", Map.of("mapping", Map.of("items", Map.of("path", "nodeOutputs.each-item.outputs"))))
        .build();

    private CheckOrderStock() {}
  }
  ```

- **Scala**

  `src/main/scala/acme/tools/CheckStock.scala`

  ```scala
  package acme.tools

  import com.kindgi.pack.scaladsl._
  import jakarta.validation.constraints.Positive

  object CheckStock {
    final case class Input(sku: String, @Positive quantity: Int)
    final case class Output(sku: String, inStock: Boolean)

    private val stock = Map("mug" -> 10, "desk" -> 0, "lamp" -> 5)

    val tool: Tool[Input, Output] = Tool[Input, Output]("acme.check-stock")
      .description("Checks whether a quantity of one item is in stock.")
      .version("0.1.0")
      .readOnly
      .handler((in, _) => Output(in.sku, stock.getOrElse(in.sku, 0) >= in.quantity))
  }
  ```

  `src/main/scala/acme/flows/CheckOrderStock.scala`

  ```scala
  package acme.flows

  import com.kindgi.pack.scaladsl._

  object CheckOrderStock {
    val flow: Flow = Flow("acme.check-order-stock")
      .version("0.1.0")
      .set("name", "Check stock for an order")
      .set("description", "Checks every item of an order, two at a time.")
      .node(Map("id" -> "order", "kind" -> "tool", "ref" -> "acme.get-order",
        "inputMapping" -> Map("orderId" -> Map("path" -> "runInput.orderId"))))
      .node(Map(
        "id" -> "each-item",
        "kind" -> "loop",
        "loopKind" -> "foreach",
        "iterateOver" -> Map("path" -> "nodeOutputs.order.items"),
        "concurrency" -> 2,
        "maxIterations" -> 100,
        "collectAllIterations" -> true,
        "outputSchema" -> Map(
          "type" -> "object",
          "properties" -> Map("sku" -> Map("type" -> "string"), "inStock" -> Map("type" -> "boolean")),
          "required" -> List("sku", "inStock")),
        "body" -> Map(
          "nodes" -> List(Map("id" -> "check", "kind" -> "tool", "ref" -> "acme.check-stock")),
          "edges" -> List(
            Map("id" -> "b1", "from" -> "$loop-start", "to" -> "check"),
            Map("id" -> "b2", "from" -> "check", "to" -> "$loop-end")))))
      .edge("e1", "$start", "order")
      .edge("e2", "order", "each-item")
      .edge("e3", "each-item", "$end")
      .set("output", Map("mapping" -> Map("items" -> Map("path" -> "nodeOutputs.each-item.outputs"))))
      .build()
  }
  ```

* **`iterateOver`** is the list: a `{ path }` that must resolve to an array when the loop starts.
* **`body`** has its own steps and edges, from `$loop-start` to `$loop-end`. Each pass gets one element as its input: here `{ "sku": "desk", "quantity": 1 }`, which is `acme.check-stock`'s input as it is.
* **`concurrency`** runs that many passes at a time, 1 by default and at most 32.
* **`maxIterations`** caps the passes, and **`outputSchema`** is what each pass must return. Both are required. An `outputSchema` that doesn't compile is refused when the flow is loaded or deployed, not at each run.

Run it:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}'
  ```

`--flow` is the flow, and `--input` its input. It prints the run, with each item's stock:

```json excerpt
{
  …
  "status": "completed",
  …
  "output": {
    "items": [
      {
        "sku": "desk",
        "inStock": false
      },
      {
        "sku": "lamp",
        "inStock": true
      }
    ]
  },
  …
}
```

The loop step's own output is in the journal, which `kindgi runs journal <run-id>` prints:

```json excerpt
{"sequence": 18, "kind": "step.completed", "nodeId": "each-item", "payload": {"output": {"outputs": [{"sku": "desk", "inStock": false}, {"sku": "lamp", "inStock": true}], "iterations": 2, "stopReason": "array-exhausted", "finalOutput": {"sku": "lamp", "inStock": true}}}, …}
```

- `finalOutput` is the last pass's output.
- `outputs` has every pass's output, in the list's order, even when passes finish out of order. It's there only with `collectAllIterations: true`.
- `iterations` is how many passes ran.
- `stopReason` is why the loop stopped: `array-exhausted`, `exit-condition` for a `while` loop, or `max-iterations`.

Each pass is in the journal as `iteration.started` and `iteration.completed`, and the body's steps carry the pass they belong to.

> **Caution: Inside the body**
>
> In a body step's `inputMapping`, `runInput` is the pass's input, the element. `nodeOutputs` has only the body's own steps. A path to a step outside the loop, or to the run's input, doesn't resolve. Put what every pass needs into the list's elements.

## Until a condition holds

A `while` loop repeats until its condition holds. Ask your coding agent: "I want a flow that reads every page of my open orders." For reference, the tool and the flow look like this. `acme.list-all-orders` reads every page of a paginated list with a new tool, `acme.list-orders`:

- **TypeScript**

  `tools/list-orders/index.ts`

  ```ts
  import { defineTool } from '@kindgi/sdk/define';
  import type { ToolId } from '@kindgi/sdk/types';
  import { z } from 'zod';

  const PAGES: Record<string, { orders: string[]; nextCursor?: string }> = {
    first: { orders: ['A-100', 'A-101'], nextCursor: 'p2' },
    p2: { orders: ['A-102', 'A-103'], nextCursor: 'p3' },
    p3: { orders: ['A-200'] },
  };

  const defined = defineTool({
    id: 'acme.list-orders' as ToolId,
    description: 'Lists open orders, one page at a time.',
    version: '0.1.0',
    input: z.object({ cursor: z.string().optional() }),
    output: z.object({ orders: z.array(z.string()), nextCursor: z.string().optional() }),
    effects: [],
    mutating: false,
    handler: async ({ cursor }) => {
      const page = PAGES[cursor ?? 'first'];
      if (page === undefined) throw new Error(`Unknown cursor ${cursor}`);
      return page;
    },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

  `flows/list-all-orders/index.ts`

  ```ts
  import { defineFlow } from '@kindgi/sdk/define';

  const defined = defineFlow({
    id: 'acme.list-all-orders',
    version: '0.1.0',
    name: 'List all open orders',
    description: 'Reads every page of open orders.',
    nodes: [
      {
        id: 'pages',
        kind: 'loop',
        loopKind: 'while',
        // stop when the last page read has no next cursor
        exitCondition: { op: 'falsy', value: { path: 'iterationOutput.nextCursor' } },
        maxIterations: 20,
        collectAllIterations: true,
        outputSchema: {
          type: 'object',
          properties: {
            orders: { type: 'array', items: { type: 'string' } },
            nextCursor: { type: ['string', 'null'] },
          },
          required: ['orders'],
        },
        body: {
          nodes: [
            {
              id: 'page',
              kind: 'tool',
              ref: 'acme.list-orders',
              // runInput here is the previous page (the run input on the first pass)
              inputMapping: { cursor: { path: 'runInput.nextCursor' } },
            },
          ],
          edges: [
            { id: 'b1', from: '$loop-start', to: 'page' },
            { id: 'b2', from: 'page', to: '$loop-end' },
          ],
        },
      },
    ],
    edges: [
      { id: 'e1', from: '$start', to: 'pages' },
      { id: 'e2', from: 'pages', to: '$end' },
    ],
    output: {
      mapping: { pages: { path: 'nodeOutputs.pages.outputs' } },
    },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

- **Python**

  `tools/list_orders.py`

  ```python
  from pydantic import BaseModel, Field

  from kindgi import tool

  PAGES = {
      "first": {"orders": ["A-100", "A-101"], "nextCursor": "p2"},
      "p2": {"orders": ["A-102", "A-103"], "nextCursor": "p3"},
      "p3": {"orders": ["A-200"]},
  }


  class ListInput(BaseModel):
      cursor: str | None = None


  class Page(BaseModel):
      orders: list[str]
      next_cursor: str | None = Field(None, alias="nextCursor")


  @tool(id="acme.list-orders", mutating=False)
  def list_orders(input: ListInput) -> Page:
      """Lists open orders, one page at a time."""
      page = PAGES.get(input.cursor or "first")
      if page is None:
          raise ValueError(f"Unknown cursor {input.cursor}")
      return Page(**page)
  ```

  `flows/list_all_orders.py`

  ```python
  from kindgi import Flow

  list_all_orders = Flow(
      id="acme.list-all-orders",
      version="0.1.0",
      name="List all open orders",
      description="Reads every page of open orders.",
      nodes=[
          {
              "id": "pages",
              "kind": "loop",
              "loopKind": "while",
              # stop when the last page read has no next cursor
              "exitCondition": {"op": "falsy", "value": {"path": "iterationOutput.nextCursor"}},
              "maxIterations": 20,
              "collectAllIterations": True,
              "outputSchema": {
                  "type": "object",
                  "properties": {
                      "orders": {"type": "array", "items": {"type": "string"}},
                      "nextCursor": {"type": ["string", "null"]},
                  },
                  "required": ["orders"],
              },
              "body": {
                  "nodes": [
                      {
                          "id": "page",
                          "kind": "tool",
                          "ref": "acme.list-orders",
                          # runInput here is the previous page (the run input on the first pass)
                          "inputMapping": {"cursor": {"path": "runInput.nextCursor"}},
                      }
                  ],
                  "edges": [
                      {"id": "b1", "from": "$loop-start", "to": "page"},
                      {"id": "b2", "from": "page", "to": "$loop-end"},
                  ],
              },
          },
      ],
      edges=[
          {"id": "e1", "from": "$start", "to": "pages"},
          {"id": "e2", "from": "pages", "to": "$end"},
      ],
      output={"mapping": {"pages": {"path": "nodeOutputs.pages.outputs"}}},
  )
  ```

- **Java**

  `src/main/java/acme/tools/ListOrders.java`

  ```java
  package acme.tools;

  import com.kindgi.pack.Tool;
  import java.util.List;
  import java.util.Map;
  import org.jspecify.annotations.Nullable;

  public final class ListOrders {
    public record Input(@Nullable String cursor) {}

    public record Page(List<String> orders, @Nullable String nextCursor) {}

    private static final Map<String, Page> PAGES = Map.of(
        "first", new Page(List.of("A-100", "A-101"), "p2"),
        "p2", new Page(List.of("A-102", "A-103"), "p3"),
        "p3", new Page(List.of("A-200"), null));

    public static final Tool<Input, Page> TOOL = Tool.define("acme.list-orders")
        .description("Lists open orders, one page at a time.")
        .version("0.1.0")
        .input(Input.class)
        .output(Page.class)
        .mutating(false)
        .handler((input, ctx) -> {
          Page page = PAGES.get(input.cursor() == null ? "first" : input.cursor());
          if (page == null) throw new IllegalArgumentException("Unknown cursor " + input.cursor());
          return page;
        });

    private ListOrders() {}
  }
  ```

  `src/main/java/acme/flows/ListAllOrders.java`

  ```java
  package acme.flows;

  import com.kindgi.pack.Flow;
  import java.util.List;
  import java.util.Map;

  public final class ListAllOrders {
    public static final Flow FLOW = Flow.define("acme.list-all-orders")
        .version("0.1.0")
        .set("name", "List all open orders")
        .set("description", "Reads every page of open orders.")
        .node(Map.of(
            "id", "pages",
            "kind", "loop",
            "loopKind", "while",
            // stop when the last page read has no next cursor
            "exitCondition", Map.of("op", "falsy", "value", Map.of("path", "iterationOutput.nextCursor")),
            "maxIterations", 20,
            "collectAllIterations", true,
            "outputSchema", Map.of(
                "type", "object",
                "properties", Map.of(
                    "orders", Map.of("type", "array", "items", Map.of("type", "string")),
                    "nextCursor", Map.of("type", List.of("string", "null"))),
                "required", List.of("orders")),
            "body", Map.of(
                "nodes", List.of(Map.of(
                    "id", "page",
                    "kind", "tool",
                    "ref", "acme.list-orders",
                    // runInput here is the previous page (the run input on the first pass)
                    "inputMapping", Map.of("cursor", Map.of("path", "runInput.nextCursor")))),
                "edges", List.of(
                    Map.of("id", "b1", "from", "$loop-start", "to", "page"),
                    Map.of("id", "b2", "from", "page", "to", "$loop-end")))))
        .edge("e1", "$start", "pages")
        .edge("e2", "pages", "$end")
        .set("output", Map.of("mapping", Map.of("pages", Map.of("path", "nodeOutputs.pages.outputs"))))
        .build();

    private ListAllOrders() {}
  }
  ```

- **Scala**

  `src/main/scala/acme/tools/ListOrders.scala`

  ```scala
  package acme.tools

  import com.kindgi.pack.scaladsl._

  object ListOrders {
    final case class Input(cursor: Option[String])
    final case class Page(orders: Seq[String], nextCursor: Option[String])

    private val pages = Map(
      "first" -> Page(Seq("A-100", "A-101"), Some("p2")),
      "p2" -> Page(Seq("A-102", "A-103"), Some("p3")),
      "p3" -> Page(Seq("A-200"), None))

    val tool: Tool[Input, Page] = Tool[Input, Page]("acme.list-orders")
      .description("Lists open orders, one page at a time.")
      .version("0.1.0")
      .readOnly
      .handler { (in, _) =>
        val cursor = in.cursor.getOrElse("first")
        pages.getOrElse(cursor, throw new NoSuchElementException(s"Unknown cursor $cursor"))
      }
  }
  ```

  `src/main/scala/acme/flows/ListAllOrders.scala`

  ```scala
  package acme.flows

  import com.kindgi.pack.scaladsl._

  object ListAllOrders {
    val flow: Flow = Flow("acme.list-all-orders")
      .version("0.1.0")
      .set("name", "List all open orders")
      .set("description", "Reads every page of open orders.")
      .node(Map(
        "id" -> "pages",
        "kind" -> "loop",
        "loopKind" -> "while",
        // stop when the last page read has no next cursor
        "exitCondition" -> Map("op" -> "falsy", "value" -> Map("path" -> "iterationOutput.nextCursor")),
        "maxIterations" -> 20,
        "collectAllIterations" -> true,
        "outputSchema" -> Map(
          "type" -> "object",
          "properties" -> Map(
            "orders" -> Map("type" -> "array", "items" -> Map("type" -> "string")),
            "nextCursor" -> Map("type" -> List("string", "null"))),
          "required" -> List("orders")),
        "body" -> Map(
          "nodes" -> List(Map(
            "id" -> "page",
            "kind" -> "tool",
            "ref" -> "acme.list-orders",
            // runInput here is the previous page (the run input on the first pass)
            "inputMapping" -> Map("cursor" -> Map("path" -> "runInput.nextCursor")))),
          "edges" -> List(
            Map("id" -> "b1", "from" -> "$loop-start", "to" -> "page"),
            Map("id" -> "b2", "from" -> "page", "to" -> "$loop-end")))))
      .edge("e1", "$start", "pages")
      .edge("e2", "pages", "$end")
      .set("output", Map("mapping" -> Map("pages" -> Map("path" -> "nodeOutputs.pages.outputs"))))
      .build()
  }
  ```

* The first pass gets the loop step's input: the output of the step before it, here the run's input. Each later pass gets the **previous pass's output**, so `runInput.nextCursor` in the body is the cursor the last page returned.
* **`exitCondition`** is checked after each pass, against `iterationOutput`, that pass's output, and `iterationIndex`, from 0. The loop stops when it holds, or after `maxIterations` passes.

Run it, with an empty input:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=acme.list-all-orders --input='{}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=acme.list-all-orders --input='{}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=acme.list-all-orders --input='{}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=acme.list-all-orders --input='{}'
  ```

The run's `output` holds every page's orders:

- **TypeScript**

  ```json
  {
    "pages": [
      { "orders": ["A-100", "A-101"], "nextCursor": "p2" },
      { "orders": ["A-102", "A-103"], "nextCursor": "p3" },
      { "orders": ["A-200"] }
    ]
  }
  ```

- **Python**

  ```json
  {
    "pages": [
      { "orders": ["A-100", "A-101"], "nextCursor": "p2" },
      { "orders": ["A-102", "A-103"], "nextCursor": "p3" },
      { "orders": ["A-200"], "nextCursor": null }
    ]
  }
  ```

  A pydantic field that is `None` is `null` in the output. So the schema allows `null`, and the condition is `falsy`, which is true for a missing value and for `null`.

- **Java**

  ```json
  {
    "pages": [
      { "orders": ["A-100", "A-101"], "nextCursor": "p2" },
      { "orders": ["A-102", "A-103"], "nextCursor": "p3" },
      { "orders": ["A-200"] }
    ]
  }
  ```

- **Scala**

  ```json
  {
    "pages": [
      { "orders": ["A-100", "A-101"], "nextCursor": "p2" },
      { "orders": ["A-102", "A-103"], "nextCursor": "p3" },
      { "orders": ["A-200"] }
    ]
  }
  ```

That's the run's `output`, from `nodeOutputs.pages.outputs`.

A loop that reaches `maxIterations` stops with `stopReason: "max-iterations"` and completes. Check `stopReason` if running out of passes is an error for you.

## Next

- [Run steps in parallel](https://docs.kindgi.com/v0.1.9/guides/flows/run-steps-in-parallel.md)
- [Pass data between steps](https://docs.kindgi.com/v0.1.9/guides/flows/pass-data-between-steps.md)
- [Retry a failing step](https://docs.kindgi.com/v0.1.9/guides/flows/retry-a-step.md)
