Browse docs

Start here

examplesGetting started with Flowdocumentation

Design

FoundationsLanguage architecturePhilosophy

Language specification

Program checkingConcurrencyDataEffectsResults, Tool problems, and faultsGrammarHistoryModules and importsLanguage specificationEvaluationStandard libraryToolsTypes

Runtime

Runtime architectureThe host boundaryDiagnosticsRunning a programThe history format

Guides

Writing programs that reach checkpointsImplementing Tools with a toolkitLoops that never returnRecursive delegationSharing types between Tool modulesHarnesses over tool registries

The host boundary

This specification fixes what crosses between a Flow runtime and the implementations behind a program's Tools: the value encoding, the Tool process protocol, schemas, declaration identity and fingerprints, the binding lifecycle, the repository configuration file, toolkits, and adapters. Two implementations that agree on this file and its data version interoperate.

It realizes contracts owned above and never redefines them. tools.md owns what a Tool is, which types may cross, and which types decoding may build; errors.md owns ToolProblem and its four cases; data.md owns what each value means; types.md owns the data-type category; history.md owns call ids.

Within the runtime track, execution.md owns how a run is started, paused, resumed and ended across this boundary; trace.md owns how history stores the values encoded here; diagnostics.md owns the diagnostic shape.

One interface, no topology

The protocol is an interface, not a deployment. The same messages and the same rules hold whether an implementation is a function registered in process, a child process, a WebAssembly module, a shared service, or an adapter in front of something else. Flow prescribes none of these and prefers none.

An encoding is required only where an exchange leaves the runtime's address space. An in-process implementation satisfies this file by exchanging the same fields as values.

Effects stay in the checker. Nothing in this protocol says whether a function is !tool, what kind of operation a Tool performs, or what authority its implementation holds.

Serves foundations: Tools are declarations, not implementations; A general core, bounded in scope.

The value encoding

Every Flow data value crossing this boundary, and every value history stores, is JSON under the rules below. Both sides know the declared type, so the encoding carries no type tags of its own.

Flow JSON
Int a number, exact, any size, no fraction or exponent
Decimal a number, exact, normalized
Bool true or false
Text a string
Bytes a string: standard base64 with padding
Duration, Instant a string: ISO 8601
List<T> an array in list order
Set<T> an array in ascending order, no duplicates
Dict<Text, V> an object
Dict<K, V>, other keys an array of [key, value] pairs in ascending key order
tuple an array of its elements; Unit is []
record an object keyed by field name, private fields included
enum constructor, no payload a string: "NotRun"
enum constructor, one positional value {"Failed": value}
enum constructor, several positional values {"Pair": [a, b]}
enum constructor, named payload {"Arrived": {"index": 3, "reply": ...}}
@tool type value the host's token, a string
  • Option in a record field is the one shortcut: a field holding None is left out, and a field holding Some(v) holds v. Anywhere else Option is an ordinary enum, "None" or {"Some": v}.
  • json.Value, an enum std marks for raw JSON encoding (stdlib.md), encodes as the JSON value it stands for, not as a tagged enum, so a program can pass arbitrary JSON through untouched. It is the only such type.
  • A host token is chosen by the host that made it. Flow stores it, compares nothing about it, and hands it back unchanged.
  • Functions and Task values have no encoding. Nothing that holds one can cross (tools.md).
  • Never has no values, so it needs no encoding, and a type that holds it, such as Result<T, Never>, crosses like any other.

The canonical form

One canonical text exists for every value. History stores it, and every hash in this file and in trace.md is taken over it.

  • No whitespace outside strings.
  • Object keys in ascending code-point order; no duplicate keys.
  • Strings escape only ", \ and the control characters U+0000 to U+001F, using \b, \f, \n, \r, \t where one exists and \u00XX otherwise. Every other code point is written as itself in UTF-8.
  • Numbers in plain decimal notation: no exponent, no +, no leading zeros, no trailing zeros after a decimal point, no decimal point when the value is whole, and 0 for zero. Decimal values are already normalized (data.md), so 1.50 and 1.5 share the text 1.5.
  • Instant in UTC with the Z suffix and the fewest fractional-second digits that are exact, on the proleptic Gregorian calendar with no leap seconds, a year outside 0000 to 9999 written with a sign and at least four digits (+10000-01-01T00:00:00Z); Duration as PT seconds S, the seconds a number written as above, with the same rule for the fraction (PT90S, PT0.25S, PT-5S).

Equal values have one canonical text and different values have different ones.

Decoding

Decoding is directed by the declared type. These rules apply to every decoded value: a Tool reply, json.decode, a run's input, and supplied fork arguments. Which types may be decoded at all is tools.md's rule.

Case Rule
A field the type declares is missing A decoding failure, unless the field is an Option
null Only for an Option field, where it means None
A field the type does not declare Ignored in a reply and in json.decode; refused in a run's input
Numbers Any JSON number syntax, read exactly; Int needs a whole value
Text Must be valid JSON; an escape naming a lone surrogate is refused
Duplicate object keys Refused
A Dict with two equal keys Refused
A Set with repeated elements The repeats collapse
An unknown constructor name Refused

Decoding never coerces between types, guesses a constructor, fills a default, or repairs a value. A value outside any limit execution.md sets is refused like any other failure to decode: in a Tool reply, including a reply larger than the host's size limit, that is BadReply, a value the program can handle, never a fault. A number is held to the limits by its value, not its spelling: 1e5000 is an Int past the size limit however few characters it takes, and is refused without being written out.

A decoding failure of a Tool reply is BadReply, with the reason in its text (errors.md). A decoding failure of a run's input refuses the run.

Rebuilding a checkpoint's arguments on resume or fork is not decoding: the program made those values, so every type is rebuilt, opaque types and host tokens included (trace.md).

Serves foundations: Typed, composable crossings; Stable meaning.

Schemas

Every data type has one JSON Schema (draft 2020-12), generated from its declaration. Nothing is written by hand.

  • A record is an object with its fields as properties; every field that is not an Option is required, and additionalProperties is not restricted, since replies may carry extra fields.
  • An enum is a oneOf, one branch per constructor: a const string for a constructor without payload, otherwise an object with that single property.
  • Int is integer, Decimal is number, Text is string, Bool is boolean, Bytes is a string with contentEncoding: "base64", Instant is a string with format: "date-time", and Duration a string with format: "duration".
  • Lists are array with items; sets add uniqueItems; tuples use prefixItems; Dict<Text, V> is an object with additionalProperties, and other dicts an array of two-element prefixItems arrays.
  • Named types go into $defs and are referenced with $ref, so recursive types have finite schemas. Each $defs key is the type's identity, written repository@version/file.Name.
  • /// doc comments become description: a type's on its $defs entry, a field's on its property's schema, a constructor's on its oneOf branch, and a named payload field's on its property's schema inside that branch.
  • json.Value is the empty schema, which accepts any JSON.

A schema describes the encoding; it adds no rule. Where JSON Schema cannot express a rule of the encoding, such as exact number size or sorted set order, the encoding decides.

Serves foundations: Typed, composable crossings.

Identity and fingerprints

A @tool declaration's identity is its repository, the repository's resolved version, the file within it, and its name. A declaration in a file outside any repository has the file's path in place of repository and version.

Its fingerprint is the SHA-256 hash of its canonical signature, written sha256: followed by lowercase hex. The canonical signature is the canonical JSON of:

{ "params":  [[name, schema], ...],      in declaration order
  "ok":      schema of T,
  "err":     schema of E,
  "generic": [type parameter names] }    in declaration order

for a declaration returning Result<T, E>, with every schema stripped of description, and every $defs key written without its @version, so the signature names each type by repository, file and name only. A generic type parameter is written in a schema as {"$param": "T"}.

The fingerprint moves when a parameter is added, removed, renamed, retyped or reordered, when the success or error type changes, or when a type those reach changes its shape. It does not move for a doc comment, for formatting, or for a new version of the repository whose signature is unchanged.

An implementation fits a declaration when their fingerprints match. A fingerprint is an equality check over a signature. It does not show that an implementation is authentic, truthful or the one an author intended.

Serves foundations: Typed, composable crossings; Bounded trust and disclosure.

The Tool process protocol

A process implementation speaks JSON-RPC 2.0 over its standard input and output, one message per line, as MCP and LSP do. There are four messages.

Message Direction Kind Carries
describe host → implementation request the host's data version
implementation → host response its data version and the declarations it implements
call host → implementation notification run, call, tool, args, and schema for a generic Tool
reply implementation → host notification run, call, outcome
cancel host → implementation notification run, call
  • describe comes first. Its response lists each declaration the implementation serves by identity and fingerprint, with its schemas: the result is {"data", "tools"}, each entry {"tool", "fingerprint", "signature"}, where signature has the canonical signature's members with each schema written as Schemas says, descriptions and versions included. Binding goes by the fingerprint; the signature is there to be read. A data version whose major differs from the host's refuses the binding, as a version refusal distinct from any shape error.

  • call names the call by its history id (history.md) and the host's run id. tool is the declaration's identity, and args an object keyed by parameter name holding canonical values. A call to a generic Tool carries the schema of the type the caller expects.

  • reply answers one call. outcome is one of:

    {"Ok": value}         the declared success value
    {"Failed": error}     the declared error value
    {"NotRun": "reason"}  the call definitely did not happen
    {"Unknown": "reason"} the call may or may not have happened

    The runtime decodes value or error against the declared types; a reply that does not decode, one larger than the host's size limit, or a malformed outcome, is admitted as BadReply. BadReply is never sent; only the runtime decides it.

  • cancel is a request to stop work on a call. It proves nothing: it does not establish that work stopped, that nothing happened, or that anything was rolled back. A call cancelled with no reply is closed as the language rule for stopping says (concurrency.md).

Calls are multiplexed. Several may be outstanding, and replies may arrive in any order; arrival order carries no meaning. The pair of run id and call id is the only correlation key. A call a host sends again after a crash (history.md) carries the same pair, so an implementation can recognize a call it has already seen.

A process that ends is started again for the next call sent to it, and answers describe again before that call is sent; a declaration it no longer serves, or no longer fits, makes that call NotRun. A call that was out to it when it ended is Unknown if it reached the process and NotRun if it did not (errors.md).

The host accepts at most one reply per call. A reply for a call that is not open, or a second reply for one call, is dropped and changes nothing in history; the host may log it.

Retries inside one call are the host's and the implementation's business, and invisible to history (history.md).

The protocol carries no callback into the run, no access to its history, and no effect class, authority, credential, approval, budget or retry policy. A host may enforce any of these around a call; none becomes a field here.

Serves foundations: Visible outside influence; Honest uncertainty; Applications remain applications.

Binding

A binding connects a @tool declaration to the implementation that answers its calls.

  • Every Tool a run can reach is bound before it starts. The compiler lists every @tool function a program can reach. A declaration with no binding, or a binding whose fingerprint does not match, refuses the run before anything is recorded (execution.md). Nothing is bound lazily at a first call.
  • A runnable file's own @tool declarations have no repository configuration, so the host must bind them.
  • The host can override any binding, keyed by the declaration's identity. Tests bind fakes; a deployment routes a Tool to a proxy; an embedding application registers implementations in code. There is no namespace of binding name strings.
  • Replay binds nothing and has nothing to dispatch to.
  • Resume may bind different implementations than the run started with, provided each fits. Which implementation served a call is the host's record, not history.
  • A nested run uses the outer run's bindings unless the host overrides them.

Which binding wins, in order: the host's override, then the declaring repository's flow-tool.toml. A declaration covered by neither is unbound.

Binding authenticates nothing. Whether the host runs an implementation automatically, asks first, or isolates it is host policy; execution.md states the reference CLI's.

Serves foundations: Tools are declarations, not implementations; Applications remain applications.

flow-tool.toml

A repository that declares Tools may carry a flow-tool.toml at its root, saying how to run each file's implementations. A runnable file has none: its configuration is the host's.

[tools."openai.flow"]
command = "acme-tools openai"     # a process speaking the protocol above

[tools."search.flow"]
mcp = "npx @acme/search-mcp"      # an MCP server, through the MCP adapter

Each table is keyed by a file path relative to the repository root and names exactly one of command or mcp. An unknown key, or a table naming none or several of them, is a configuration error that refuses the run.

The file is a convention of the reference host and CLI. It carries no credential, isolation, approval or authority setting, and the language reads nothing from it.

Toolkits

A toolkit is a library for one implementation language. It generates that language's types from the @tool declarations and converts values under the encoding above, so an implementation author never writes decoding by hand.

  • Int maps to the language's exact big integer: Python int, JavaScript BigInt, a Rust big-integer type. Narrowing to a fixed width is a checked conversion that the author writes and handles.
  • Decimal maps to an exact decimal type, never to binary floating point.
  • A toolkit answers describe from the declarations it was generated from, so its fingerprints are the declarations' own. It generates from what flow tools lists, not from Flow source.
  • A call whose args do not decode at the declared types is answered NotRun, since the author's code never ran, and a call that names no declaration the toolkit serves likewise. An author's code that stops with an error other than the declared one is answered Unknown, since what it did before it stopped is not known.
  • A generic Tool's values of a type parameter reach the author as JSON, with the schema the call carries for its success type.

Adapters

An adapter lets an implementation that does not speak this protocol answer Flow calls.

  • MCP. The adapter starts the named MCP server, offers each declaration as the MCP tool of the same name, passes args as the tool's arguments, and decodes the structured result against the declared types. A result the adapter cannot map is BadReply.

    The server speaks MCP over its standard input and output. The adapter completes MCP's initialize handshake and lists the server's tools before the run starts, and answers describe from the declarations, as a toolkit does; a declaration the server offers no tool of its name for is unbound. A call is a tools/call request whose id is the call's history id. The structured result is the result's structuredContent or, when it has none, the JSON its one text content block holds; it is decoded against the success type, or against the error type when the result is marked isError. An error response is a result the adapter cannot map. A cancel is MCP's notifications/cancelled, and a server that ends is a process that ends.

An adapter is an implementation like any other: it is bound, fits by fingerprint, and answers with the four outcomes above. It is a host component; the language knows nothing of MCP. Any other service, such as a plain HTTP API, is put behind a process written with a toolkit.

Versioning

The value encoding, this protocol and the history format share one data version. It is exchanged in describe and recorded in every history's Start (trace.md). Within a major version, minors only add; a change of meaning takes a new major.

No encoding here is stable before the first public release (runtime architecture).

Serves foundations: Stable meaning.