Skip to main content
A Flow is an immutable JSON document describing a multi-step DeFi operation. Every Flow has three parts: a version header, declared inputs, and an ordered list of op calls (also called nodes). All name and id fields in a Flow (input names, node ids, the flow’s own id) are user-defined. You pick them when authoring; the compiler uses them only to correlate outputs with downstream bindings. Pick short, meaningful names like amountIn, swap, zap. They’re what you read when debugging the wire-format JSON.

Top-level shape

{
  "version": 1,
  "id": "swap-and-zap-weth-to-aave",
  "chainId": 1,
  "inputs": [ /* FlowInput[] */ ],
  "nodes":  [ /* Call[] */ ]
}
The chainId is the EVM chain every call in this Flow targets; inputs whose resource is on a different chain are rejected at build time. inputs declares the named values the flow consumes; nodes is the ordered list of op calls. For the field-by-field reference of every key, see Flow Schema.

Inputs

Each input is either a resource input (a token, with linear consumption) or a handle input (a typed scalar like uint256 or address, copy-mode). Resource inputs carry a balance through the flow. By default exactly one downstream call consumes them. A copy-mode binding can read a resource without consuming it — core.approve, for example, references a resource to approve a spender while leaving the balance available for a later consuming binding. A copy-mode read can coexist with one consuming binding for the same resource. The op manifest’s port metadata determines which binding consumes and which only reads. Handle inputs are non-linear: a uint256 representing a deadline or slippage tolerance can be wired into every op that needs it.

Calls (nodes)

A Call is a single op invocation. The order of nodes is the order the compiler lowers them. Each call has a user-defined id (referenced by downstream calls as <id>.<port>), an op name from the manifest, a bind map wiring its input ports to refs or literal values, op-specific config, and optional guards enforcing invariants. See References for the ref grammar and Guards for what guards do.

Ports and handles

Ops declare typed input and output ports in the manifest. Each port has a type (resource or handle), a mode for resource ports (consuming vs read-only), and an availability (input vs output). The SDK hands you handles for those ports (builder.inputs.amountIn, or the object returned by a previous op call) and converts them to wire-format $ref strings when it serialises the Flow. The validator rejects bindings whose handle type or mode doesn’t match the port. See References for scope rules, reserved prefixes, and handle-to-ref conversion.

Putting it together

The swap-and-zap quickstart produces a Flow that, after builder.build(), looks roughly like:
{
  "version": 1,
  "id": "swap-and-zap-weth-to-aave",
  "chainId": 1,
  "inputs": [
    {
      "name": "amountIn",
      "resource": { "kind": "erc20", "token": "0xC02a…Cc2", "chainId": 1 }
    }
  ],
  "nodes": [
    {
      "id": "swap",
      "op": "lifi.swap",
      "bind":   { "amountIn": { "$ref": "input.amountIn" } },
      "config": { "resourceOut": { "kind": "erc20", "token": "0xA0b8…eB48", "chainId": 1 }, "slippage": 0.03 }
    },
    {
      "id": "zap",
      "op": "lifi.zap",
      "bind":   { "amountIn": { "$ref": "swap.amountOut" } },
      "config": { "resourceOut": { "kind": "erc20", "token": "0x98C2…6F5c", "chainId": 1 } },
      "guards": [ { "kind": "slippage", "port": "amountOut", "bps": 100 } ]
    }
  ]
}
Inputs and node bindings carry resources (token balances) and handles (typed scalars). The next page, Resource Model, covers how those behave at runtime.

See also