Pass data between steps
This guide builds each flow step's input from the run's input, earlier steps' outputs and fixed values, and declares what the run returns.
Each step's inputMapping says where every key of its input comes from. The
flow's output says what the run returns.
Before you start
Section titled “Before you start”- A pack with the tools from Write a flow, such as
acme.get-order, on a runtime such askindgi dev, with Kindgi 0.1's CLI.
This guide adds one more tool. Ask your coding agent: "I want a tool that puts
an order on hold." Kindgi's skills in your pack tell it how
(How it knows Kindgi). For reference, the
tool looks like this. This guide calls it acme.hold-order:
import { defineTool } from '@kindgi/sdk/define';import type { ToolId } from '@kindgi/sdk/types';import { z } from 'zod';
const defined = defineTool({ id: 'acme.hold-order' as ToolId, description: 'Puts an order on hold for a person to review.', version: '0.1.0', input: z.object({ orderId: z.string(), reason: z.string() }), output: z.object({ orderId: z.string(), status: z.literal('held') }), effects: [], // no `mutating: false`: it writes, so a dry run stops before it handler: async ({ orderId }) => ({ orderId, status: 'held' as const }),});
if (defined.kind === 'err') throw new Error(defined.error.message);
export default defined.value;from typing import Literal
from pydantic import BaseModel, Field
from kindgi import tool
class HoldInput(BaseModel): order_id: str = Field(alias="orderId") reason: str
class Held(BaseModel): order_id: str = Field(alias="orderId") status: Literal["held"] = "held"
@tool(id="acme.hold-order") # no mutating=False: it writes, so a dry run stops before itdef hold_order(input: HoldInput) -> Held: """Puts an order on hold for a person to review.""" return Held(orderId=input.order_id)package acme.tools;
import com.fasterxml.jackson.annotation.JsonProperty;import com.kindgi.pack.Tool;
public final class HoldOrder { public record Input(String orderId, String reason) {}
public enum Status { @JsonProperty("held") HELD }
public record Output(String orderId, Status status) {}
public static final Tool<Input, Output> TOOL = Tool.define("acme.hold-order") .description("Puts an order on hold for a person to review.") .version("0.1.0") .input(Input.class) .output(Output.class) // no mutating(false): it writes, so a dry run stops before it .handler((input, ctx) -> new Output(input.orderId(), Status.HELD));
private HoldOrder() {}}package acme.tools
import com.kindgi.pack.scaladsl._
object HoldOrder { final case class Input(orderId: String, reason: String)
enum Status { case held }
final case class Output(orderId: String, status: Status)
val tool: Tool[Input, Output] = Tool[Input, Output]("acme.hold-order") .description("Puts an order on hold for a person to review.") .version("0.1.0") // no readOnly: it writes, so a dry run stops before it .handler((in, _) => Output(in.orderId, Status.held))}Map a step's input
Section titled “Map a step's input”A mapping gives each step exactly the input it needs. Ask your coding agent:
"I want a flow that looks up an order, then puts it on hold while it waits for
stock." For reference, the flow looks like this. acme.hold-for-stock looks
an order up, then holds it with a fixed reason:
import { defineFlow } from '@kindgi/sdk/define';
const defined = defineFlow({ id: 'acme.hold-for-stock', version: '0.1.0', name: 'Hold an order for stock', description: 'Looks up an order and puts it on hold.', 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: 'nodeOutputs.order.orderId' }, reason: { literal: 'Waiting for stock' }, }, }, ], edges: [ { id: 'e1', from: '$start', to: 'order' }, { id: 'e2', from: 'order', to: 'hold' }, { id: 'e3', from: 'hold', to: '$end' }, ], output: { mapping: { orderId: { path: 'runInput.orderId' }, customer: { path: 'nodeOutputs.order.customer' }, status: { path: 'nodeOutputs.hold.status' }, }, schema: { type: 'object', properties: { orderId: { type: 'string' }, customer: { type: 'string' }, status: { type: 'string' }, }, required: ['orderId', 'customer', 'status'], }, },});
if (defined.kind === 'err') throw new Error(defined.error.message);
export default defined.value;from kindgi import Flow
hold_for_stock = Flow( id="acme.hold-for-stock", version="0.1.0", name="Hold an order for stock", description="Looks up an order and puts it on hold.", 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": "nodeOutputs.order.orderId"}, "reason": {"literal": "Waiting for stock"}, }, }, ], edges=[ {"id": "e1", "from": "$start", "to": "order"}, {"id": "e2", "from": "order", "to": "hold"}, {"id": "e3", "from": "hold", "to": "$end"}, ], output={ "mapping": { "orderId": {"path": "runInput.orderId"}, "customer": {"path": "nodeOutputs.order.customer"}, "status": {"path": "nodeOutputs.hold.status"}, }, "schema": { "type": "object", "properties": { "orderId": {"type": "string"}, "customer": {"type": "string"}, "status": {"type": "string"}, }, "required": ["orderId", "customer", "status"], }, },)package acme.flows;
import com.kindgi.pack.Flow;import java.util.List;import java.util.Map;
public final class HoldForStock { public static final Flow FLOW = Flow.define("acme.hold-for-stock") .version("0.1.0") .set("name", "Hold an order for stock") .set("description", "Looks up an order and puts it on hold.") .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", "nodeOutputs.order.orderId"), "reason", Map.of("literal", "Waiting for stock")))) .edge("e1", "$start", "order") .edge("e2", "order", "hold") .edge("e3", "hold", "$end") .set("output", Map.of( "mapping", Map.of( "orderId", Map.of("path", "runInput.orderId"), "customer", Map.of("path", "nodeOutputs.order.customer"), "status", Map.of("path", "nodeOutputs.hold.status")), "schema", Map.of( "type", "object", "properties", Map.of( "orderId", Map.of("type", "string"), "customer", Map.of("type", "string"), "status", Map.of("type", "string")), "required", List.of("orderId", "customer", "status")))) .build();
private HoldForStock() {}}package acme.flows
import com.kindgi.pack.scaladsl._
object HoldForStock { val flow: Flow = Flow("acme.hold-for-stock") .version("0.1.0") .set("name", "Hold an order for stock") .set("description", "Looks up an order and puts it on hold.") .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" -> "nodeOutputs.order.orderId"), "reason" -> Map("literal" -> "Waiting for stock")))) .edge("e1", "$start", "order") .edge("e2", "order", "hold") .edge("e3", "hold", "$end") .set("output", Map( "mapping" -> Map( "orderId" -> Map("path" -> "runInput.orderId"), "customer" -> Map("path" -> "nodeOutputs.order.customer"), "status" -> Map("path" -> "nodeOutputs.hold.status")), "schema" -> Map( "type" -> "object", "properties" -> Map( "orderId" -> Map("type" -> "string"), "customer" -> Map("type" -> "string"), "status" -> Map("type" -> "string")), "required" -> List("orderId", "customer", "status")))) .build()}Each key of inputMapping is a key of the tool's input, and its value is one
of:
{ path: 'runInput.…' }: from the input the run was started with.{ path: 'nodeOutputs.<step>.…' }: from the output of an earlier step, by the step'sid.{ literal: … }: a fixed value, any JSON.
Paths are dot-separated field names: nodeOutputs.order.customer is the
customer field of the order step's output.
In Python, the keys are the tool input's names as they travel: a field's
alias when it has one, else its name. order_id: str = Field(alias="orderId")
travels as orderId.
Run it:
pnpm exec kindgi runs start --flow=acme.hold-for-stock --input='{"orderId":"A-200"}'uv run kindgi runs start --flow=acme.hold-for-stock --input='{"orderId":"A-200"}'./kindgiw runs start --flow=acme.hold-for-stock --input='{"orderId":"A-200"}'./kindgiw runs start --flow=acme.hold-for-stock --input='{"orderId":"A-200"}'--flow is the flow, and --input its input. It prints the run, with the
output the flow declares:
{ … "flowId": "acme.hold-for-stock", "status": "completed", … "output": { "status": "held", "orderId": "A-200", "customer": "grace@example.com" }, …}The journal shows the input each step got, after mapping:
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": 5, "kind": "step.started", "nodeId": "hold", "payload": { "input": { "reason": "Waiting for stock", "orderId": "A-200" } }, … },Without a mapping
Section titled “Without a mapping”A step without inputMapping gets the output of the step before it, or the
run's input after $start. The tool's input schema takes the fields it
declares and drops the rest. So a step that needs exactly what the previous
step returns doesn't need a mapping. A step with several incoming edges needs
one.
When a path doesn't resolve
Section titled “When a path doesn't resolve”A path that doesn't resolve leaves its key out of the input, rather than
setting it to null. If the tool requires that key, the step fails. Mapping
reason from nodeOutputs.order.reason, which acme.get-order doesn't
return, fails the run with:
input-validation-failed: Input for tool "acme.hold-order" failed validationA key that may be missing, because it comes from a branch that may not run,
belongs in the tool's input as optional: .optional() in zod, a default in
pydantic. Join branches has an example.
An agent step's answer
Section titled “An agent step's answer”An agent step's output has the answer as text, and as fields when the agent declares a typed answer:
nodeOutputs.<step>.text: the answer as text.nodeOutputs.<step>.output.<field>: a field of the typed answer. Note the.output:nodeOutputs.<step>.<field>doesn't resolve.
Declare the run's output
Section titled “Declare the run's output”output.mapping is resolved when the run finishes, from the same roots as
inputMapping. With a schema, the result is checked against it, and a run
whose output doesn't match fails. A schema that doesn't compile is refused
when the flow is loaded or deployed, not at each run. If customer were
mapped from a path that doesn't resolve, acme.hold-for-stock's run would
fail with:
output-schema-violation: the run's output does not match the flow's output schema: [{"instancePath":"","schemaPath":"#/required","keyword":"required","params":{"missingProperty":"customer"},"message":"must have required property 'customer'"}]Without output, the run returns the output of the step that reached $end.
Declare output when callers rely on the shape: it stays the same when you
add or reorder steps.