The node edit panel
Selecting a node on the canvas opens a tabbed edit panel — Settings, Input, Output, State. Settings is the per-kind configuration form and its Update-node action; a fan-in or loop node’s Settings also carry the barrier’s wait knobs (wait mode and the timeout give-up pair) and, where the flow binds states, the opt-in state-refresh toggle. Input and Output are read-only: they render the node’s declared input and output schemas — with the node’s outcomes surfaced on Output — so a node’s contract is readable in build mode, and each shows an empty state when nothing is declared. A scheduled node’s Settings also carry an optional run condition — a jq predicate that gates whether the node runs at all. A false condition skips the node: it does not execute, its output is null, its status reads skipped in Run mode, and the flow continues past it (the gate stops the one node, not the path). The field is absent on a node that runs only as a Recovery, since such a node is dispatched directly and never scheduled. When an agent node carries a structured-output schema, the Output tab renders that schema live and its label reads Output (structured). Editing that schema to remove a field surfaces a non-blocking honesty note on the agent’s Settings: static analysis scans the flow for downstream jq that still reads the removed path off this agent (.upstream_outputs.<agent>.<path>, including the flow-level On Error) and
names the references that would silently start resolving to null — it never blocks the
save (references built dynamically are, honestly, not detected). The State tab is the
node’s state binding — the shared binding editor, one per node — and
its tab label carries a live count of the states the node binds.
The holder panel
A node that calls another flow or an agent — a Flow Call, a custom node, or an agent — is a holder: it starts that call, and can cancel or resume the call’s own parked interactions. Its Settings form is the holder panel, whose controls read top to bottom:- Target. A Flow Call picks its held flow or agent here, from a select scoped to saved flows and agents — never a plain tool. Every load state is drawn: the select disables with a “Loading…” placeholder while the registry is in flight, a load failure shows inline, and an empty target is a loud field error, so a placed-but- unconfigured Flow Call cannot apply. An agent card holds no Target picker — its target is the agent itself.
- Cancel. One jq over the node envelope with
$parkedthat emits null, one parked-ask id, or a list of ids to cancel — a whole-chain cancel of each named call. - Resume. One row per ask of the held call, each an id jq naming the parked ask and a payload jq carrying the answer to resume it with. Leave a row’s payload empty to take the ask’s resolved waiting outcome — a finished run’s result, while a failed outcome raises; a payload jq giving null resumes with null.
- Start. The
start_exprjq that shapes the held call’s input (a holder has no plain expression input): absent starts the held target on the node input, a jq giving null starts nothing, and any other result merges over the resolved node input (a non-dict result wrapped as{input: …}), the expression’s keys winning on collision. A Flow Call nests its warm-start rows inside Start — each names a node of the held flow and a jq whose value pre-fills that node’s output, so the held run skips producing it; a row whose jq gives null runs that node normally. An agent holds no flow graph, so it shows no warm-start rows. - Send the ask up to my caller. A switch that, instead of routing a caller ask to an ask exit, parks it as a nested park and hands it up to this flow’s own caller unchanged.
Ask and expiry controls
A runnable that calls theask tool carries a small ask
face over its own kwargs — the raw JSON stays the single source of truth. To
picks the recipient, user or caller; mode picks sync or async delivery;
an async ask carries a deadline; and On expiry says what the run does when
nobody answers in time — end the run (the default) or resume with the expired
marker. A caller ask always resumes asynchronously, so its Sync option is hidden
and it runs async; an async ask requires a deadline, checked on apply.
A held ask — an ask flow called through a holder — has no On-expiry control at
all. Its expiry is derived from the outcome wiring:
wire the holder’s expired outcome downstream and the ask resumes on expiry so
that branch runs; leave it unwired and expiry ends the held chain. The two never
disagree, because the derivation, not an authored field, owns it.
The test panel’s caller asks
Running a flow in-page against a subject surfaces the caller asks waiting on that subject. The run controls carry a subject input — a target kind, target name, kind, and key — threaded into the run and every interaction call; left empty, no subject is sent. While a run sits suspended with caller asks outstanding, the panel lists each one — its question, its payload, and who asked it — with a payload editor and three actions: Resume the ask with the edited payload, Take its resolved waiting outcome (no payload — a finished run’s result, while a failed outcome raises), or Cancel it. Each action re-reads the run’s outcome: a result ends the loop, fresh asks re-render the list, a re-park on user asks alone ends it with the parked note, and a cancel that started nothing drops only the cancelled ask. A node timeline shows what each holder node did — the calls it started, resumed, took, or cancelled, and any warm-filled node — so a held run’s decisions are readable as it progresses.

The test panel's ask controls: resume, take and cancel.
Schema-aware run results
When you run a flow in-page, the run panel and each node’s run popup render results schema-aware. Where a node declared its output shape — an agent’s structured-output schema, or a preset’s (or the whole flow’s) output schema — the actual result renders as a labeled, typed display keyed off that schema: the schema’s titles as field labels, its descriptions as captions, enum values as chips, nested objects as sections, and a declared array as a list of its typed items. The view stays honest about the data: value keys the schema does not declare are never hidden — they render as an appended raw-JSON remainder — a declared field absent from the value shows a muted dash, and a value that does not match the schema’s shape at the top level falls back to the plain JSON tree with a small note rather than a crash. A per-instance Raw toggle flips the whole view to the plain tree, so the underlying data is never more than one click away, and a flow (or node) that declares no output shape keeps the plain results tree throughout.The Router card
The Router card carries its own edit surface: the kinds it classifies into, optional turn-level output fields that describe the whole classified turn (each a top-level output beside the kinds), the author’s system prompt, a routing mode — every match (the default, firing every matching route) or first match (the first matching route wins) — and a routes table listing every wired route with its flavor, its key, condition, guard, target, and a per-route On Error switch. A route’s target is the edge drawn from its chip on the card, so the table shows it rather than picking it. Under first match the table also orders the rows — the row number is the evaluation priority — and a row reorders by dragging its grip or stepping its move buttons; the table’s advisory notes flag a row an earlier one already covers, without ever blocking the save. Each route’s key names its branch in the saved flow and in traces, and a route’s On Error switch gives that route its own On Error, covering a runnable, agent, flow call, or custom node the route leads to. A segmented Decided by control at the top of the form chooses who judges the kinds — LLM agent (glossed reads, extracts items, can loop) or Classifier model (fast yes/no per kind, with a probability), the two backings the router page describes. The Router entry is offered whenever the host registers either backing tool; on a server with neither it is disabled with the reason Requires the tools_agent or classify tool. Each option is disabled on its own when its tool is missing (Requires the tools_agent tool / Requires the classify tool), and the classifier option is also disabled while Loop, per-kind fields, or turn-level fields are set — the control names what to clear first, so switching backing is never a silent drop. Under the Classifier model backing the form drops the agent-only controls: the system prompt is hidden — a classifier reads state and its questions, not a prompt, and the prompt draft is kept in the session so switching back to the LLM agent restores it — Loop is disabled with Loop needs extracted items. Only the LLM agent extracts them., and each kind’s description field asks When is this kind a yes?. In their place the card shows a Yes when the probability is at least threshold field (0 to 1, step 0.05, default 0.5) and a classifier Model section — a Provider and a Model input, the Provider glossed Leave empty to use the host’s default classifier provider — replacing the LLM provider/model/temperature section. A kind route then fires when its yes-probability reaches the threshold.

The Router form under the Classifier model backing: the Decided by control, the threshold, and the classifier Model section.


A classifier-backed Router on the canvas with its classifier face badge.
noul probability keyed by the kind name — and under the LLM agent
the extracted-item schema with any turn-level fields. As on the Classifier
card, the classifier verdict schema is not saved into the flow, so
downstream reads of its per-kind probability stay untyped.
The system prompt is written one of two ways on the card’s tabbed control:
inline (the Plain text tab) or pointed at a stored template (the Template tab). In
both modes the platform fills a router_kinds render variable with the generated kinds
block — the classify instruction and the kind descriptions.
Inline, the editor appends {{ router_kinds }} after your prompt on save; with a stored
template, the template writes {{ router_kinds }} where the block should appear and supplies
its other variables through its kwargs. A kind’s description is never parsed as template
markup — it rides as a value. A stored template must declare router_kinds in its schema —
the editor refuses one that does not — and the author’s own kwargs may not use the reserved
names router_kinds or _tai_locale. Locale variants do not apply here. A router saved by an
earlier editor version, its kinds block written into the prompt text under a heading, is
re-saved once: open it, trim the stale generated text from the prompt, and save.
The card also carries one Dispatch control, set once for the whole card — Once
(every matched chip continues the flow along its line) or Loop (each chip’s line starts
a body that runs once per classified item, sequentially or in parallel with an optional
max-concurrency cap, or repeatedly while an authored jq condition holds under a max-runs
safety cap). Under Loop the card grows a result chip through which the collected output
of every chip leaves. The chips look and connect the same in both modes; the modes are
documented on the router page.
The classifier card
The Classifier card judges its input against typed questions — the classifier model type as a flow node (see Judging with a classifier). It sits in the palette’s Agentic group between Sub-flow Start and Router, drawn with the Scale icon and a Judge badge, and glossed “Asks typed questions about its input — yes/no, pick one, rate — and returns probabilities.” The entry is enabled only when theclassify tool is registered; on
a server without it, the entry is disabled with the reason Requires the classify
tool.


The Classifier entry in the palette's Agentic group.


The Classifier palette entry disabled when the classify tool is not registered.


The Classifier card on the canvas with its per-question chip band.


A selected Classifier card revealing every question chip.
- Yes/No — two optional one-line inputs, Means yes when… and Means no when….
- Pick one — an options list, each option a label and a description, added and removed in place.
- Rate — an ordered list of levels (two or more), added, removed, and moved to set the rubric’s order.
state key — what the classifier reads. Empty: the whole upstream input is judged.”;
the Template tab points the slot at a stored template by id, with its kwargs edited
beside it. A stored reference lowers onto the runnable verbatim and folds back into the
card on load, so a classify node authored the template-and-kwargs way is a Classifier card
like any other. The Model section is a Provider and a Model text input; the
Provider field’s helper reads “Leave empty to use the host’s default classifier provider”
— so an unset pair runs on the host’s configured classifier.


The Classifier form: the questions table with the three editors, the input expression, and the model.


The Classifier card's Output tab showing the schema generated from its questions.
classify and folds back into the
single card on load. A saved runnable marked as a classifier but no longer calling
classify is shown as a refusal naming the mismatch rather than folded silently,
leaving the plain runnable visible.
The updates half of the binding editor
A node’s state binding applies updates to a bound state after the node runs, edited in the Updates list of the shared binding editor. Each update writes one of two ways:- a template jq the state’s attached templates declare (
purpose: "update"), picked from a select that lists each update program asname — description; the selected program’s declared write paths show beside it as awrites:badge. The run applies its returned op batch — keyed list ops and appends — under the attachment path. Because a named update jq never sees a tool’s field names, an adapter form maps the node’s output and the run input to the update jq’s declared input — one row per declared key, each picking a field, a literal, or a jq, with a write-jq escape hatch for the whole adapter. - a custom jq reading the node’s output as
., with the bound record as$recordand the run input as$input, returning the op batch itself, for a write the templates do not name.
Presets
The palette’s Presets section manages a preset as a node: an edit control opens its flow editor, a + New preset action starts a fresh one, and a delete control raises a guarded confirm that states the blast radius — the node leaves the palette and any flow already embedding it keeps a dangling reference. When a node edit removes an outcome value that wired callers route on, the make-a-node dialog shows an informational warning naming the removed values and how many flows use the node (their routes would go dead) — it never blocks the save. This inform-don’t-block convention is the editor’s house style for changes whose impact lands on callers. The one thing the make-a-node dialog does block on is a genuinely invalid input-schema edit. The make-a-node dialog’s input-schema editor nulls its buffer while an edit is unparseable, and saving then would silently erase the schema — the right outcome for a deliberate clear, the wrong one for a typo. So Save is disabled precisely while the input-schema buffer is invalid (a bad shape, not an empty one), with an inline “fix the input schema before saving” note; a cleared buffer reports valid and saves a null schema as intended. This is a validity gate, distinct from the non-blocking caller-impact warnings above.Message and expression slots
A message slot on the agent form is edited as a Plain / Template tabbed control: Plain is a raw message textarea, and Template picks a stored template with a deep link to the file and a collapsed preview of its first lines, editing its kwargs through a schema-assisted form over the authoritative JSON. A jq expression slot — on the conditional, branch, runnable, On Error, and agent forms — is instead an Inline / Template control: the Inline tab is the visual jq editor’s field (below), and Template is the same picker. In both, the inline value and the template are mutually exclusive — the wire carries one, and switching between them clears the mode being left after a confirm when it holds data. A legacy node that carries both an inline message and a template opens on the Template tab with a rescue note and a one-click “clear inline text” action, so the exactly-one-of rule is fixable in place. A node with a return result may carry a return expression (jq) that shapes the value it contributes to the flow result, and a conditional’s branch may be marked pass-through to inject the conditional’s single upstream output verbatim — which excludes a post-route expression and a return. Settings the form does not model — kwargs an API-authored or newer agent carries — are surfaced read-only so nothing an agent persists is fully invisible, and are round-tripped untouched. A jq field that can reach a bound state — a binding field, a node expression — offers autocomplete from the template jq of the templates attached to that state: eachpurpose: "input" program by name, inserting its tjq_<name>({…})
call handle (tjq_<template>__<name>({…}) when a name is shared across the state’s
attachments).
The visual jq editor
The Inline tab’s field is the visual jq editor: a jq text control with a Visual editor button that opens the canvas in a dialog, where the expression is drawn as a small, editable node graph and written back as jq. The dialog wraps a Test panel (run the jq against a sample seeded to the field’s input shape, plus a server Validate where the engine exposes one), a live readout of the jq the graph currently serialises to, an error summary that jumps to each problem, and a Legend of the node kinds and wire notations. The node vocabulary and its semantic hues — Input, Value, Operator, Condition, Try/Catch, Call Function, Define Function, Comment — are documented with the feature. The editor draws a graph only when it can prove that reading it back gives exactly your expression: a WASM oracle round-trips the graph and adopts it only when the two behave identically. Otherwise the fallback is non-destructive — an expression the editor cannot draw, or one whose graph would read back differently, keeps your text verbatim and shows it as text with a neutral “not shown here” notice; nothing is altered unless you explicitly Start empty. This keeps two questions apart — is the jq valid and can the editor draw it — so only jq that genuinely fails to compile raises the loud “invalid expression” alert, and valid jq the editor cannot represent is never mislabelled as broken. On opening the dialog, a faithful expression is drawn on the canvas; an unfaithful one shows the fallback, and the editable canvas is then reached only through Start empty.State bindings
Flows bind to the platform state store rather than a store of their own, so declaring states, attaching templates, and browsing records all live on the Studio’s States screen. What the flow editor owns is the binding: how each node reads and writes those states. Every node’s edit panel carries a State tab, and its binding is the same editor the platform’s door screens use — shipped from the Studio SDK so a door and a node bind a state the same way. The editor attaches one or more states to the node. Each state card picks the state and one or more templates (a template not yet attached is attached on save), then a Subject expression (a full subject object or a bare key) and an optional Scope predicate that gates the state for the run — the same on every door and node. Below the card, the Inputs list injects a value into the node’s input before it runs — each row apurpose: "input" template jq (or a custom jq
reading the bound record as . with the run input as $input) and the input field it lands in — and the Updates list applies
a write after the node runs (the updates half
above). Every jq field autocompletes from the attached templates’ template jq.
Where the node’s tool is a registered preset that carries its own door binding, the editor
shows an advisory on any state both bind — Overrides the preset’s subject — with the
preset’s own subject shown as the read-only default beside it; the node’s binding wins.
Every control rebuilds from the stored binding: an update’s adapter form recovers each row
from the stored adapter jq, and a state or template the binding names but the server no
longer serves shows a Template not attached. error with the raw jq, never a blank
control. A refused save renders the engine’s message verbatim.
Upload
The flows toolbar carries an Upload action for a flow or node document with akind,
reporting the uploaded kind and name in a status line and prompting when a file carries
no kind. State and state-template documents are uploaded from the
States screen instead.


The flows toolbar Upload action with its result line.

