kortecxdocs

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 handle

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

OperatorMeaningBinding
@tag a tool onto a model handletightest (a handle suffix)
[ ]groupingtightest
>sequential — adds a data edgetighter
&parallel mergelooser
|parallel mergeloosest

& 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 > c

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

ExpressionNodesEdgesShape
a > ba, b0→1sequential
a > b > ca, b, c0→1, 1→2pipeline
a > [b & c]a, b, c0→1, 0→2fan-out
[a & b] > ca, b, c0→2, 1→2fan-in
[a & b] > [c & d]a, b, c, d0→2, 0→3, 1→2, 1→3full join
a > b | ca, b, c0→1> binds tighter than |
a & b > ca, b, c1→2> binds tighter than &
a > b | a > ca, b, c0→1, 0→2reuse 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 wroteError
An empty expression, or an empty group []parse
A dangling @p@, p@@x, @toolparse
A handle that is not in your task mapunknown handle
A cycle or self-loop — a > a, a > b | b > acycle
@ grants on a non-model step — pure@toolgrants 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 --wait

tasks.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."}' --wait
from 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]", …)
PythonString DSL
a >> ba > b
a & ba & b
a | ba | 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.

CombinatorString 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 > review

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

ShapeTopologyHow you get it
Map-reduce[m0 & m1 & …] > reducekx.map_reduce(...) · mapReduce(...) · the DSL
Fan-out / gather[w0 & w1 & …] > gatherkx.fan_out_gather(...) · fanOutGather(...) · kx swarm
Supervisorplanner > [w0 & w1 & …] > gatherkx swarm --pattern supervisor · kx.supervisor(...)
Consensus[v0 & v1 & …] > reducekx swarm --pattern consensus · kx.consensus(...)
Review loopworker > 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]
PatternTopologyKey flags
swarm (default)[a0 & a1 & …] > gather--gather
supervisorplanner > [a0 & a1 & …] > gather--planner, --gather
consensus --vote judge[a0 & a1 & …] > judge--gather steers the judge
consensus --vote majority[a0 & a1 & …] > reducenone — 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?" --wait

Supervisor — 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" --wait

This 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?" --wait
kx 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?" --wait

Blueprints — 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-run

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

Run it.

kx blueprint run --file plan.json --wait

Export 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"}' --wait

The 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:

  1. kx invoke <handle> — run something that already exists.
  2. The chain DSL — compose your own handles into a graph. This page.
  3. 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 20

Newest 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 --wait

This 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 --wait
chain("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.