# Write a flow

> Define a flow of tool and agent steps, run it, and change it while kindgi dev runs.

This page builds `acme.process-order`: look an order up, have an agent write a thank-you note, then confirm the order with that note. Two tools, one agent, one flow.

## The tools

Ask your coding agent: "I want a tool that looks up an order, and one that confirms an order with a note." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.3.1/start/coding-agents.md)). For reference, the tools look like this. `acme.get-order` only reads, so it says `mutating: false`. `acme.confirm-order` changes something, so it doesn't:

- **TypeScript**

  `tools/get-order/index.ts`

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

  const Order = z.object({
    orderId: z.string(),
    customer: z.string(),
    total: z.number(),
    items: z.array(z.object({ sku: z.string(), quantity: z.number().int() })),
  });

  const ORDERS: Record<string, z.infer<typeof Order>> = {
    'A-100': { orderId: 'A-100', customer: 'ada@example.com', total: 42.5, items: [{ sku: 'mug', quantity: 2 }] },
    'A-200': {
      orderId: 'A-200',
      customer: 'grace@example.com',
      total: 1250,
      items: [
        { sku: 'desk', quantity: 1 },
        { sku: 'lamp', quantity: 2 },
      ],
    },
  };

  const defined = defineTool({
    id: 'acme.get-order' as ToolId,
    description: 'Looks up an order by id.',
    version: '0.1.0',
    input: z.object({ orderId: z.string().min(1) }),
    output: Order,
    effects: [],
    mutating: false, // only reads
    handler: async ({ orderId }) => {
      const order = ORDERS[orderId];
      if (order === undefined) throw new Error(`No order ${orderId}`);
      return order;
    },
  });

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

  export default defined.value;
  ```

  `tools/confirm-order/index.ts`

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

  const defined = defineTool({
    id: 'acme.confirm-order' as ToolId,
    description: 'Confirms an order and emails the customer.',
    version: '0.1.0',
    input: z.object({ orderId: z.string(), note: z.string().optional() }),
    output: z.object({
      orderId: z.string(),
      status: z.literal('confirmed'),
      note: z.string().optional(),
    }),
    effects: [],
    // no `mutating: false`: it changes something
    handler: async ({ orderId, note }) => ({ orderId, status: 'confirmed' as const, note }),
  });

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

  export default defined.value;
  ```

- **Python**

  `tools/get_order.py`

  ```python
  from pydantic import BaseModel, Field

  from kindgi import tool


  class OrderQuery(BaseModel):
      order_id: str = Field(alias="orderId", min_length=1)


  class Item(BaseModel):
      sku: str
      quantity: int


  class Order(BaseModel):
      order_id: str = Field(alias="orderId")
      customer: str
      total: float
      items: list[Item]


  ORDERS = {
      "A-100": {"customer": "ada@example.com", "total": 42.5, "items": [{"sku": "mug", "quantity": 2}]},
      "A-200": {
          "customer": "grace@example.com",
          "total": 1250,
          "items": [{"sku": "desk", "quantity": 1}, {"sku": "lamp", "quantity": 2}],
      },
  }


  @tool(id="acme.get-order", mutating=False)  # only reads
  def get_order(input: OrderQuery) -> Order:
      """Looks up an order by id."""
      order = ORDERS.get(input.order_id)
      if order is None:
          raise ValueError(f"No order {input.order_id}")
      return Order(orderId=input.order_id, **order)
  ```

  `tools/confirm_order.py`

  ```python
  from typing import Literal

  from pydantic import BaseModel, Field

  from kindgi import tool


  class ConfirmInput(BaseModel):
      order_id: str = Field(alias="orderId")
      note: str | None = None


  class Confirmed(BaseModel):
      order_id: str = Field(alias="orderId")
      status: Literal["confirmed"] = "confirmed"
      note: str | None = None


  @tool(id="acme.confirm-order")  # no mutating=False: it changes something
  def confirm_order(input: ConfirmInput) -> Confirmed:
      """Confirms an order and emails the customer."""
      return Confirmed(orderId=input.order_id, note=input.note)
  ```

- **Java**

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

  ```java
  package acme.tools;

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

  public final class GetOrder {
    public record Input(@Size(min = 1) String orderId) {}

    public record Item(String sku, int quantity) {}

    public record Order(String orderId, String customer, double total, List<Item> items) {}

    private static final Map<String, Order> ORDERS = Map.of(
        "A-100", new Order("A-100", "ada@example.com", 42.5, List.of(new Item("mug", 2))),
        "A-200", new Order("A-200", "grace@example.com", 1250,
            List.of(new Item("desk", 1), new Item("lamp", 2))));

    public static final Tool<Input, Order> TOOL = Tool.define("acme.get-order")
        .description("Looks up an order by id.")
        .version("0.1.0")
        .input(Input.class)
        .output(Order.class)
        .mutating(false) // only reads
        .handler((input, ctx) -> {
          Order order = ORDERS.get(input.orderId());
          if (order == null) throw new IllegalArgumentException("No order " + input.orderId());
          return order;
        });

    private GetOrder() {}
  }
  ```

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

  ```java
  package acme.tools;

  import com.fasterxml.jackson.annotation.JsonProperty;
  import com.kindgi.pack.Tool;
  import org.jspecify.annotations.Nullable;

  public final class ConfirmOrder {
    public record Input(String orderId, @Nullable String note) {}

    public enum Status {
      @JsonProperty("confirmed") CONFIRMED
    }

    public record Confirmed(String orderId, Status status, @Nullable String note) {}

    public static final Tool<Input, Confirmed> TOOL = Tool.define("acme.confirm-order")
        .description("Confirms an order and emails the customer.")
        .version("0.1.0")
        .input(Input.class)
        .output(Confirmed.class)
        // no mutating(false): it changes something
        .handler((input, ctx) -> new Confirmed(input.orderId(), Status.CONFIRMED, input.note()));

    private ConfirmOrder() {}
  }
  ```

- **Scala**

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

  ```scala
  package acme.tools

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

  object GetOrder {
    final case class Input(@Size(min = 1) orderId: String)
    final case class Item(sku: String, quantity: Int)
    final case class Order(orderId: String, customer: String, total: Double, items: Seq[Item])

    private val orders = Map(
      "A-100" -> Order("A-100", "ada@example.com", 42.5, Seq(Item("mug", 2))),
      "A-200" -> Order("A-200", "grace@example.com", 1250, Seq(Item("desk", 1), Item("lamp", 2))))

    val tool: Tool[Input, Order] = Tool[Input, Order]("acme.get-order")
      .description("Looks up an order by id.")
      .version("0.1.0")
      .readOnly // only reads
      .handler((in, _) =>
        orders.getOrElse(in.orderId, throw new NoSuchElementException(s"No order ${in.orderId}")))
  }
  ```

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

  ```scala
  package acme.tools

  import com.kindgi.pack.scaladsl._

  object ConfirmOrder {
    final case class Input(orderId: String, note: Option[String])

    enum Status {
      case confirmed
    }

    final case class Confirmed(orderId: String, status: Status, note: Option[String])

    val tool: Tool[Input, Confirmed] = Tool[Input, Confirmed]("acme.confirm-order")
      .description("Confirms an order and emails the customer.")
      .version("0.1.0")
      // no readOnly: it changes something
      .handler((in, _) => Confirmed(in.orderId, Status.confirmed, in.note))
  }
  ```

## The agent

Ask your coding agent: "I want an agent that writes a one-sentence thank-you note for an order." For reference, the agent looks like this. It has no tools: it reads the order's customer and total from its input and answers with one sentence:

- **TypeScript**

  `agents/note-writer/index.ts`

  ```ts
  import { defineAgent } from '@kindgi/sdk/define';
  import type { AgentId, Semver } from '@kindgi/sdk/types';

  const defined = defineAgent({
    id: 'acme.note-writer' as AgentId,
    version: '0.1.0' as Semver,
    name: 'Note writer',
    description: 'Writes a one-sentence thank-you note for an order.',
    instructions:
      'Write a one-sentence thank-you note to {{ input.customer }} for their order of {{ input.total }} USD. Answer with the note only.',
    capabilities: [{ needs: [{ feature: 'tool-use' as const }] }],
    tools: [],
    retrieval: [],
    guardrails: [],
    parameters: [],
    budget: { maxSteps: 2, maxCostUsd: 0.05, maxWallMs: 30_000 },
  });

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

  export default defined.value;
  ```

- **Python**

  `agents/note_writer.py`

  ```python
  from kindgi import Agent

  note_writer = Agent(
      id="acme.note-writer",
      version="0.1.0",
      name="Note writer",
      description="Writes a one-sentence thank-you note for an order.",
      instructions=(
          "Write a one-sentence thank-you note to {{ input.customer }} for their order of "
          "{{ input.total }} USD. Answer with the note only."
      ),
      capabilities=[{"needs": [{"feature": "tool-use"}]}],
      budget={"maxSteps": 2, "maxCostUsd": 0.05, "maxWallMs": 30_000},
  )
  ```

- **Java**

  `src/main/java/acme/agents/NoteWriter.java`

  ```java
  package acme.agents;

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

  public final class NoteWriter {
    public static final Agent AGENT = Agent.define("acme.note-writer")
        .version("0.1.0")
        .name("Note writer")
        .description("Writes a one-sentence thank-you note for an order.")
        .instructions("Write a one-sentence thank-you note to {{ input.customer }} for their order of "
            + "{{ input.total }} USD. Answer with the note only.")
        .capability(Map.of("needs", List.of(Map.of("feature", "tool-use"))))
        .set("budget", Map.of("maxSteps", 2, "maxCostUsd", 0.05, "maxWallMs", 30_000))
        .build();

    private NoteWriter() {}
  }
  ```

- **Scala**

  `src/main/scala/acme/agents/NoteWriter.scala`

  ```scala
  package acme.agents

  import com.kindgi.pack.scaladsl._

  object NoteWriter {
    val agent: Agent = Agent("acme.note-writer")
      .version("0.1.0")
      .name("Note writer")
      .description("Writes a one-sentence thank-you note for an order.")
      .instructions("Write a one-sentence thank-you note to {{ input.customer }} for their order of " +
        "{{ input.total }} USD. Answer with the note only.")
      .capability(Map("needs" -> Seq(Map("feature" -> "tool-use"))))
      .set("budget", Map("maxSteps" -> 2, "maxCostUsd" -> 0.05, "maxWallMs" -> 30000))
      .build()
  }
  ```

## The flow

Ask your coding agent: "I want a flow that looks up an order, has the agent write a thank-you note, then confirms the order with that note." For reference, the flow looks like this:

- **TypeScript**

  `flows/process-order/index.ts`

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

  const defined = defineFlow({
    id: 'acme.process-order',
    version: '0.1.0',
    name: 'Process an order',
    description: 'Looks up an order, writes a thank-you note, and confirms the order with it.',
    nodes: [
      {
        id: 'order',
        kind: 'tool',
        ref: 'acme.get-order',
        inputMapping: { orderId: { path: 'runInput.orderId' } },
      },
      {
        id: 'note',
        kind: 'agent',
        ref: 'acme.note-writer',
        inputMapping: {
          customer: { path: 'nodeOutputs.order.customer' },
          total: { path: 'nodeOutputs.order.total' },
        },
      },
      {
        id: 'confirm',
        kind: 'tool',
        ref: 'acme.confirm-order',
        inputMapping: {
          orderId: { path: 'runInput.orderId' },
          note: { path: 'nodeOutputs.note.text' },
        },
      },
    ],
    edges: [
      { id: 'e1', from: '$start', to: 'order' },
      { id: 'e2', from: 'order', to: 'note' },
      { id: 'e3', from: 'note', to: 'confirm' },
      { id: 'e4', from: 'confirm', to: '$end' },
    ],
  });

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

  export default defined.value;
  ```

- **Python**

  `flows/process_order.py`

  ```python
  from kindgi import Flow

  process_order = Flow(
      id="acme.process-order",
      version="0.1.0",
      name="Process an order",
      description="Looks up an order, writes a thank-you note, and confirms the order with it.",
      nodes=[
          {
              "id": "order",
              "kind": "tool",
              "ref": "acme.get-order",
              "inputMapping": {"orderId": {"path": "runInput.orderId"}},
          },
          {
              "id": "note",
              "kind": "agent",
              "ref": "acme.note-writer",
              "inputMapping": {
                  "customer": {"path": "nodeOutputs.order.customer"},
                  "total": {"path": "nodeOutputs.order.total"},
              },
          },
          {
              "id": "confirm",
              "kind": "tool",
              "ref": "acme.confirm-order",
              "inputMapping": {
                  "orderId": {"path": "runInput.orderId"},
                  "note": {"path": "nodeOutputs.note.text"},
              },
          },
      ],
      edges=[
          {"id": "e1", "from": "$start", "to": "order"},
          {"id": "e2", "from": "order", "to": "note"},
          {"id": "e3", "from": "note", "to": "confirm"},
          {"id": "e4", "from": "confirm", "to": "$end"},
      ],
  )
  ```

- **Java**

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

  ```java
  package acme.flows;

  import acme.agents.NoteWriter;
  import acme.tools.ConfirmOrder;
  import acme.tools.GetOrder;
  import com.kindgi.pack.Flow;
  import java.util.Map;

  public final class ProcessOrder {
    public static final Flow FLOW = Flow.define("acme.process-order")
        .version("0.1.0")
        .set("name", "Process an order")
        .set("description", "Looks up an order, writes a thank-you note, and confirms the order with it.")
        .node(Map.of(
            "id", "order",
            "kind", "tool",
            "ref", GetOrder.TOOL,
            "inputMapping", Map.of("orderId", Map.of("path", "runInput.orderId"))))
        .node(Map.of(
            "id", "note",
            "kind", "agent",
            "ref", NoteWriter.AGENT,
            "inputMapping", Map.of(
                "customer", Map.of("path", "nodeOutputs.order.customer"),
                "total", Map.of("path", "nodeOutputs.order.total"))))
        .node(Map.of(
            "id", "confirm",
            "kind", "tool",
            "ref", ConfirmOrder.TOOL,
            "inputMapping", Map.of(
                "orderId", Map.of("path", "runInput.orderId"),
                "note", Map.of("path", "nodeOutputs.note.text"))))
        .edge("e1", "$start", "order")
        .edge("e2", "order", "note")
        .edge("e3", "note", "confirm")
        .edge("e4", "confirm", "$end")
        .build();

    private ProcessOrder() {}
  }
  ```

  A step with no `inputMapping` can be shorter: `.toolNode("order", GetOrder.TOOL)` or `.agentNode("note", NoteWriter.AGENT)`.

- **Scala**

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

  ```scala
  package acme.flows

  import acme.agents.NoteWriter
  import acme.tools.{ConfirmOrder, GetOrder}
  import com.kindgi.pack.scaladsl._

  object ProcessOrder {
    val flow: Flow = Flow("acme.process-order")
      .version("0.1.0")
      .set("name", "Process an order")
      .set("description", "Looks up an order, writes a thank-you note, and confirms the order with it.")
      .node(Map(
        "id" -> "order",
        "kind" -> "tool",
        "ref" -> GetOrder.tool,
        "inputMapping" -> Map("orderId" -> Map("path" -> "runInput.orderId"))))
      .node(Map(
        "id" -> "note",
        "kind" -> "agent",
        "ref" -> NoteWriter.agent,
        "inputMapping" -> Map(
          "customer" -> Map("path" -> "nodeOutputs.order.customer"),
          "total" -> Map("path" -> "nodeOutputs.order.total"))))
      .node(Map(
        "id" -> "confirm",
        "kind" -> "tool",
        "ref" -> ConfirmOrder.tool,
        "inputMapping" -> Map(
          "orderId" -> Map("path" -> "runInput.orderId"),
          "note" -> Map("path" -> "nodeOutputs.note.text"))))
      .edge("e1", "$start", "order")
      .edge("e2", "order", "note")
      .edge("e3", "note", "confirm")
      .edge("e4", "confirm", "$end")
      .build()
  }
  ```

* **`nodes`** are the steps. Each has an `id` (unique in the flow), a `kind` and a `ref`: the id of the tool or agent it runs. In Python, Java and Scala, `ref` can also be the tool or agent itself.
* **`edges`** connect them. `$start` and `$end` are where the run enters and leaves; an edge without a condition fires when its source step completes.
* **`inputMapping`** builds each step's input from the run's input (`runInput.…`) and earlier steps' outputs (`nodeOutputs.<step>.…`). See [Pass data between steps](https://docs.kindgi.com/v0.3.1/guides/flows/pass-data-between-steps.md).

The shape is checked when the file loads: ids, edges between steps that exist, `$start` and `$end`, no cycles. That the tools and the agent exist is checked when a run starts: a run of a flow that names a tool your tenant doesn't have is refused before anything runs.

## Run it

With `kindgi dev` running in the pack, start a run from a second terminal:

- **TypeScript**

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

- **Python**

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

- **Java**

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

- **Scala**

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

```json excerpt
{
  "id": "f35d0699-00d8-4e85-944c-e0c9da3ce8e5",
  …
  "flowId": "acme.process-order",
  "flowVersion": "0.1.0",
  "status": "completed",
  "dryRun": false,
  …
  "output": {
    "note": "Thank you for your order of $42.50, ada@example.com!",
    "status": "confirmed",
    "orderId": "A-100"
  },
  …
}
```

The command waits for the run to finish. The flow declares no `output`, so the run returns the output of the step that reached `$end`: here, `acme.confirm-order`'s.

## The agent step

An agent step runs one turn of the agent, as a run of its own (a child of the flow's run). The agent gets the step's input twice: as structured input, which `{{ input.customer }}` in its instructions reads, and as its user message (the input as JSON). [Give an agent its input](https://docs.kindgi.com/v0.3.1/guides/agents/give-an-agent-input.md) has the details.

The step's output has the answer as `text`, and as `output` when the agent declares a typed answer, plus the turn's `runId`. Without a model provider, `dev-echo` answers, and the note is the agent's input echoed back.

## Change it

Save a file and `kindgi dev` loads the new version; the next run uses it, with no restart. A run already in flight keeps the version it started on. Change `version` when what callers send or get back changes, not on every save.

## In the console

A project's **Flows** page lists its flows: "A flow runs agents and tools as its steps: in order, side by side or repeated, all in one run. Each flow is shown at its latest version; retired ones last." Each row has the flow's name and description, its latest version, what it runs ("3 steps: 2 agents, 1 repeating step") and its agent and tool versions ("Locked when published"). A flow whose every version is retired is marked **retired**. The list has a search box, sorts by a column's header, and **Columns** chooses which columns it shows.

![The Flows page: "A flow runs agents and tools as its steps: in order, side by side or repeated, all in one run. Each flow is shown at its latest version; retired ones last.", a search box and the Columns menu, then Refunds ("Triage, check each item, then answer.") at 2.0.0 with 3 steps: 2 agents, 1 repeating step, and acme.single at 1.0.0 with 2 steps: 2 agents, each Locked when published.](https://docs.kindgi.com/v0.3.1/_astro/flows-list.2e9SJHpH_Z2kYrCU.webp)

A flow's page has **Runs** and **Cost** at the top, and **Start run** while a version is in use, only to people who may run the flow. It says which version it shows: "2.0.0 (its latest): 3 steps: 2 agents, 1 repeating step." It draws that version as a graph, from Start to End: each step with what it runs, by name, and at which version, such as "locked at 1.0.0", and a repeating step in words ("For each item of the run's items, at most 50 times: Dev-Echo Agent"). Below it:

- **What 2.0.0 uses:** its agents and tools, each with what it does and its version: "Locked when this version was published: every run of it uses these versions."
- **Versions:** "Runs use the latest unless they name a version." Every version is listed, a retired one marked **retired**, each with what it runs and **Runs →**, the runs list filtered to that version.
- **What triggers it:** the schedules, incoming webhooks and mail triggers that start it, each with the version it starts, such as "Schedule: Every day at 2:00 AM UTC · version 2.0.0" or "Mail trigger: new mail in the company's Gmail, inbox · version 2.0.0". With none, it says "No schedule, incoming webhook or mail trigger: people and apps start it."

![Refunds' page at 2.0.0, its latest: Runs, Cost and Start run; the graph Start, triage, a step repeating for each of the run's items, answer, End, each agent locked at 1.0.0; What 2.0.0 uses: Dev-Echo Agent, locked at 1.0.0; Versions: 2.0.0 (latest) and 1.1.0, each with Runs → and Retire's archive icon at the row's end, and 1.0.0 (retired) with Runs → and Bring back; and What triggers it: the nightly refunds schedule and the Refund requests incoming webhook, each starting version 2.0.0.](https://docs.kindgi.com/v0.3.1/_astro/flow-page.D5zNVdoW_Z11Qvhn.webp)

For flow admins, each version in use has **Retire**: the archive icon at the row's end, with the word only on hover. It asks first, saying what changes:

```text
Retire 1.0.0?
Runs that name no version keep using 2.0.0.
Runs that name 1.0.0 are refused, a schedule's included; runs already going finish. It stays listed here as retired, with Bring back.
```

![Refunds' Retire 1.0.0? dialog: runs that name no version keep using 2.0.0, runs that name 1.0.0 are refused, a schedule's included, and it stays listed as retired, with Bring back; then Keep it and Retire.](https://docs.kindgi.com/v0.3.1/_astro/flow-retire-dialog.08bqDZsK_2wYVz2.webp)

Retiring the latest says which version runs that name none use instead, such as "Runs that name no version use 1.1.0 instead." While the dialog is still open, **Bring it back** undoes it. Later, the version's row has **Bring back**, and the version's own page says "1.0.0 is retired", since when, and "Runs that name it are refused, and runs that name no version don't use it.", with **Bring back**.

![Version 1.0.0's page: "1.0.0 (retired): 2 steps: 2 agents.", then "1.0.0 is retired", since when, that runs naming it are refused and runs naming no version don't use it, and Bring back, above its graph; under Versions, 1.0.0 is marked retired and shown, with Bring back.](https://docs.kindgi.com/v0.3.1/_astro/flow-retired-version.BQvng_Ru_Z2sAnaM.webp)

Retiring a flow's only version in use says "It is the only version in use: the Flows list shows the flow as retired, and runs that name no version are refused until a version is brought back or published." Its page then says "Every version of this flow is retired": "The Flows list shows it as retired, and runs that name no version are refused. Bring a version back under Versions, or publish a new one."

Under `kindgi dev`, Retire and Bring back are off: "the pack's files are this flow's versions. Change the flow there, and kindgi dev reloads it."

## Next

- [Pass data between steps](https://docs.kindgi.com/v0.3.1/guides/flows/pass-data-between-steps.md)
- [Branch a flow](https://docs.kindgi.com/v0.3.1/guides/flows/branch-a-flow.md)
- [Start a run from your app](https://docs.kindgi.com/v0.3.1/guides/runs/start-a-run.md)
