Orchestrate
Workflows
A workflow runs several agents together. Where a callable agent is one agent using another as a tool, a workflow is an orchestration you define and can watch.
Workflows need Tracon.Workflows. Without the engine registered, definition
management still works and only the execution endpoints answer 501 — the package
stays optional on purpose.
Five patterns
Section titled “Five patterns”| Kind | What it does |
|---|---|
Sequential |
Agents run in order; each output is the next input |
Concurrent |
Agents run at the same time; results are merged |
Handoff |
One agent starts and hands off when needed — the model decides |
GroupChat |
A manager distributes turns among participants |
Magentic |
A manager plans, tracks progress, and replans; a manager agent is required |
MaxIterations bounds the turn count for Handoff, GroupChat, and Magentic — the
only structural guard against two agents handing off to each other forever. Each
participant call also carries the same two-layer wait limit a callable agent’s
sub-call does (see Agents calling agents):
a cooperative deadline, then a hard cutoff for a participant that ignores it. There
is no per-participant override here — the installation-wide default applies.
new WorkflowDefinition{ Name = "triage", Kind = WorkflowKind.Handoff, AgentNames = ["frontline", "billing", "technical"],}Which fields are required depends on the kind, and the definition is validated when it is saved using the same rules the compiler applies. A shape that could not run is rejected at write time rather than on the first execution.
Function nodes
Section titled “Function nodes”A real pipeline has steps that are not AI calls — a file download, a format
conversion, a database write. AddWorkflowFunction registers one by name:
tracon.AddWorkflowFunction<List<ChatMessage>, List<ChatMessage>>( "word-count", services => (messages, context, cancellationToken) => { var text = messages[^1].Text; return ValueTask.FromResult<List<ChatMessage>>([new(ChatRole.User, $"{text}\n\n({text.Split(' ').Length} words)")]); }, "Appends a word count. Runs no model call.");A Sequential definition’s Nodes list can then mix that name in with catalog
agents, in order:
new WorkflowDefinition{ Name = "summarize-and-count", Kind = WorkflowKind.Sequential, Nodes = [ new WorkflowNodeReference { Name = "summarizer", Kind = WorkflowNodeKind.Agent }, new WorkflowNodeReference { Name = "word-count", Kind = WorkflowNodeKind.Function }, ],}Nodes and AgentNames are mutually exclusive — a definition sets one or the
other. Only Sequential supports function nodes: the ready-made builders for
the other four patterns accept only agents.
The factory passed to AddWorkflowFunction runs once, when the function
registry is built — not once per run. Every workflow compile shares the same
handler closure, so the handler must be thread-safe: a captured counter or
non-thread-safe client needs its own guard.
A function node opens no runs row of its own and contributes nothing to
cost or token totals — it made no model call. ExecutorInvoked /
ExecutorCompleted / ExecutorFailed still fire for it, same as any node, so
it is visible in the run’s event stream and colored live in the graph.
Retry a node on a transient error
Section titled “Retry a node on a transient error”AddWorkflowFunction takes an optional retry policy:
tracon.AddWorkflowFunction<List<ChatMessage>, List<ChatMessage>>( "call-shipping-api", services => (messages, context, cancellationToken) => CallShippingApiAsync(messages, cancellationToken), "Looks up a shipment.", retryPolicy: new WorkflowNodeRetryPolicy { MaxAttempts = 3, InitialDelay = TimeSpan.FromSeconds(1), BackoffMultiplier = 2.0, });Only a transient failure is retried — a provider error, an open circuit breaker, a
rate limit, or a timeout. Anything else (a bad argument, a permanent downstream
error) is thrown on the first attempt, exactly as without a policy. The retry loop
runs entirely inside the node’s own call: Microsoft Agent Framework invokes the
node once per routed message either way, so retrying costs nothing against
TraconWorkflowOptions.MaxSuperSteps — a node that succeeds on its third
attempt still counts as exactly one super-step.
Workflows can also be built in code with MAF’s own builder.
Running one
Section titled “Running one”curl -N -X POST http://localhost:5081/tracon/api/workflows/triage/run \ -H 'Content-Type: application/json' \ -d '{"message":"My invoice is wrong and the app crashes"}'Events stream over SSE. The first frame reports the run id, and every agent invoked
inside the workflow opens its own run row — so the whole thing is readable as a tree
with GET /api/runs/{runId}/tree, with each agent’s tokens and duration attributed
separately.
GET /api/workflows/{name}/graph returns the compiled graph. Node ids are identical
to the executor ids in the run events, which is how the console colours nodes live as
the workflow progresses. The response also carries MAF’s generated Mermaid text.
A workflow run’s error follows the same redaction rule as an agent
run: a
node failure from your own code keeps its message, a failure from a provider,
library, or transport carries only its exception type and a correlation id.
A workflow run ends Canceled only when the run was actually cancelled — the
caller’s request going away, or the workflow’s own MaxDuration elapsing. A node
whose model or tool call never came back ends the run Failed instead, even
though .NET surfaces an HttpClient timeout as a TaskCanceledException. Alert
on the two separately: one is a person stopping, the other is an outage.
A workflow’s root run shares the same call-tree budget every agent run does
(AgentGraph.MaxTotalTokens/MaxTotalCost/MaxDuration — see Reliable
runs): every agent node the
workflow invokes spends against the same tree, so a workflow with a long chain
of agent steps is bounded the same way a single agent’s own tool loop is.
Checkpoints
Section titled “Checkpoints”A workflow writes checkpoints as it goes, controlled by
Tracon:Workflows:EnableCheckpointing (default true).
GET /api/workflows/runs/{runId}/checkpoints lists them and
POST /api/workflows/runs/{runId}/resume continues from one — omit the id to resume
from the latest.
Resuming opens a new run. The original is never rewritten, so “what happened, then what we did about it” stays two readable records rather than one edited one.
Checkpoints survive a process restart only with a SQL provider registered
(UsePostgreSql(), UseSqlServer(), or UseSqlite()). The in-memory store keeps at
most 50 checkpoints per session and drops the oldest — enough for local development,
not for a workflow you expect to resume after a restart. They are also a retention
target, so an old run may have none left even on durable storage.
Turning off EnableCheckpointing does not silently disable resumption: a workflow
that stops to wait for a human answer fails outright instead of hanging with no way
to resume.
A checkpoint’s state is the Microsoft Agent Framework’s own serialized graph; Tracon does not interpret it and makes no promise that a checkpoint written by one Microsoft Agent Framework version can be resumed by a different one. See Versions and upgrades for the compatibility policy.
tracon state-check counts stored checkpoints by the Tracon envelope
generation stamped on them and says which of those the build running the
command understands. That is as far as it can go for a checkpoint: the state
inside has no decoder outside a running workflow, so the command reports a
sampled checkpoint as checked for structure only and never claims to have
resumed it.
Asking a human
Section titled “Asking a human”A workflow can stop and wait for input:
flowchart LR
accTitle: Durable workflow input cycle
accDescr: A workflow request closes the run as awaiting input, writes a checkpoint, then resumes from that checkpoint in a new run after a response.
RUN["run"] --> ASK["executor raises a request"]
ASK --> WAIT["run closes as AwaitingInput<br/>a checkpoint is written"]
WAIT --> LIST["GET .../requests"]
LIST --> RESP["POST .../respond"]
RESP --> NEW["a NEW run resumes from the checkpoint<br/>events stream over SSE"]
Requests are read from the run’s own event stream — there is no separate table — and
only a run in AwaitingInput has any. The answer is matched to the request that is
re-published with the same id when execution resumes.
This is the same append-only shape as tool approvals: the waiting run stays as it was, and the response opens a new one.
Read next
Section titled “Read next”- Runs and recording — reading the tree
- Evaluation and experiments — compare agent versions using cases, judges, and experiments.