Branch a flow
This guide puts a condition on an edge, so a step runs only when it holds, and writes the "otherwise" branch.
An edge with a when condition fires only when the condition holds. Two edges
out of one step, with opposite conditions, make an if/else.
Before you start
Section titled “Before you start”- A pack with the tools from Write a flow and
Pass data between steps:
acme.get-order,acme.hold-orderandacme.confirm-order. - A runtime such as
kindgi dev, with Kindgi 0.1's CLI.
Branch on a condition
Section titled “Branch on a condition”Ask your coding agent: "I want a flow that holds an order over 1000 USD and
confirms the rest." Kindgi's skills in your pack tell it how
(How it knows Kindgi). For reference, the
flow looks like this. This guide calls it acme.review-order:
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.review-order', version: '0.1.0', name: 'Review an order', description: 'Holds a large order for review and confirms the rest.', 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: 'confirm', kind: 'tool', ref: 'acme.confirm-order', inputMapping: { orderId: { path: 'runInput.orderId' } }, }, ], edges: [ { id: 'e1', from: '$start', to: 'order' }, { id: 'e2', from: 'order', to: 'hold', when: isLarge }, { id: 'e3', from: 'order', to: 'confirm', when: { op: 'not', child: isLarge } }, { id: 'e4', from: 'hold', to: '$end' }, { id: 'e5', from: 'confirm', to: '$end' }, ],});
if (defined.kind === 'err') throw new Error(defined.error.message);
export default defined.value;from kindgi import Flow
IS_LARGE = { "op": "gt", "left": {"path": "nodeOutputs.order.total"}, "right": {"literal": 1000},}
review_order = Flow( id="acme.review-order", version="0.1.0", name="Review an order", description="Holds a large order for review and confirms the rest.", 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": "confirm", "kind": "tool", "ref": "acme.confirm-order", "inputMapping": {"orderId": {"path": "runInput.orderId"}}, }, ], edges=[ {"id": "e1", "from": "$start", "to": "order"}, {"id": "e2", "from": "order", "to": "hold", "when": IS_LARGE}, {"id": "e3", "from": "order", "to": "confirm", "when": {"op": "not", "child": IS_LARGE}}, {"id": "e4", "from": "hold", "to": "$end"}, {"id": "e5", "from": "confirm", "to": "$end"}, ],)package acme.flows;
import com.kindgi.pack.Flow;import java.util.Map;
public final class ReviewOrder { 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.review-order") .version("0.1.0") .set("name", "Review an order") .set("description", "Holds a large order for review and confirms the rest.") .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", "confirm", "kind", "tool", "ref", "acme.confirm-order", "inputMapping", Map.of("orderId", Map.of("path", "runInput.orderId")))) .edge("e1", "$start", "order") .edge(Map.of("id", "e2", "from", "order", "to", "hold", "when", IS_LARGE)) .edge(Map.of("id", "e3", "from", "order", "to", "confirm", "when", Map.of("op", "not", "child", IS_LARGE))) .edge("e4", "hold", "$end") .edge("e5", "confirm", "$end") .build();
private ReviewOrder() {}}package acme.flows
import com.kindgi.pack.scaladsl._
object ReviewOrder { private val isLarge = Map( "op" -> "gt", "left" -> Map("path" -> "nodeOutputs.order.total"), "right" -> Map("literal" -> 1000))
val flow: Flow = Flow("acme.review-order") .version("0.1.0") .set("name", "Review an order") .set("description", "Holds a large order for review and confirms the rest.") .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" -> "confirm", "kind" -> "tool", "ref" -> "acme.confirm-order", "inputMapping" -> Map("orderId" -> Map("path" -> "runInput.orderId")))) .edge("e1", "$start", "order") .edge(Map("id" -> "e2", "from" -> "order", "to" -> "hold", "when" -> isLarge)) .edge(Map("id" -> "e3", "from" -> "order", "to" -> "confirm", "when" -> Map("op" -> "not", "child" -> isLarge))) .edge("e4", "hold", "$end") .edge("e5", "confirm", "$end") .build()}e2 fires when the order's total is over 1000, and e3 when it isn't. The
condition is defined once and wrapped in not for the other branch.
Run it:
pnpm exec kindgi runs start --flow=acme.review-order --input='{"orderId":"A-200"}'uv run kindgi runs start --flow=acme.review-order --input='{"orderId":"A-200"}'./kindgiw runs start --flow=acme.review-order --input='{"orderId":"A-200"}'./kindgiw runs start --flow=acme.review-order --input='{"orderId":"A-200"}'--flow is the flow, and --input its input. It prints the run:
{ … "flowId": "acme.review-order", "status": "completed", … "output": { "status": "held", "orderId": "A-200" }, …}A-200's total is 1250, so the order is held. With A-100, whose total is
42.50, e3 fires and the order is confirmed.
What the journal records
Section titled “What the journal records”Every edge the run evaluates is in the journal with its decision. A step whose edge didn't fire is skipped and has no entries:
pnpm exec kindgi runs journal <run-id>uv run kindgi runs journal <run-id>./kindgiw runs journal <run-id>./kindgiw runs journal <run-id><run-id> is the run's id. It prints:
{"sequence": 4, "kind": "edge.evaluated", "payload": {"edgeId": "e2", "decision": true}, …}{"sequence": 5, "kind": "edge.evaluated", "payload": {"edgeId": "e3", "decision": false}, …}{"sequence": 6, "kind": "step.started", "nodeId": "hold", "payload": {"input": {"reason": "Over 1000 USD", "orderId": "A-200"}}, …}This shows one entry per line. The command prints them as one JSON document.
Conditions
Section titled “Conditions”A condition is JSON. Each operand is a { path }, with the same roots as
inputMapping, or a { literal }:
| Operator | Shape | True when |
|---|---|---|
eq ne lt lte gt gte |
{ op, left, right } |
the comparison holds. lt and the others compare two numbers or two strings |
in notIn |
{ op, value, set } |
value is, or isn't, an element of the array set |
exists notExists |
{ op, value } |
the path resolves, or doesn't |
truthy falsy |
{ op, value } |
the value is truthy, or falsy. A missing path is falsy |
and or |
{ op, children: [...] } |
all, or any, of the children hold |
not |
{ op, child } |
the child doesn't hold |
Branch on an agent's answer
Section titled “Branch on an agent's answer”To branch on what an agent decided, give the agent a
typed answer and compare one of its fields:
{ path: 'nodeOutputs.<step>.output.<field>' }.
Wait for an approval branches on
nodeOutputs.hold.output.held, a boolean the agent returns.
When no branch fires
Section titled “When no branch fires”A step none of whose incoming edges fired is skipped, and so is every step
only it leads to. If no edge reaches $end, the run still completes, with no
output. Cover every case with your conditions, or bring the branches back
together, as Join branches shows.