Skip to content

Join branches

View as Markdown

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.

  • A pack with the tools and the branch from Branch a flow: acme.get-order and acme.hold-order.
  • A runtime such as kindgi dev, with Kindgi 0.1's CLI.

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). 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:

tools/email-customer/index.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
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;

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:

Terminal window
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"}'

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

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

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:

{"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"}}, …}
Excerpt

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.

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 is shorter.