Why a DSL
Everyone who runs agents in production eventually hits the same class of incident:
The flow reaches step seven and the model has quietly "optimized away" a validation; some branch never ran. Two people debug for two days and finally find, deep in the transcript, that a long context in turn three diluted the instructions.
That is not a model-capability problem. You put control flow on a probabilistic channel. Prompts are for judgment, not for process; process needs determinism, and determinism needs a form that can be checked.
Agent Flow's entire position is one sentence:
Whatever a machine can execute deterministically, never let a model improvise.
The three ways you write this today, and what each lacks
Way 1: control flow in the prompt
You have probably seen the symptoms:
- Constraints develop amnesia. Change one word, behavior shifts; ten turns later "validate, then continue" has become "looks fine, continue"
- Un-reviewable. No one can prove a 500-word prompt covers every branch — code review is blind here
- The verdict lives in the model. The prompt says "continue after the check passes" — whether it passed, the model reports. In production that is no verification at all
- Not replayable. Same input, different execution path; when something breaks you cannot reproduce it
Way 2: control flow in YAML / JSON
Feels more "engineered", but it's the same recipe on a different semantics-free text:
- No types: a mistyped field or a drifted shape explodes at runtime
- No shared semantics: every framework invents its own
depends_onand${ref}; the same YAML changes meaning across frameworks - What orchestration actually needs — concurrency budgets, output contracts, verification verdicts, team membership — has no representation, only layer upon layer of field conventions
Way 3: general-purpose code (LangGraph / Semantic Kernel / raw SDKs)
Maximum expressiveness, therefore maximum exposure:
- Workflow is arbitrary code: sandboxing is theater; untrusted distribution is off the table
- "Can this flow concurrently write the same file?" — requires tracing every callback by hand; no static analysis exists
- Concurrency gate, budget, timeout, retry, order restoration… every constraint is a hand-written pattern; miss one line, ship one incident
- A framework's constraints live in convention; a language's live in grammar. The former rots, the latter doesn't
One task, four ways
An ordinary task: process 20 emails concurrently — classify each, draft
replies for the ones that need them. Constraints: concurrency ≤ 4;
total calls ≤ 40; per-call timeout 2 minutes; output must carry id and
category; ids must not repeat.
The prompt version
You are an email assistant. Classify the 20 emails below as urgent /
normal / spam, and draft polite replies for those needing one. Note: do
not process too many at once, keep total calls under 40, spend at most
2 minutes per email, return JSON, every item must include id and
category, ids must not repeat, please strictly follow…
How many turns do those constraints survive? Nobody knows. When an id
duplicates, who intercepts it?
The YAML version
steps:
- id: classify
foreach: ${emails}
concurrency: 4 # Who enforces it? Does it stack with other steps?
output_schema: classified_email # Who validates? And after failure?
- id: draft
when: ${classify.needs_reply} # Does the field exist? When do typos surface?
budget: 40 # Is "budget" this framework's word — or your imagination?
Every line leans on one interpreter's private semantics — switch frameworks, rewrite everything.
The code version
const sem = new Semaphore(4);
let runs = 0;
const seen = new Set();
const out = await Promise.all(emails.map(async (e) => {
if (++runs > 40) throw new Error('budget'); // forgot on the retry path
const r = await withTimeout(call(e), 120_000); // forgot to void late results
if (!r.id || seen.has(r.id)) throw new Error('dup'); // you must remember all this
return r;
}));
// Is the gate fair? Is output order restored? Would you run this in an untrusted sandbox?
Every line is a pattern; miss one line, ship one incident — and nothing statically checks what you missed.
The Agent Flow version
limits {
concurrency: 4
agent_runs: 40
duration: 20m
}
stage classify -> ClassifiedEmail[] {
let items = parallel map input.emails as item limit 4 {
agent(assistant) {
task "Classify one email: urgent, normal or spam"
input { email: item }
tools none
expect ClassifiedEmail // contract: id and category, or it fails
timeout 2m
}
}
require unique(items[*].id)
else fail "ids must stay unique"
return items
}
Concurrency, budget, timeout, contract, uniqueness — every one of them
is checked syntax, not convention. Get any of them wrong and
flow check refuses you with a line number, instead of production
finding out for you.
One bug, four places to die
These failure modes die at different stages depending on how you wrote the flow — the earlier it dies, the cheaper it is:
| Failure mode | Prompt | YAML / JSON | General code | Agent Flow |
|---|---|---|---|---|
| Mistyped field / drifted shape | production incident | runtime error | maybe code review | compile time |
| A step gets skipped | routine | rare | rare | impossible (grammar) |
| A forged "passed" verdict | undetectable | host code's job | code's job | impossible (Runtime verdict) |
| Over budget / unauthorized write | the invoice, later | no representation | hand-written, missed | enforced by limits / write |
Write permissions form a compile-time contract
write accepts only a static path[]. Array literals infer a compatible
common element type, so [input.kb_path] is correctly path[] when the field
is a path; arrays containing text, numbers, or incompatible elements are
rejected at compile time. Passing the type check validates only the request
shape—the runtime still intersects it with host policy.
Why it has to be a language
Because three guarantees only exist at the language level — no framework or convention provides them:
- Grammar: the mistake cannot be written. Referencing a stage
dependency without declaring it,
expect-ing a type that doesn't exist, using a reserved word as a variable — these do not parse. Not a lint suggestion; an impossibility - Types: bad data cannot enter. An agent's output contract = a type = a JSON Schema, enforced by Ajv at runtime with one automatic repair, then a hard failure; every downstream field access was checked at compile time
- Runtime: verdicts and budgets cannot be bypassed.
verification.passedcan only be computed bypass when;agent_runs/duration/tools/writeare gates, not suggestions
The third one needs a controlled execution environment: Agent Flow
compiles to restricted JavaScript running in a subprocess isolate
whose only egress is the host ABI, with JSON-only boundary values.
General-purpose code cannot give you "cannot" — a restricted language
can. This is also the technical foundation of "embeddable and
vendor-free": the entire coupling between language and model is one
interface, AgentRuntime.
Frequent objections
Models keep getting stronger — won't prompts be enough?
Stronger models fix judgment, not reliability. No matter how strong, a model should never own the verdict "I checked" — that's an architecture question, not a capability question. Quite the opposite: the stronger the model, the more it deserves a deterministic skeleton so its capability lands consistently instead of fluctuating with conversation quality.
Another language to learn — isn't that expensive?
Forty-three reserved words and one page of grammar — an afternoon to
read, and the Quickstart gets a first workflow
running in ten minutes. Compare that with the 800-word prompt you
maintain today, versioned as v7_final_final.txt, manually re-tested
after every edit. A DSL isn't added complexity; it's the flow already
living in your head, written into a form a machine can check.
Why not just LangGraph / Semantic Kernel?
They solve "writing orchestration in a general language, comfortably";
Agent Flow solves "orchestration you can trust". Different goals:
general languages are powerful but cannot draw boundaries; a restricted
language gives up Turing completeness and gets back static checking,
sandboxed execution, and gates that cannot be bypassed. They also
compose: write host and integrations in your general language, run the
workflow core as .flow.
How do I integrate an existing system?
The language binds to no model or vendor. The host implements the
AgentRuntime interface (resolveAgent / resolveTeam / invocation)
and injects it — see Packages & Architecture.
Switch models or vendors; the workflow doesn't change a line.
What it is not for (an honest boundary)
- Single-turn Q&A, summaries, translating a passage — call the API directly; no intermediate layer
- Exploratory multi-agent conversations where the process itself is
still evolving — let it settle in conversation first, then freeze it
as
.flow - One-off scripts with no governance or replay requirements — whatever is fastest
The sweet spot: the process is thought through, and what you need is reliable, auditable, replayable execution. Leave the chaos of exploration to conversation; give the settled skeleton to a language — that is exactly what "determinism first" means.
Next steps
- Quickstart: verify the claims above in ten minutes
- Examples: 24 real-world
.flowfiles