# Join branches

> Bring the branches of a flow back together in a step that runs after whichever of them ran.

This guide brings a flow's branches back together, in a step that runs once after whichever of them ran.

A step with several incoming edges waits until each of them is decided, then runs once if at least one fired.

## Before you start

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

## Join two branches

Ask your coding agent: "I want a flow that holds a large order, then emails the customer either way." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.1.10/start/coding-agents.md)). For reference, the tool and the flow look like this. This guide calls the flow `acme.handle-order`, and it adds one tool, `acme.email-customer`:

- **TypeScript**

  `tools/email-customer/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.email-customer' as ToolId,
    description: 'Emails the customer about their order.',
    version: '0.1.0',
    input: z.object({
      customer: z.string(),
      status: z.string().optional(), // absent when the order wasn't held
    }),
    output: z.object({ to: z.string(), message: z.string() }),
    effects: [],
    handler: async ({ customer, status }) => ({
      to: customer,
      message: status === 'held' ? 'Your order is being reviewed.' : 'Your order is on its way.',
    }),
  });

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

  export default defined.value;
  ```

  `flows/handle-order/index.ts`

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

  const isLarge = {
    op: 'gt',
    left: { path: 'nodeOutputs.order.total' },
    right: { literal: 1000 },
  } as const;

  const defined = defineFlow({
    id: 'acme.handle-order',
    version: '0.1.0',
    name: 'Handle an order',
    description: 'Holds a large order, then tells the customer either way.',
    nodes: [
      {
        id: 'order',
        kind: 'tool',
        ref: 'acme.get-order',
        inputMapping: { orderId: { path: 'runInput.orderId' } },
      },
      {
        id: 'hold',
        kind: 'tool',
        ref: 'acme.hold-order',
        inputMapping: {
          orderId: { path: 'runInput.orderId' },
          reason: { literal: 'Over 1000 USD' },
        },
      },
      {
        id: 'email',
        kind: 'tool',
        ref: 'acme.email-customer',
        inputMapping: {
          customer: { path: 'nodeOutputs.order.customer' },
          status: { path: 'nodeOutputs.hold.status' }, // absent when hold didn't run
        },
      },
    ],
    edges: [
      { id: 'e1', from: '$start', to: 'order' },
      { id: 'e2', from: 'order', to: 'hold', when: isLarge },
      { id: 'e3', from: 'order', to: 'email', when: { op: 'not', child: isLarge } },
      { id: 'e4', from: 'hold', to: 'email' },
      { id: 'e5', from: 'email', to: '$end' },
    ],
  });

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

  export default defined.value;
  ```

- **Python**

  `tools/email_customer.py`

  ```python
  from pydantic import BaseModel

  from kindgi import tool


  class EmailInput(BaseModel):
      customer: str
      status: str | None = None  # absent when the order wasn't held


  class Emailed(BaseModel):
      to: str
      message: str


  @tool(id="acme.email-customer")
  def email_customer(input: EmailInput) -> Emailed:
      """Emails the customer about their order."""
      message = (
          "Your order is being reviewed." if input.status == "held" else "Your order is on its way."
      )
      return Emailed(to=input.customer, message=message)
  ```

  `flows/handle_order.py`

  ```python
  from kindgi import Flow

  IS_LARGE = {
      "op": "gt",
      "left": {"path": "nodeOutputs.order.total"},
      "right": {"literal": 1000},
  }

  handle_order = Flow(
      id="acme.handle-order",
      version="0.1.0",
      name="Handle an order",
      description="Holds a large order, then tells the customer either way.",
      nodes=[
          {
              "id": "order",
              "kind": "tool",
              "ref": "acme.get-order",
              "inputMapping": {"orderId": {"path": "runInput.orderId"}},
          },
          {
              "id": "hold",
              "kind": "tool",
              "ref": "acme.hold-order",
              "inputMapping": {
                  "orderId": {"path": "runInput.orderId"},
                  "reason": {"literal": "Over 1000 USD"},
              },
          },
          {
              "id": "email",
              "kind": "tool",
              "ref": "acme.email-customer",
              "inputMapping": {
                  "customer": {"path": "nodeOutputs.order.customer"},
                  "status": {"path": "nodeOutputs.hold.status"},  # absent when hold didn't run
              },
          },
      ],
      edges=[
          {"id": "e1", "from": "$start", "to": "order"},
          {"id": "e2", "from": "order", "to": "hold", "when": IS_LARGE},
          {"id": "e3", "from": "order", "to": "email", "when": {"op": "not", "child": IS_LARGE}},
          {"id": "e4", "from": "hold", "to": "email"},
          {"id": "e5", "from": "email", "to": "$end"},
      ],
  )
  ```

- **Java**

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

  ```java
  package acme.tools;

  import com.kindgi.pack.Tool;
  import org.jspecify.annotations.Nullable;

  public final class EmailCustomer {
    public record Input(String customer, @Nullable String status) {} // status: absent when the order wasn't held

    public record Output(String to, String message) {}

    public static final Tool<Input, Output> TOOL = Tool.define("acme.email-customer")
        .description("Emails the customer about their order.")
        .version("0.1.0")
        .input(Input.class)
        .output(Output.class)
        .handler((input, ctx) -> new Output(input.customer(),
            "held".equals(input.status()) ? "Your order is being reviewed." : "Your order is on its way."));

    private EmailCustomer() {}
  }
  ```

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

  ```java
  package acme.flows;

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

  public final class HandleOrder {
    private static final Map<String, Object> IS_LARGE = Map.of(
        "op", "gt",
        "left", Map.of("path", "nodeOutputs.order.total"),
        "right", Map.of("literal", 1000));

    public static final Flow FLOW = Flow.define("acme.handle-order")
        .version("0.1.0")
        .set("name", "Handle an order")
        .set("description", "Holds a large order, then tells the customer either way.")
        .node(Map.of("id", "order", "kind", "tool", "ref", "acme.get-order",
            "inputMapping", Map.of("orderId", Map.of("path", "runInput.orderId"))))
        .node(Map.of("id", "hold", "kind", "tool", "ref", "acme.hold-order",
            "inputMapping", Map.of(
                "orderId", Map.of("path", "runInput.orderId"),
                "reason", Map.of("literal", "Over 1000 USD"))))
        .node(Map.of("id", "email", "kind", "tool", "ref", "acme.email-customer",
            "inputMapping", Map.of(
                "customer", Map.of("path", "nodeOutputs.order.customer"),
                "status", Map.of("path", "nodeOutputs.hold.status")))) // absent when hold didn't run
        .edge("e1", "$start", "order")
        .edge(Map.of("id", "e2", "from", "order", "to", "hold", "when", IS_LARGE))
        .edge(Map.of("id", "e3", "from", "order", "to", "email",
            "when", Map.of("op", "not", "child", IS_LARGE)))
        .edge("e4", "hold", "email")
        .edge("e5", "email", "$end")
        .build();

    private HandleOrder() {}
  }
  ```

- **Scala**

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

  ```scala
  package acme.tools

  import com.kindgi.pack.scaladsl._

  object EmailCustomer {
    final case class Input(customer: String, status: Option[String]) // status: absent when the order wasn't held
    final case class Output(to: String, message: String)

    val tool: Tool[Input, Output] = Tool[Input, Output]("acme.email-customer")
      .description("Emails the customer about their order.")
      .version("0.1.0")
      .handler { (in, _) =>
        val message = if (in.status.contains("held")) "Your order is being reviewed." else "Your order is on its way."
        Output(in.customer, message)
      }
  }
  ```

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

  ```scala
  package acme.flows

  import com.kindgi.pack.scaladsl._

  object HandleOrder {
    private val isLarge = Map(
      "op" -> "gt",
      "left" -> Map("path" -> "nodeOutputs.order.total"),
      "right" -> Map("literal" -> 1000))

    val flow: Flow = Flow("acme.handle-order")
      .version("0.1.0")
      .set("name", "Handle an order")
      .set("description", "Holds a large order, then tells the customer either way.")
      .node(Map("id" -> "order", "kind" -> "tool", "ref" -> "acme.get-order",
        "inputMapping" -> Map("orderId" -> Map("path" -> "runInput.orderId"))))
      .node(Map("id" -> "hold", "kind" -> "tool", "ref" -> "acme.hold-order",
        "inputMapping" -> Map(
          "orderId" -> Map("path" -> "runInput.orderId"),
          "reason" -> Map("literal" -> "Over 1000 USD"))))
      .node(Map("id" -> "email", "kind" -> "tool", "ref" -> "acme.email-customer",
        "inputMapping" -> Map(
          "customer" -> Map("path" -> "nodeOutputs.order.customer"),
          "status" -> Map("path" -> "nodeOutputs.hold.status")))) // absent when hold didn't run
      .edge("e1", "$start", "order")
      .edge(Map("id" -> "e2", "from" -> "order", "to" -> "hold", "when" -> isLarge))
      .edge(Map("id" -> "e3", "from" -> "order", "to" -> "email", "when" -> Map("op" -> "not", "child" -> isLarge)))
      .edge("e4", "hold", "email")
      .edge("e5", "email", "$end")
      .build()
  }
  ```

`email` has two incoming edges: `e4` from `hold`, and `e3` straight from `order` when the order isn't large. Whichever path the run takes, `email` runs once, at the end of it.

Run it for a small order and a large one:

- **TypeScript**

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

- **Python**

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

- **Java**

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

- **Scala**

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

`--flow` is the flow, and `--input` its input. The two runs return:

```json
{ "to": "ada@example.com", "message": "Your order is on its way." }
{ "to": "grace@example.com", "message": "Your order is being reviewed." }
```

## The input of a joining step

`email` maps `status` from `nodeOutputs.hold.status`. For `A-100`, `hold` didn't run, so the path doesn't resolve and the key is left out. Its journal, which `kindgi runs journal <run-id>` prints, shows it:

```json excerpt
{"sequence": 4, "kind": "edge.evaluated", "payload": {"edgeId": "e2", "decision": false}, …}
{"sequence": 5, "kind": "edge.evaluated", "payload": {"edgeId": "e3", "decision": true}, …}
{"sequence": 6, "kind": "step.started", "nodeId": "email", "payload": {"input": {"customer": "ada@example.com"}}, …}
```

That's why `status` is optional in `acme.email-customer`'s input. A required key fed by a branch that may not run fails the step whenever that branch doesn't run.

Give a joining step an `inputMapping`. Without one, a step gets the output of the step before it, and a joining step has several: its input is empty.

## Branches that both run

Edges without conditions fire together, so two steps after the same step run at the same time. A step that joins them runs once, after both. To run several tools on the same input and collect their outputs as one, a [fanout step](https://docs.kindgi.com/v0.1.10/guides/flows/run-steps-in-parallel.md) is shorter.

## Next

- [Run steps in parallel](https://docs.kindgi.com/v0.1.10/guides/flows/run-steps-in-parallel.md)
- [Pass data between steps](https://docs.kindgi.com/v0.1.10/guides/flows/pass-data-between-steps.md)
- [Repeat steps in a loop](https://docs.kindgi.com/v0.1.10/guides/flows/loop.md)
