Skip to main content
A flow is a JSON object. The editor builds it, the backend stores and serves it, and the iOS SDK renders it. This page documents the exact shape so you can read an exported flow, bundle one offline, or debug a resolve response. The schema is the single source of truth for both the editor and the SDK. Its current content version is CURRENT_SCHEMA_VERSION = 9 (see Schema versions).
This is the published and exported shape. The SDK receives it as the flow_schema field of a resolve response. You normally never write this by hand; the editor generates it. Read it when you export a flow, bundle one for offline use, or inspect what the SDK actually received.

Top level: FlowDefinition

The root object.

Nodes

nodes is a discriminated union keyed on kind. Every node extends a base:
Only screen produces UI. condition, assign, and abTest are logic nodes the SDK evaluates to decide the next node. The kinds api, event, and subflow are declared in the schema but are not supported by the current SDKs: they are skipped during rendering. The editor also has no UI to create non-screen nodes today (see Navigation and branching). For A/B testing, use experiments, not abTest nodes.

ScreenNode (kind: "screen")

The only node kind that renders UI. It holds a component tree. ScreenNode.props fields:

ConditionNode (kind: "condition")

If/else branching. Evaluated by the SDK; produces no UI.

AssignNode (kind: "assign")

Sets variables from expressions. Evaluated by the SDK; produces no UI.

AbTestNode (kind: "abTest")

A schema-level split. Most A/B testing is done with experiments instead.

Declared but unsupported nodes

These exist in the type definitions but are not rendered or evaluated by the current iOS or Expo SDKs. Do not rely on them.
  • ApiNode (kind: "api"): a backend/external HTTP call (request, responseMapping, onError).
  • EventNode (kind: "event"): an event-driven entry point (eventType, eventKey).
  • SubflowNode (kind: "subflow"): reuse another flow (subflowId, inputMapping, outputMapping).

Edges (FlowEdge)

Connections between nodes. The editor keeps edges in sync with screen order, so a goNext action follows the default edge to the next screen.

Components

Each ScreenNode.layout is a ComponentNode tree: Props are stored as an untyped Record<string, any>. The intended shape per component kind is documented in the Component reference.

Persistent UI

persistentUI holds three optional zones that wrap every screen. Full coverage is in Persistent UI zones. The shape:

Variables

variables is an array of FlowVariable. Full coverage is in Variables. The shape:

Bounds (min / max)

min and max declare the range a number variable is allowed to hold. They are a guarantee, not a hint: the SDK clamps every write to the range, so the value can never leave it — whichever path does the writing.
With the above, age stays within 0–100 for all of:
  • setVariable number operations (increment, decrement, multiply, divide)
  • assign expressions
  • two-way component bindings (slider, picker, ruler, input)
  • values written by the host app, and progress restored from a previous session
Either bound may be omitted for no limit on that side. An inverted range (min greater than max) is invalid and is ignored rather than pinning the value. Bounds apply only to number variables — for text length use an input’s minLength / maxLength validation instead.
Clamping is silent, with one exception: when a user types an out-of-range number into a number input, the field snaps into range on blur and shows “Must be between X and Y”. Clamping every keystroke would make values below min impossible to type.
min / max are optional additions, so they do not change the schema version — but an SDK build predating them ignores the range. The bound only takes effect once the host app upgrades to an SDK that supports it.

Example

A minimal one-screen flow with a single text and a Continue button:

Notes

  • The SDK decodes leniently. An unknown ComponentType decodes to an internal unknown case and renders as nothing (or a placeholder in debug builds) rather than failing the whole screen. A single malformed child is skipped, not fatal.
  • chrome and chromeOverrides are deprecated. New flows use persistentUI and screenSettings.
  • The integer version here (the content/migration version, up to 9) is not the same as the wire-format schema_version the SDK checks. See Schema versions.