Workflows and Chaining
Compose agents into a workflow with the chain operators, save it as a portable blueprint, and run the swarm, supervisor and consensus multi-agent patterns.
Most useful work is more than one step. You research, then you draft. You ask three agents the same question, then you pick the best answer. A chain is how you say that out loud: a short expression that names the steps and how they connect.
A chain is written with task handles — short names like plan or review — joined by four operators. Each handle points at one step: a model step (an agent that reasons), a pure step (a deterministic transform, no model call), or a tool step. The expression describes the shape only. The runtime still compiles and authorises every step it runs; a chain changes what is proposed, never what is permitted.
The result is a DAG — a directed acyclic graph. "Directed" means edges point one way, parent to child. "Acyclic" means nothing loops back on itself. If you write a loop, authoring fails before anything runs.
The operators
a > b sequential — b reads a's committed output
a & b parallel — both run, no edge between them
a | b parallel — the same thing, looser binding
[ a > b ] grouping — overrides precedence
plan@tool tool grants on a model handleWhitespace does not matter. a>b and a > b are the same expression.
Precedence
Tightest first. This is the part that decides what your expression actually means.
| Operator | Meaning | Binding |
|---|---|---|
@ | tag a tool onto a model handle | tightest (a handle suffix) |
[ ] | grouping | tightest |
> | sequential — adds a data edge | tighter |
& | parallel merge | looser |
| | parallel merge | loosest |
& and | are the same operation — a parallel merge that adds no edges. They sit at two precedence levels so you can express tight and loose grouping without brackets. Use [ ] when you want to be explicit.
This precedence matches Python's native >>, & and |, which is why the Python operator sugar and the string DSL lower to identical output.
A handle used twice is the same node
This is the rule that turns a line of text into a graph. The first time a handle appears it registers a node. Every later appearance refers to that same node — it is not a copy.
a > b | a > cThree nodes (a, b, c) and two edges, both out of a. Reuse is how you build a DAG without drawing one.
Worked examples
Nodes are listed in first-appearance order; the position in that list is the node index. Edges are written parent → child after the canonical sort.
| Expression | Nodes | Edges | Shape |
|---|---|---|---|
a > b | a, b | 0→1 | sequential |
a > b > c | a, b, c | 0→1, 1→2 | pipeline |
a > [b & c] | a, b, c | 0→1, 0→2 | fan-out |
[a & b] > c | a, b, c | 0→2, 1→2 | fan-in |
[a & b] > [c & d] | a, b, c, d | 0→2, 0→3, 1→2, 1→3 | full join |
a > b | c | a, b, c | 0→1 | > binds tighter than | |
a & b > c | a, b, c | 1→2 | > binds tighter than & |
a > b | a > c | a, b, c | 0→1, 0→2 | reuse builds a fan-out |
Read a > [b & c] as fan-out: a runs, then b and c both run on its output. Read [a & b] > c as fan-in: a and b both run, and c waits for both.
The trap is precedence. a > b & c is not a fan-out — it parses as (a > b) & c, so only a → b gets an edge and c is a second, unconnected root. Bracket the parallel side when you mean to fan out.
When authoring fails
Every one of these fails before a run starts, with a named error class:
| What you wrote | Error |
|---|---|
An empty expression, or an empty group [] | parse |
A dangling @ — p@, p@@x, @tool | parse |
| A handle that is not in your task map | unknown handle |
A cycle or self-loop — a > a, a > b | b > a | cycle |
@ grants on a non-model step — pure@tool | grants on a non-model step |
Tasks you define but never use in the expression are ignored.
Running a chain
Each handle in the expression needs a step definition. In the CLI that is a JSON map — from a --tasks file, an inline --tasks-json, or repeated --task name='{…}'. All three merge into one map, and a handle defined twice is an error rather than a silent overwrite.
The step's kind is optional. Leave it out and it is inferred: a prompt or model_id means a model step, a tool_contract alone means a tool step, anything else is a pure step. An explicit kind that disagrees with the fields is an error, not a silent correction.
In the SDKs you do not write those dictionaries by hand. You build each task with a factory — pure() / model() / tool() in Python, task.pure() / task.model() / task.tool() in TypeScript — and pass the resulting handle map to chain().
kx chain run "a > [b & c]" --tasks tasks.json --waittasks.json:
{
"a": { "prompt": "Research the topic." },
"b": { "prompt": "Draft the argument for." },
"c": { "prompt": "Draft the argument against." }
}Or skip the file entirely for a small chain:
kx chain run "plan > review" \
--task plan='{"prompt":"Research the topic."}' \
--task review='{"prompt":"Critique the findings."}' --waitfrom kortecx import KxClient, chain, pure
tasks = {
"a": pure(),
"b": pure(),
"c": pure(),
}
# Fan-out: `a` feeds both `b` and `c` -> edges 0->1, 0->2
spec = chain("a > [b & c]", tasks, seed=0)
with KxClient("http://127.0.0.1:50151") as kx:
result = kx.run_chain(spec, wait=True)
print(result.text)run_chain lowers the chain and submits it. The operator sugar lowers to exactly the same thing, because Python's own precedence already matches the DSL — wrap the expression with Chain.from_node to get the same runnable object:
from kortecx import Chain, pure
a, b, c = pure(), pure(), pure()
spec = Chain.from_node(a >> (b & c)) # identical lowering to chain("a > [b & c]", …)| Python | String DSL |
|---|---|
a >> b | a > b |
a & b | a & b |
a | b | a | b |
(…) | [ … ] |
There is also a fluent builder that fills in the defaults for you:
import kortecx as kx
out = (kx.flow()
.agent("Research the topic")
.then("Critique the findings")
.run())
print(out.text)import { KxClient, chain, task } from "@kortecx/sdk";
const tasks = {
a: task.pure(),
b: task.pure(),
c: task.pure(),
};
// Fan-out: `a` feeds both `b` and `c` -> edges 0->1, 0->2
const spec = chain("a > [b & c]", { tasks, seed: 0 });
const kx = new KxClient("http://127.0.0.1:50151");
const result = await kx.runChain(spec, { wait: true });
console.log(result.text);
kx.close();TypeScript has combinators instead of operator sugar. They build a fragment; chainFrom turns a fragment into a runnable chain. Reusing the same task object reuses the same node.
| Combinator | String DSL |
|---|---|
seq(a, b) | a > b |
par(a, b) | a & b |
group(expr) | [ … ] |
import { chainFrom, group, par, seq, task } from "@kortecx/sdk";
const a = task.pure(), b = task.pure(), c = task.pure();
const spec = chainFrom(seq(a, group(par(b, c)))); // same as chain("a > [b & c]", …)Or the fluent builder:
import { flow } from "@kortecx/sdk";
const out = await flow()
.agent("Research the topic")
.then("Critique the findings")
.run();One lowering, three surfaces
The CLI, Python and TypeScript parse and lower a chain to byte-identical steps and edges, pinned by a shared test corpus. Whichever surface you author in, you get the same graph.
Model steps need a served model
A model step in a chain only runs against a model this runtime is serving. Leave model_id empty and the runtime binds the served model for you. Name a model the runtime is not serving and authoring is refused before anything runs — the runtime never routes a step to a model it does not offer. Pure chains need no served model at all. See Serving.
Tool grants on a model step
A model handle can carry tool tags with @:
plan@web-search@fs-list > reviewplan is one node granted two tools, and review is a downstream step. The tags are order-preserving and de-duplicated, so p@x@x is the same as p@x. The runtime resolves each tagged tool against its live registry and builds that step's authorisation — you never hand it a grant. The tool set is part of the step's identity, so the run replays the same way.
The turn budget for the reason-tool-observe loop rides the task definition, not the @ grammar: max_turns and max_tool_calls. Omit them and the step gets the runtime defaults of 16 turns and 20 tool calls. The ceilings are 32 turns and 20 tool calls, and a value above either is refused at authoring — so turns have room above their default and tool calls do not. The two are independent, because a single turn can fire several tool calls at once. See Tools for what a tool is and how it gets registered.
Reusable shapes
Five shapes cover most of what people build. Each is an ordinary chain — no new step kind, no new wire format — so recovery and delivery behave identically whichever you pick.
| Shape | Topology | How you get it |
|---|---|---|
| Map-reduce | [m0 & m1 & …] > reduce | kx.map_reduce(...) · mapReduce(...) · the DSL |
| Fan-out / gather | [w0 & w1 & …] > gather | kx.fan_out_gather(...) · fanOutGather(...) · kx swarm |
| Supervisor | planner > [w0 & w1 & …] > gather | kx swarm --pattern supervisor · kx.supervisor(...) |
| Consensus | [v0 & v1 & …] > reduce | kx swarm --pattern consensus · kx.consensus(...) |
| Review loop | worker > review > review > … | kx.review_loop(...) · reviewLoop(...) |
Map-reduce and fan-out/gather are the same fan-in family. The difference is intent: map-reduce folds a uniform list of mapper outputs with a deterministic reduce; fan-out/gather collects outputs that differ from each other and merges them.
The review loop is iterative refinement. A worker drafts, then a reviewer reads the previous version — its data-edge parent — and emits a better one, for however many rounds you ask for. The last step's output is the result. This is an author-static loop: you fix the number of rounds up front. A runtime-adaptive "revise until a critic passes" loop is not available today.
If you came from the old Recipes page
map_reduce and fan_out_gather are still real names in the SDKs. The old retry_until_critic, react_tool_loop and image_batch_describe_reduce shapes are not exposed as named builders on the CLI or in the SDKs — the closest reachable forms are the review loop above, the published kx/recipes/react recipe for a steered tool loop, and a fan-in over image-grounded agent steps.
Multi-agent patterns
kx swarm authors the three multi-agent patterns from bare prompts, with no chain expression to hand-write. Each positional argument is one agent's prompt. --goal is appended to every participant's prompt so they all work the same brief.
kx swarm "<agent prompt>"... [--pattern swarm|supervisor|consensus] [--planner <p>]
[--gather <p>] [--vote judge|majority] [--goal <g>] [--seed N] [--wait] [--dry-run]| Pattern | Topology | Key flags |
|---|---|---|
swarm (default) | [a0 & a1 & …] > gather | --gather |
supervisor | planner > [a0 & a1 & …] > gather | --planner, --gather |
consensus --vote judge | [a0 & a1 & …] > judge | --gather steers the judge |
consensus --vote majority | [a0 & a1 & …] > reduce | none — reduced server-side |
Every one of these composes down to an ordinary chain expression before it runs. --dry-run lowers and validates the topology without submitting anything and without needing a running gateway — the offline check that the shape is what you meant.
Swarm — many angles, one answer
N agents run at the same time, each an independent step that commits on its own. A gather step fires once they have all committed and merges their outputs into one answer.
kx swarm "Research the case for" "Research the case against" \
--gather "Synthesize both sides into one balanced brief" \
--goal "Should we adopt durable execution?" --waitSupervisor — plan, delegate, integrate
A lead planner breaks the goal into subtasks. Every worker runs on that plan — the planner's committed output is a data-edge parent of each worker. Then the lead integrates the results.
kx swarm "Research crash-recovery" "Write the briefing" \
--pattern supervisor \
--planner "Plan a briefing on durable execution" \
--goal "Cover exactly-once" --waitThis is a static hierarchy: a fixed team, decided up front. A planner that re-decides team size each round is not available today.
Agents chain inside one app
A supervisor delegates to agents in the same graph. There is no mechanism for one app to call or chain to another app — when you want more moving parts, add more agents to the blueprint, not more apps. See Apps.
Consensus — judge or vote
N voters answer independently, then the graph reduces to one answer. Two reduce modes:
--vote judge(the default) — a model judge reads the candidates and selects the single best one, verbatim. It does not merge them. That is what separates it from a swarm's gather. Steer the judge with--gather.--vote majority— the runtime reduces to the most frequent voter output by exact byte-equality, with ties broken by first appearance. There is no model call and no similarity score. It is a deterministic fold, which makes it a good fit for constrained or classification-style answers, and a poor fit for free prose where no two answers will ever match byte for byte.
kx swarm "Argue for" "Argue against" "Weigh both" \
--pattern consensus --vote judge --goal "Is this design sound?" --waitkx swarm "Answer only yes or no." "Answer only yes or no." "Answer only yes or no." \
--pattern consensus --vote majority --goal "Is the Q3 forecast credible?" --waitBlueprints — save a chain and run it later
A lowered chain can be written out as a blueprint: a plain JSON file holding the exact steps and edges. Save it, keep it in version control, hand it to a teammate, run it again in six months.
Export the chain without running it. --dry-run lowers and validates but does not submit, so this needs no gateway.
kx chain run "a > b" --tasks tasks.json --emit-blueprint plan.json --dry-runCheck it offline. import re-compiles the file and prints the resolved shape. Nothing is submitted and nothing is contacted. Bad kinds, bad edges and bad tool arguments all fail here.
kx blueprint import --file plan.jsonRun it.
kx blueprint run --file plan.json --waitExport then import re-compiles to a byte-identical request. The file pins each step's kind explicitly, but leaves model_id as you authored it — an empty one binds to whatever model the runtime is serving, so the same blueprint stays portable across machines.
The SDKs mirror this: flow.export(path) and Chain.from_blueprint_file(path) in Python, .export(path) and Chain.fromBlueprintFile(path) in TypeScript.
A blueprint file is a document, not a credential
Sharing a blueprint shares a topology. It carries no signature and no publisher identity, and nothing about it makes the steps inside it trusted. Read one before you run it, the same way you would read a script.
Running something already published
If the capability you want already exists as a published blueprint, you do not need to author anything. Invoke it by handle and pass JSON arguments:
kx invoke kx/recipes/echo --args '{"topic":"durable agents"}' --waitThe CLI sends the handle and the raw argument bytes. The runtime resolves, validates, binds and submits — fail-closed at every step. --args-file <path> reads the arguments from a file instead, and --context <handle> attaches a context bundle.
That gives you three rungs to choose from. Take the lowest one that fits:
kx invoke <handle>— run something that already exists.- The chain DSL — compose your own handles into a graph. This page.
kx blueprint run --file— full control over every step and edge.
Run history and re-running
Every run is durable, so you can list what happened and fork any of it.
kx runs list --limit 20Newest first. --before-seq <n> pages back through older runs; pass the lowest sequence number from the previous page.
To re-run something with one input changed:
kx runs rerun <instance-hex16> --set topic="new topic" --set count=5 --waitThis fetches the arguments the original run was submitted with, overlays your --set edits, and re-invokes. A value that parses as JSON keeps its type, so --set count=5 is the number 5; anything else is a string. Only the part of the graph the change actually affects recomputes — the rest is reused. A re-run with no edits returns the existing result rather than doing the work twice.
For what a run emits while it is going, and where a failure ends up, see Observability.
Grounding a chain
Context is attached at the chain level, not to a node. The runtime injects it into the chain's entry steps, so where you write the flag makes no difference.
kx chain run "plan > write" --tasks tasks.json --context team/ctx/spec --waitchain("plan > write", tasks, context=["team/ctx/spec"])chain("plan > write", { tasks, context: ["team/ctx/spec"] });--context is repeatable. See Context bundles for what a bundle is and how to build one.
What holds
- No new primitives. Swarm, supervisor, consensus and the review loop are compositions of the same fan-out, fan-in and sequential edges as every other chain. No new step kind, no new wire format.
- Replayable. Stop a chain part-way and recovery re-derives identical step identities. A tool-bearing step's tool set is part of its identity, so it replays the same way.
- The client proposes, the runtime decides. The runtime compiles and authorises every step. Chains, swarms and blueprints change what is proposed — never what is allowed.
- Single-system by default. A chain runs on one system; local worker concurrency is set by the runtime's worker pool. Multi-node execution is on the roadmap.
Sharing an App
Export a Kortecx App to a single portable .kxapp file, import it under your own account, or clone it locally — and understand exactly what travels and what never does.
Scheduling and Triggers
Make a run start itself — on a clock, when a webhook lands, or on an authenticated gRPC call — with signed inbound requests, replay dedup, and a dry run that rehearses the wiring without firing it.