Quester Studio

How flows work

How nodes connect, what edges carry, and how wire data differs from {{input}} / {{nodes}}

A flow is a directed graph of nodes. Edges decide run order. The JSON that travels across an edge is the wire (execute input of the next node). Templates like {{input.*}} and {{nodes.id.*}} are a separate layer.

Quick picture — how data moves

Labels on the arrows show what JSON the next node receives as execute input.

Data moving along start → input → http → extract start entry {} input run payload { productId } http URL uses {{input.productId}} { status, body… } extract JMESPath body.title "Mascara" … Wire = previous output. extract / assert / json read it with JMESPath — not mustache.
Same shape as demo-main-nodes.flow.json. Later strings can also use {{nodes.fetchProduct.body.title}}.

How nodes connect

Sketches with a red Fails validation badge show an invalid graph. Those cases break quester validate and desktop save checks.

Exactly one start

Required. No incoming edges. At most one outgoing edge. This sketch is valid — zero starts, two starts, or http → start fail validation.

Executable nodes must be reachable from start

Orphan steps have no path from start. They fail validation (except note stickies).

Fails validation
No cycles

The graph must be a DAG. a → b → a (or any longer loop) fails validation.

Fails validation
note stays offline

No handles. Any edge to or from a note is invalid. Notes may sit disconnected.

Fails validation
Edges need real node ids

Each edge’s source and target must equal an existing node id in that flow. Typos, renamed nodes, and deleted ids all fail (same for a bad source).

Fails validation
Duplicate node ids

Every node id in a flow must be unique.

Fails validation

Ports (handles) and edges

Desktop shows target (in) and source (out) ports. Branch nodes expose named source handles.

Linear step

In ×1 · out ×1. Fan-out allowed — several children get the same wire.

start

In ×0 · out ×1 · max one outgoing edge.

output

In ×1 · out ×0. Terminal for that path.

if

In ×1 · two named outs on the node header. Engine follows matching sourceHandle.

try / foreach frames

Header in + outer continuation handles. Inner body uses entry → child → exit on the inner border — not the same as if.

switch

In ×1 · one named handle per case + default. Only the matching case runs.

note

In ×0 · out ×0 · no edges.

fan-out — every child runs http 1st assert 2nd extract same wire JSON branch — one path only if true · runs A false · skipped B
Fan-out: both children run, one after another (not in parallel), in the order edges are listed in the flow file — here assert then extract. Each gets the same http response as execute input.
Branch: only the matching handle runs (here true → A); the other edge is skipped.

One incoming edge per node, except join which accepts N. For diamonds or reconvergence after branches, insert a join (collect-map of predecessor outputs). Deep-merge named bags with merge (still one wire in).

After if/switch/try branches — reconverge with join (N inputs → 1 collect-map) a b c join next { b: …, c: … } true arm false arm
Exclusive branches skip untaken arms. Diamond fan-out waits for both arms. Use merge when you need deep-merge of named bags on a single wire instead.

Framed containers (try / foreach)

try and foreach are framed subgraphs on the canvas. Outside wires attach only to the frame (in, then success/failed or complete). Inside, wire one path: entry → body children → exit. Body nodes use parentId pointing at the frame.

Both frame types may nest inside each other (sample: nested-frames.flow.json). See the frame diagrams on the try and foreach pages.

Mid-flow forms

form nodes pause the run until values are submitted (desktop UI) or supplied via CLI --forms. Unlike the input node (run payload once at the start), a flow may include multiple form steps.

search → pick → confirm (each form node pauses the run) http search form await pick http detail form await confirm http cart {{nodes.pickForm.productId}} {{nodes.detailForm.quantity}}
Bind reusable form inputs with bindings (e.g. products: "{{nodes.search.body.products}}"). Later steps read {{nodes.<formNodeId>.fieldId}}. Author forms under forms/*.form.json — see Workspace files.

Wire data vs templates

When http finishes, the next node’s execute input is already the response object.

Wire / JMESPath

On extract, assert, json, if checks: body.title, status, products[0].id.

Named node template

Any later string field, by node id.

Run input template

Run panel / CLI --input — not the same as the input node.

Env / secrets / vars

{{env.API_BASE}}, {{secrets.TOKEN}}, {{vars.token}}.

There is no {{previous.*}} mustache scope. Previous output is already the wire.

Three meanings of “input”

Execute input

JSON on the wire into execute() (previous node output).

Run input

Object from Run panel / CLI --input.

input node

Copies run input onto the wire for the next step. Optional if you only need {{input.field}}.

How a step runs

  1. Resolve templates in that node’s string fields.
  2. Call execute(wireJson, context).
  3. Store the return value as {{nodes.<id>}}.
  4. Enqueue next edges (all for linear fan-out; filtered by handle for branches).

Cheat sheet

I need…Use
Field from the last HTTP response in extractbody.id / products[0]
Same field later in a string{{nodes.httpId.body.id}}
Run panel field{{input.field}}
Env / secret / set var{{env.*}} / {{secrets.*}} / {{vars.*}}
Sticky text on canvasnote (not executed)
Mid-flow user inputform (pauses until submit)
Reconverge branch armsjoin