Skip to content

Write a flow

View as Markdown

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.

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

tools/get-order/index.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
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;

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:

agents/note-writer/index.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;

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:

flows/process-order/index.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;
  • 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.

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 workspace doesn't have is refused before anything runs.

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

Terminal window
pnpm exec kindgi runs start --flow=acme.process-order --input='{"orderId":"A-100"}'
{
"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"
},
…
}
Excerpt

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.

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

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.

A project's Flows page lists its flows: "A flow runs agents and tools as the steps of one run: in sequence, in a loop, or as tool calls in parallel. Each step can take the output of the steps before it. A step can't run another flow yet." Each row has the flow's name, with its description under it, its Latest version, its Steps ("3 (2 agents, 1 repeating step)") and Versions locked: Yes, No, latest at each run or Nothing to lock, with what that means on hover. In a later environment of an environment order, the version column is Newest you can see: the newest version in any of the order's projects you can see, which may not be promoted there yet. The flow's page shows the version that environment serves. A flow whose every version is retired is marked retired, and is listed only under Show retired, in the Columns menu, which counts them; it then comes after the flows in use. 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 the steps of one run: in sequence, in a loop, or as tool calls in parallel. Each step can take the output of the steps before it. A step can't run another flow yet.", a search box and the Columns menu, then Refunds ("Triage, check each item, then answer.") at 2.0.0 with 3 (2 agents, 1 repeating step), and acme.single at 1.0.0 with 2 (2 agents), each with Versions locked: Yes.

A flow's page has its name and description, with Download its JSON and Start a run as icons at the top. Start a run shows while a version is in use, to people who may run the flow. Its pages are Overview, Runs and Corrections. The breadcrumbs at the top end on the one you're on, such as Flows › Refunds › Overview ▾, and its ▾ lists the others.

Overview starts with the version it shows: its ID, its Version, tagged Latest for the newest ("The newest version: runs use it unless they name another.") or Retired, with "Latest is 2.0.0", a link, when it shows another, and Versions locked. Steps 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:

  • Agents and tools: each agent and tool it runs, with its Name, Kind, What it does and Version: the version it runs, or "Latest at each run" when it isn't locked.
  • 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". A trigger that names no version starts "the latest version", and a paused one is marked Paused. With none, it says "No schedule, incoming webhook or mail trigger: people and apps start it."
  • Recent runs: its latest 10 runs in the project, at any version, each with when it started, its status, its version and how long it took. Cost → opens the project's costs filtered to the flow, and All runs → its Runs page.
  • Versions: every version, each with what it does, its steps, what it's Locked by, such as a schedule that starts it, and Runs →, its Runs page filtered to that version. A retired version is listed only under Show retired. The version on the page always shows, and so does every version of a flow with none in use.
  • Full spec: the version's definition, as the runtime sent it.

A version with a flow inside it says "A run of 1.0.0 is refused": "It has a flow inside it, which runs can't do yet. Put that flow's steps in this one instead."

Runs lists the flow's runs as a project's Runs page does, and Corrections has what people said about them.

Refunds' page at 2.0.0: Download its JSON and Start a run at the top; ID acme.refunds, Version 2.0.0 with its Latest tag, Versions locked: Yes; the steps Start, triage, a step repeating for each of the run's items at most 50 times, answer, End, each agent locked at 1.0.0; Agents and tools: Dev-Echo Agent at 1.0.0; What triggers it: the nightly refunds schedule and the Refund requests webhook, each at 2.0.0; Recent runs, none yet; Versions, with 2.0.0 locked by both; and Full spec.

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:

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 under Show retired, with Bring back.

Retiring the latest says which version runs that name none use instead, such as "Runs that name no version use 1.1.0 instead." Once it's done, the dialog says "Retired.", and Bring it back undoes it while the dialog is still open. Later, under Show retired, 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: the version, 1.0.0, marked Retired, with "Latest is 2.0.0"; 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 of two agent steps; under Versions, 1.0.0 is marked Retired and This version, with Bring back.

Retiring a flow's only version in use says "It is the only version in use: the Flows list shows the flow only under Show 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 only under Show 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."