Skip to main content
The Babelfish Flows page is the flow engine’s own editor in the Studio, contributed by the Babelfish Flows plugin (see the Studio screens for how a plugin contributes a page). The page is a browser for your saved flows that mirrors the Tools screen’s organization: it navigates folders with a breadcrumb, toggles between a list and a card view (the choice persists per browser), and filters the list with a tag filter and a search box. Each flow row edits its display name, tags, and folder in place; hide management stays centralized on the Tools screen. The list carries an Attachments column — one badge per kind of thing that references the flow (a schedule, a hook, a route, an extension, or a bound state) — reserved at a fixed width from first paint across its loading, empty (a muted em dash), and populated (the kind chips) states, so a resolved attachment index never re-lays out the list. The whole view — the open flow, the current folder, dialogs, and the active filters — is derived from the URL, so a flow is bookmarkable and the back button works; opening a link to a flow that no longer exists renders a loud not-found card, never a blank editor.

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. Changes in the panel stay a draft until Update node applies them. Closing the panel, opening another node, switching mode, leaving the editor, an undo or redo that changes the open node, or importing a flow file over the canvas while the panel holds a draft first asks Unapplied changes: Keep editing, Discard changes (the node keeps its applied values), or Apply changes (the step then goes on, and a flow left unsaved by the apply asks to be saved before the editor is left). Browser and shell navigation ask the same way. Picking a tool in a node’s panel — a Runnable, an On Error, or a router route’s On Error — that replaces a bound tool clears the kwargs written for the bound tool, broken JSON included, since they name that tool’s arguments; the Expression is the node’s input, not the tool’s, so it is kept. Picking the bound tool again changes nothing, and kwargs typed while no tool is bound are kept. A Map node’s own tool picker clears its per-item kwargs the same way. The pick then fills the node’s empty kwargs from the tool schema’s root default and its empty Expression from the schema’s x-tai42-default-expr, once that tool’s schema has loaded; slots that already hold text are left as they are. The cleared and filled values are part of the pick: Discard changes reverts them with the tool, Apply changes commits them with it. A pick applied before its tool’s schema has loaded is committed with the kwargs cleared but nothing filled, and nothing is filled when the schema arrives later. Opening a node never changes it: the panel shows exactly what the node holds. Every tool picker in the panel, a route’s inline On Error included, shows each tool’s declared description as a quieter line under its name, and its search box finds a tool by that description as well as by its name. A failed read of the descriptions shows one notice in the form, “Could not load tool descriptions:” and the error, with Retry; the pickers keep working from the tools’ names, and a picker with a search box adds “The tool search matches tool names only.” The Input tab also carries a Receives from block — resolved by the engine after edge transforms — resolved live off the canvas as you edit: the inputs the engine works out a node will actually receive at run time, which are its declared inputs plus the ones its incoming edges add. Each is a chip, solid for a declared input and outlined (titled from an edge) for one an edge derives, so what reaches a node beyond what it declares is visible at a glance. The block draws its own loading and empty states and surfaces a loud alert when the canvas cannot be converted to a graph or the engine cannot resolve it, rather than showing stale chips.
The Input tab of a loop node's edit panel: a Receives from block, resolved by the engine after edge transforms, with a solid seed chip for the declared input and an outlined producer chip for the edge-derived input, above an input-schema panel reading that no input is declared and the node derives its input from incoming edges.The Input tab of a loop node's edit panel: a Receives from block, resolved by the engine after edge transforms, with a solid seed chip for the declared input and an outlined producer chip for the edge-derived input, above an input-schema panel reading that no input is declared and the node derives its input from incoming edges.

A loop node's Input tab: the Receives from block, its declared input solid and its edge-derived input outlined, above the node's declared input schema.

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. A runnable’s Settings also carry Run after the reply — the after_reply toggle, “Call the tool after the reply”. With it on, the node’s tool is called after the run has answered; in Run mode the node settles with a clock glyph labelled Node call runs after the reply and a muted ring, and its run popup shows the status deferred.
A finished run on the canvas: the runnables warmup and answer settled with a success check, and the runnable later, set to run after the reply, settled with the clock glyph and a muted ring.A finished run on the canvas: the runnables warmup and answer settled with a success check, and the runnable later, set to run after the reply, settled with the clock glyph and a muted ring.

A finished run: the node set to run after the reply settles with the clock glyph and a muted ring.

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, every registered agent and the presets over them — never a plain tool; each option is labelled with its kind ((flow) or (agent)). 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. While the agents list is still loading the flows are offered and the agents are not yet listed; if it fails to load, the panel says so under the field — “Could not load the agents list, so agents cannot be picked as a target” — the flows stay selectable, and a holder already holding an agent keeps its target shown. Picking tools_agent (or a preset over it) turns the node into an Agent card; any other agent stays a holder with this panel. An agent card holds no Target picker — its target is the agent itself.
  • Cancel. One jq over the node envelope with $parked that 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_expr jq 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.
Every jq slot reads through the same Inline / Template control the other editor jqs use, so a stored template id shows and survives an open-edit-save. The Agent card over a saved agent. An Agent card that holds a saved agent preset reads that preset and shows, read-only under Preset settings, everything the preset sets: system message, provider, model (or the host default, when the preset fixes model settings without a model), temperature and every other model setting, structured output (a fixed response_format, or the preset’s output_schema forced in as one), the output map that reshapes the result, the input schema that replaces the agent’s own arguments, and every other fixed argument by name. A setting the preset fixes is hidden from the card’s own fields, which sit under Changes on top of the preset (or The card’s input to the preset for a preset with its own input schema); a field the preset leaves open reads “Host default (the preset sets none)”. Every argument the card would send that the preset fixes (its tools and sub-flows included) is flagged, since the run refuses such a call, with a Clear <setting> button where the card’s own field holds the value. Clear Model settings clears the model, the temperature and every other stored model setting. An argument the card carries without a field of its own reads Carried argument “<key>”, and its clear drops just that key, keeping the other carried arguments. While the preset list loads the card says so; a failed read of the list or of the preset shows the error with Retry, and a failed reload of the list shows that error even while an earlier list is still held, rather than saying the preset is not in the list. The Start template and the prompt. The engine merges the Start result over the card’s own settings, its keys winning. So while a Start template is chosen (and the preset does not fix the prompt), the card says under System message: “The Start template “<id>” shapes this agent’s start input: a system_message it gives is the agent’s prompt, in place of the one set here.”, followed by an Open in Storage link to the template. The ask exit. When the holder’s target can ask its caller and the send-up switch is off, the card grows an extra exit handle labelled ask on its outcome edge — the node a caller ask routes to. Unwired it renders as a dashed ring and the save gate blocks with a “wire the ask exit” message; wired it is a solid ring. A holder that can ask its caller needs either this exit or the send-up switch, or a caller ask has nowhere to go.

Ask and expiry controls

A runnable that calls the ask 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 cancel ends the whole run the ask belongs to, and nothing is sent to the person asked: the run’s other asks close with it, and acting on one of them shows That ask is no longer waiting — reload the run. 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 caller-ask controls: resume, take and cancel on each waiting ask.The test panel's caller-ask controls: resume, take and cancel on each waiting ask.

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 run panel’s Results shows the run’s returned result — the result envelope’s result, read from the run’s record once the run ends (or from the resolving caller-ask outcome) — with the missing_results surfaces named under “Not in the result”. An empty result says plainly that no step or branch has Return result turned on. Nothing is said while the run is launching, waits on an ask, or its record has not settled; a reopened run that is still running reads “No result yet: this run is still running.”. A run that paused on an ask stays watched while it waits: the panel keeps reading its status, so a run continued from outside the panel (an answer given elsewhere, an expiry) is seen running again and then ended. The parked line shows the deadline of the park the status names, so a run that paused again on a later ask shows that ask’s deadline, and the View full trace link shows while the run is paused as well as once it is over. Once such a run ends, its run record still holds only the pause, so Results reads the run’s final result with the flow_result tool. A run that ended in an error shows no Results: its status shows the error. A run whose state is gone reads “This run paused for an answer, and its state is no longer available, so no result can be read.”. A failed read shows “Could not read the result:” and the error, with Retry. A stored value the Studio cannot read as a result names where it breaks, in the form “The run’s stored result is in a format this studio cannot read, so it is not shown. Details: <path>: <message>”.

Values the run state does not keep

A run keeps of each node’s inputs and outputs only what a later step reads (see Run state). A node’s run popup shows each section from the first place that has it:
  • Kept in the run state — the node’s newest value, shown directly.
  • Recorded by a monitoring plugin — the popup reads the value from the step that recorded it: a muted Loading this step’s recorded data… line with a spinner, then the value exactly as a kept one. When the value cannot be read yet, Could not load this step’s recorded data: and the reason show with a Retry button.
  • Not written for this node — the section is hidden.
With no monitoring plugin, a popup missing a value says once, under its sections: Values that no later step reads are not kept in the run state. A monitoring plugin records every step’s input and output.
A node's run popup showing its output and the note that values no later step reads are not kept in the run state.A node's run popup showing its output and the note that values no later step reads are not kept in the run state.

A result node's popup: its kept output, and the note for the input the run state does not keep.

A node's run popup showing Could not load this step's recorded data: Run span not found, with a Retry button.A node's run popup showing Could not load this step's recorded data: Run span not found, with a Retry button.

A node's popup opened before its step was ingested: the read's error with Retry.

A node's run popup showing its input and output loaded from the recorded step, with no note.A node's run popup showing its input and output loaded from the recorded step, with no note.

With a monitoring plugin: the input and output read from the step that recorded them, shown as kept values.

A node inside a loop body shows one row per iteration — from the running loop’s results, the loop’s published output, or, with a monitoring plugin, the recorded body steps under each finished iteration (rows of a nested loop’s node read Iteration 0 › 1). Before any iteration finishes the popup says No iteration of this loop has finished yet.; a node that ran in none says This node did not run in any finished iteration.; with no monitoring plugin it says Per-iteration detail for this node is not kept in the run state. A monitoring plugin records it.
A loop body node's run popup listing one row per iteration with each iteration's output.A loop body node's run popup listing one row per iteration with each iteration's output.

A loop body node's popup: one row per iteration, from the loop's kept output.

A loop body node's run popup saying its per-iteration detail is not kept in the run state and a monitoring plugin records it.A loop body node's run popup saying its per-iteration detail is not kept in the run state and a monitoring plugin records it.

An interior loop body node's popup with no monitoring plugin.

An interior loop body node's run popup listing three iteration rows, the second errored with its failure.An interior loop body node's run popup listing three iteration rows, the second errored with its failure.

An interior loop body node with a monitoring plugin: one row per iteration from the recorded body steps, one errored.

A nested loop body node's run popup listing rows Iteration 0 › 0 through Iteration 1 › 1 with each value.A nested loop body node's run popup listing rows Iteration 0 › 0 through Iteration 1 › 1 with each value.

A nested loop's body node: its rows labelled Iteration i › j.

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 route’s On Error kwargs behave like a node’s own: text that is not yet a JSON object stays in the panel’s draft, Update node is refused with a message naming the route and asking to fix its kwargs, closing the panel asks the unapplied-changes question, and Discard changes returns the route to its applied kwargs. 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 Settings form with the Decided by control set to Classifier model, the yes-probability threshold, and the classifier Model section.The Router Settings form with the Decided by control set to Classifier model, the yes-probability threshold, and the classifier Model section.

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

On the canvas a classifier-backed router carries a classifier face badge in place of the loop badge (Loop cannot apply there); an LLM-agent router keeps its loop badge when it loops.
A classifier-backed Router card on the canvas showing its classifier face badge.A classifier-backed Router card on the canvas showing its classifier face badge.

A classifier-backed Router on the canvas with its classifier face badge.

The card’s Output tab renders the verdict schema: under the classifier, one yes/no answer per kind — a 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 the classify tool is registered; on a server without it, the entry is disabled with the reason Requires the classify tool.
The palette Agentic group with the Classifier entry, its Judge badge and gloss.The palette Agentic group with the Classifier entry, its Judge badge and gloss.

The Classifier entry in the palette's Agentic group.

The Classifier palette entry disabled with the reason Requires the classify tool.The Classifier palette entry disabled with the reason Requires the classify tool.

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

On the canvas the card carries routing-in, routing-out, and error handles, and a compact band of one chip per question — a type glyph and the question’s key. The band shows up to four chips at rest with the rest folded as “+K more” and every chip when the card is selected; the chips are display only and draw no handles.
A Classifier card on the canvas showing its per-question chip band.A Classifier card on the canvas showing its per-question chip band.

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

A selected Classifier card with all its question chips shown.A selected Classifier card with all its question chips shown.

A selected Classifier card revealing every question chip.

Its Settings form reads top to bottom: Name, Description, the Questions table, Input expression, Model, Run condition, and an Advanced toggle that reveals the standard runnable surface and the read-only round-trip carriers. Each row of the Questions table has a Key, a Type segmented control — Yes/No · Pick one · Rate — an Instructions textarea, and the type’s own criteria editor:
  • 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.
Validation is inline per row, and the apply gate blocks Update on any invalid row — the card needs at least one question to save. The Input expression field is the same Inline / Template control every other editor jq uses: the Inline tab takes a jq and helps with “Must return an object with a 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 Settings form with the questions table, input expression and model fields.The Classifier Settings form with the questions table, input expression and model fields.

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

The Output tab renders the response schema generated from the card’s questions — one answer per question keyed by its key — so the answer shape is readable in build mode and run mode renders the result against it. The schema is not saved into the flow: downstream reads of the answers stay untyped, as Judging with a classifier explains.
The Classifier card's Output tab showing the generated answer schema.The Classifier card's Output tab showing the generated answer schema.

The Classifier card's Output tab showing the schema generated from its questions.

On save the card lowers to one runnable calling 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 as name — description; the selected program’s declared write paths show beside it as a writes: 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 $record and the run input as $input, returning the op batch itself, for a write the templates do not name.
An update may carry an optional op id for idempotency. The write-regime check runs at save: a landing path outside the update’s declared writes, or an adapter whose output misses the declared input, is refused loudly. A save that writes a path another flow also writes succeeds and surfaces a quiet, non-alarming note; a genuine write failure still renders loudly. With the states store not configured, the binding surfaces self-hide rather than erroring.

Presets

The palette’s Custom Nodes section manages a preset as a node: an edit control opens its flow editor, a + New custom node action starts a fresh one, and a delete control raises a guarded confirm that states the blast radius — the node leaves the palette and each flow already embedding it keeps referencing it and fails until updated. The confirm also lists what still holds the name and keeps Delete custom node disabled until that list is empty (see Renaming and deleting a flow). 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. An invalid 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: each purpose: "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 a purpose: "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.

Steps that call nothing

A Runnable step with no tool and no Expression is legal: the engine runs it as a pass-through whose output is its own input. So that such a step is never a surprise, the Save flow dialog (opened once a save passes the editor’s checks and the engine’s validation, for Save and for save-and-exit) and the editor’s Duplicate flow dialog (Save as and Duplicate) list each one, in canvas order, as a line in the dialog: “‘<step>’ has no tool and no Expression, so it calls nothing: its output is its input, passed on as it is.” The step is named by its name, or its id when it has none. The warnings never block the write. The flow list’s own Duplicate copies the stored flow as it is and lists none.

Upload

The flows toolbar carries an Upload action for a flow or node document with a kind, 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 reporting the uploaded document's kind and name.The flows toolbar Upload action reporting the uploaded document's kind and name.

The flows toolbar Upload action with its result line.