Skip to main content
Now that you know what’s being passed (resources, handles), this page covers the dotpath grammar that wires them together.
A ref is a dotpath string wrapped in { "$ref": "…" }. Refs let a Call bind one of its input ports to a value produced elsewhere in the Flow: a declared input, a previous call’s output, or a runtime context value. The grammar recognises three scopes and reserves a handful of prefixes to avoid ambiguity.

The three ref scopes

1. input.<name>

References a flow-level input declared in flow.inputs.
{ "$ref": "input.amountIn" }
Parses to { scope: "input", port: "amountIn" }.

2. context.<key>

References a runtime context value. Only two keys are currently recognised:
KeyMeaning
senderThe transaction signer’s address.
executionAddressThe predicted execution proxy address where the compiled flow runs.
{ "$ref": "context.sender" }
Parses to { scope: "context", key: "sender" }. Any other key under context.* is rejected.

3. <nodeId>.<port>

Any ref whose prefix is not input or context is treated as a reference to another call’s output port. The <nodeId> is the user-defined id you passed when you added the node (e.g. builder.lifi.swap('swap', …) produces refs of the form swap.<port>).
{ "$ref": "swap.amountOut" }
Parses to { scope: "output", node: "swap", port: "amountOut" }.

Reserved prefixes

Three prefixes are reserved and cannot be used as node ids: input, context, and literal. Flow validation rejects a node whose id matches any of them. literal is not a ref scope; it is reserved to keep the grammar unambiguous. Literal values are expressed through a separate mechanism, a LiteralBinding, which lives alongside refs in a node’s bind record:
{
  "bind": {
    "amount": { "kind": "uint256", "value": "1000000000000000000" }
  }
}

How SDK handles become refs

Partner code usually does not construct refs directly. The SDK gives you handles (typed JavaScript values returned by the builder), and converts them to refs when it serialises calls. Handles come in three flavours:
  • InputHandle. Returned by builder.inputs.<name>. Serialises to input.<name>.
  • ResourceInputHandle. A subtype of InputHandle for resource inputs, carrying the resource declaration.
  • OutputHandle. Returned by an op call. builder.lifi.swap('swap', …).amountOut serialises to swap.amountOut (where swap is the user-defined node id you passed as the first argument).
The conversion is automatic. You pass handles into bind and the SDK produces the right $ref string. Context refs are exposed through builder.context:
  • builder.context.sender{ $ref: "context.sender" }
  • builder.context.executionAddress{ $ref: "context.executionAddress" }

Raw refs (escape hatch)

When you need to reference an output of a call you created through builder.untypedOp(...), or a path the typed API does not cover, use raw.ref<T>(path) to get a typed TypedRef accepted by a Bindable<T> slot:
import { raw } from '@lifi/composer-sdk';

builder.untypedOp('custom', 'some.op', {
  bind: { x: { $ref: 'input.token' } },
  config: {},
});

builder.core.asResource('wrapped', {
  bind: { handle: raw.ref<'uint256'>('custom.result') },
  config: { resource: /* ... */ },
});
raw.ref does no runtime validation; the caller is responsible for choosing the right type parameter.

Summary

  • Three scopes exist: input.<name>, context.<sender|executionAddress>, <nodeId>.<port>.
  • <name> and <nodeId> are user-defined: you pick them when you author the flow.
  • literal is a reserved prefix, not a ref scope. Literal values use LiteralBinding, not a ref.
  • SDK handles convert to refs automatically. You almost never write ref strings by hand.
  • Use raw.ref<T>(path) only when the typed API cannot express the ref you need.
Once a Flow is wired, you POST it to /compose. The next page, Execution Model, covers the pipeline.