Studio editor contracts¶
Studio's editor is extended and driven through a small set of frozen contracts: the step details registry every step kind registers in, the test data document that keeps inputs, pinned outputs and scripts, the Execute step requests that run a step offline or in an environment, and the storage and keyboard conventions Studio follows. This page is the reference for each.
Who it is for. Contributors who add a step kind, an AI step or an editor feature, and integrators who read or write Studio test data. Status. Version 1 of each contract is frozen. Studio and the platform add the editor views and endpoints described here in later releases, and Studio starts applying the storage rules then too; the types, the JSON Schema and the models already exist, so code can be written against them.
Field casing¶
| Where | Casing |
|---|---|
Platform API bodies, query parameters and error result payloads |
snake_case |
The definition language, Studio documents (test data, canvas), and the Studio host's /studio/local/* routes |
camelCase |
| A Studio document stored on the platform | camelCase inside a snake_case envelope |
A TypeScript interface follows the wire body it describes, so those for platform
bodies, such as StepTestRequest, are snake_case. One host route answers in the
platform's shape: POST /studio/local/decision/evaluate returns
matched_rule_ids and used_default.
Step details registry¶
The registry is studio/src/app/editor/ndv/registry.ts (ndv is the code name
of step details), version 1 (NDV_REGISTRY_VERSION). Step details, the canvas
and the Add a step panel read it; nothing else extends step details.
| Function | Registers |
|---|---|
registerKind(descriptor) |
A step kind: names, icon, role, Add a step category, ID prefix, defaults, language feature, expression fields, nested step lists, labeled outputs, summary, output schema, whether its output can be pinned, the test data script it needs and how it runs in an environment |
registerParameters(registration) |
The component that edits a kind's parameters, or one AI agent slot's (subNode), loaded on first use |
registerSubNodes(kind, slots) |
The slots under an AI agent (Model, Memory, Tools, Output): whether each is required and how many entries it takes, chip data, one chip per entry of a many-valued slot, and the entries the scoped Add a step panel offers, grouped, with a reason when one can't be used |
Registering the same kind, the same parameters or the same kind's slots twice
throws RegistryError, as does an invalid ID prefix, a slot listed twice or a
slot maximum that isn't a whole number of at least 1. Registration modules are
listed in studio/src/app/editor/ndv/kinds/index.ts and imported lazily; each
exports register(). Studio awaits loadKindRegistrations() before it reads
the registry. The modules load in parallel and register in list order, so the
registry lists kinds the same way however the chunks arrive. A module that fails
to load is tried again on the next call, after the modules that did load have
registered, and a module is never registered twice.
Built-in kinds¶
Role and category are the registry's NodeRole and PanelCategory values.
| Kind | Role | Add a step category | Output can be pinned | Test data script |
|---|---|---|---|---|
action |
app |
app |
Yes | — |
llm (AI task) |
ai-task |
ai |
Yes | — |
decisionTable |
rules |
data |
No | — |
transform |
transform |
data |
No | — |
switch (Decision) |
decision |
flow |
No | — |
parallel |
parallel |
flow |
No | — |
fail |
fail |
flow |
No | — |
wait |
wait |
wait |
No | — |
signal |
signal |
wait |
No | signal |
humanTask |
human |
human |
No | human |
A nested step list lives at the path of a branch plus steps:
["cases", 0, "steps"] and ["default", "steps"] for a decision,
["branches", "first", "steps"] for a parallel step. The language manifest lists
the fixed branches of a kind, such as default for a decision and body for a
loop (["body", "steps"]); decision cases and parallel branches are found by
position and by name. Output handles are case:<index> and default for a
decision and branch:<name> for a parallel step; fail has none.
Editing from a parameters component¶
A parameters component receives NdvContext through a context input declared
with input.required(). It reads the step, its scope, sample data and the
catalog, and changes the definition only through edit(changes, label), which
applyEdits() in studio/src/app/editor/ndv/edits.ts carries out on a copy:
- A path is relative to the step. With
scope: "workflow"it is relative to the workflow document and must name a field underspec(neverspec.steps) or undermetadata, for example["spec", "llmProfiles", "support"];["spec"]or["metadata"]alone is refused. value: undefineddeletes the key or list item. Its parent stays, so deleting the last field of{"object": {...}}leaves{"object": {}}.- A list position is a number or
"-". Writing at the position just after the last item, or at"-", appends; a write past that is refused. When the list isn't there yet,"-"or position 0 starts a one-item list and any other position is refused. A position under a field that holds something other than a list is refused. - A step's
idandkindcan't be edited, including those of steps inside a case, a branch or a loop: rename steps with Rename, which rewrites references, and move steps on the canvas. - A refused change throws
EditError, and none of the call's changes apply. - One call is one undo step, and edits within 600 ms merge into it.
openAddStep({slot?, after?}) opens the Add a step panel, scoped to an AI agent
slot when slot is set. openSubNode(slot, index?) opens a slot, or one entry
of a many-valued slot such as Tools. sample.context(at) returns what an
expression at that path can read: input, the output of each step under
steps, and, inside a loop, the roots of the iteration picked in the Input
pane: item, index and loops (every enclosing loop by step ID).
catalog.workflows({callableBy}) lists published workflows; those that don't
allow that caller stay listed and carry disabled with the reason.
Running a step in an environment¶
A kind that can run in an environment sets real on its descriptor. Its
request(ctx) returns {body} or {blocked} with the reason the step can't
run, such as Choose an action first. The body is a StepTestRequest
(snake_case, see In an environment) without draft_id and
draft_revision: the kind sets kind, step_id, input, the connection and
release bindings, the side effect it acknowledges, tool mocks, real_tools and
timeout_seconds, and the editor adds the saved draft's draft_id and
draft_revision when it sends POST {ENV}/step-tests. NdvContext doesn't
expose the draft, so a kind can't set those two fields.
Coverage against the language manifest¶
studio/tests/schema-coverage.test.ts fails when a step kind the language
manifest marks studio: "ready" has no descriptor, or when a descriptor names a
kind the manifest doesn't list or a different language feature than the manifest
gives it. Kinds marked pending may lack one. A change that adds a step kind to
the language adds its descriptor or the pending mark.
Test data¶
One test data document per workflow keeps the test input, pinned outputs, signal
and human task scripts, recorded AI turns and mocks for steps inside called
workflows. Its JSON Schema, urn:firefly-weave:schema:studio-test-data:v1, is
studio-test-data-v1.json;
the Python models are in firefly_weave.contracts.studio_documents and the
TypeScript types in studio/src/app/editor/state/test-data.ts.
| Field | Holds |
|---|---|
schemaVersion, kind, updatedAt |
1, weave.studio/testData, and an RFC 3339 time |
input |
The test input: {value, source} |
pins |
Pinned outputs, with the time they were pinned (pinnedAt) and the fingerprints that mark them out of date (configHash, contractHash, upstreamHash) |
signals, humans |
Scripts: a payload or a decision, or {"outcome": "timeout"} and {"outcome": "expire"}; an optional frame |
aiTurns |
Recorded agent turns: {turns, memory?, autoApprove?} with their source and fingerprints; each item of turns is an object whose shape urn:firefly-weave:schema:studio-ai-turns:v1 defines |
workflows |
Mocks for steps inside called workflows, by callee reference: {"notify-customer@1.0.0": {"mocks": {...}}} |
waits |
autoAdvance, true when absent |
- Keys of
pins,signals,humans,aiTurnsand each called workflow'smocksare a step ID, which applies to every iteration, or an instance key (send[1],support#2.1), which overrides it for one iteration or activation. Lookups try the instance key first. An instance key is the step ID, then[i]per enclosing loop,#and dot-separated segments for repeated activations, and~nfor a loop's yields. The step ID part of a key is ASCII: an optional@, then a letter or digit, then letters, digits and_,.,:,@and-. Authored step IDs are narrower, with only letters, digits,_,.and-; the@and:forms are the platform's own IDs. A key that doesn't follow this grammar, such assend mailorsend[01], is refused;firefly_weave.contracts.instance_keysdefines it, and$defs.keyin the JSON Schema repeats it. - Frames. A script with
frame(enrich,notify[2]/enrich) applies only inside that sub-workflow. - Source.
sourceismanual,schema,simulated,test-callorrun(anaiTurnsentry is neverschema). Data from a run or a test call namesrunIdandrunEnvironmentId, andrunIdnever appears withoutsourceandrunEnvironmentId. - Size. A document is at most 1 MiB.
- Secrets. Test data never holds a value a schema marks
x-secretorwriteOnly; such entries are refused and not kept anywhere. - Strictness. The Python models accept exactly what the JSON Schema accepts.
An optional field is left out, never
null; JSON uses the camelCase names only; a UUID is written with hyphens, 8-4-4-4-12; and a timestamp is an RFC 3339 date-time such as2026-10-07T10:00:00Z.
Stored on the platform¶
When Studio is connected, the document is stored with its draft at
{PROJECT}/drafts/{identifier}/studio/{kind}, where kind is canvas or
test-data. The envelope is snake_case:
| Field | Meaning |
|---|---|
draft_id, kind, revision, draft_revision |
The draft, the document, and their revisions |
document |
The document, in its own camelCase |
contains_run_data, source_environment_ids |
Derived by the server and sticky: set when the document holds data from a run or a test call, cleared only by deleting the document |
updated_at, updated_by |
When and by whom |
A save sends only {draft_revision, document}; a body that sets
contains_run_data or source_environment_ids is refused. Reading or writing a
document with run data needs run.read in each source environment. A canvas
document is at most 256 KiB and a test data document at most 1 MiB
(413 WV-STUDIO-DOCUMENT-SIZE).
Execute step¶
Simulated, on the Studio host¶
These routes run in the Studio host, offline, through the simulator; their
bodies are camelCase. The Python models are in
firefly_weave.studio.local_contracts and the TypeScript types in
studio/src/app/editor/state/execution.ts.
| Route | Body | Result |
|---|---|---|
POST /studio/local/debug/execute |
ExecuteRequest |
ExecuteResponse |
POST /studio/local/debug/sessions |
SimulationRequest (the Execute body without target) |
A debug session, in the platform's debug.* shape |
POST /studio/local/testdata/check |
CheckTestDataRequest |
{entries: [{path, ok, code?}]} |
POST /studio/local/decision/evaluate |
{table, input} |
{output, matched_rule_ids, used_default} |
- Target.
modeisworkflow(Execute workflow, which names no step),step(Execute step: until the step has output) orbefore(Execute previous steps: paused before the step).stepandbeforenamestepId;instanceKey, when set, is an instance of that step and picks one iteration (iteration 0 otherwise). - Mocks are keyed
node:<instance key>,node:<step ID>,agent:<instance key>,tool:<instance key>/<tool>oraction:<reference>, where a step ID is itself an instance key; the most specific applies, in that order. Scripts come from test data:signals,humans,aiTurnsandautoAdvanceWaits. - Called workflows.
workflowscarries each callee's local source and format, or only its mocks when the callee is published. - Result.
statusiscompleted,blocked,failedorlimit. Every response carriestraceand acompilesummary, and the summary carriesdiagnosticsandstubs; any of the three lists can be empty.blockednames itsreason(missing_mock,signal,human,path,callee) exactly when the status isblocked, and a definition that doesn't compile reportsfailedwith its diagnostics incompile.diagnostics. The response's owndiagnosticslist holds debugger errors, such as a missing mock. Eachtraceentry namesnodeId(the step ID) andinstanceKey(""when it equals the step ID), andframeinside a called workflow;loops,framesandselectedScopedescribe loop progress, sub-workflow frames and the picked iteration's roots. - Test data checks report each entry by JSON pointer; refused entries name
WV-SCHEMA-SECRET_VALUE,WV-SCHEMA-CLASSIFICATION,WV-STUDIO-TESTDATA-SCHEMAorWV-STUDIO-TESTDATA-UNRESOLVED.
| Status | Code | When |
|---|---|---|
| 422 | WV-STUDIO-REQUEST |
The body doesn't fit its model |
| 422 | WV-DEBUG-* |
The simulator refuses a mock, a command or a limit |
| 422 | WV-SCHEMA-SECRET_VALUE |
The input or a mock holds a secret |
| 409 | WV-STUDIO-CATALOG-CONFLICT |
A local definition conflicts with the catalog |
| 409 | WV-DEBUG-REVISION |
A session changed since it was read |
| 404, 410 | WV-STUDIO-DEBUG-NOT-FOUND, WV-DEBUG-EXPIRED |
Unknown or expired session |
| 429 | WV-STUDIO-SESSIONS-FULL |
Four sessions are open |
| 502 | WV-STUDIO-CATALOG |
The platform catalog is unavailable |
| 503 | WV-STUDIO-BUSY |
The simulation slot is busy, or the work took over 30 seconds |
In an environment¶
Execute in environment runs a step, or a whole draft, through the normal
runtime. It needs the step.test capability on the environment, the
environment's test policy set to enabled, and connection.bind on every
connection revision the test uses. Bodies are snake_case; the Python models are
in firefly_weave.contracts.step_tests and the TypeScript types in
studio/src/app/editor/state/execution.ts.
| Operation | Body | Result |
|---|---|---|
POST {ENV}/step-tests |
StepTestRequest |
{id, run_id, status: "running", side_effect, expires_at} |
GET {ENV}/step-tests/{id} |
— | StepTestView |
POST {ENV}/step-tests/{id}/answers |
{instance_key, confirm} |
— |
POST {ENV}/step-tests/{id}/cancel |
— | — |
POST {ENV}/test-activations |
DraftTestActivationRequest |
{id, activation_id, artifact_digest, side_effect, expires_at} |
POST {ENV}/test-activations/{id}/runs |
{input} |
The run, with test: true |
POST {ENV}/test-activations/{id}/listen |
{secret_ref, max_body_bytes?, tolerance_seconds?} |
{test_url, expires_at} |
DELETE {ENV}/test-activations/{id} |
— | — |
GET and PUT {ENV}/test-policy |
{test_calls}, with If-Match on PUT |
{test_calls, revision} |
- Side effects. A step that changes data (
idempotency_keyornon_idempotent) runs only whenacknowledged_side_effectnames that effect; otherwise the answer is409 WV-STEPTEST-CONFIRM. An AI agent's non-idempotent tools use theirtool_mocksunless listed inreal_tools, and each real call of such a tool waits for aconfirmanswer:trueruns the call,falsereturns its mock. - Waiting. While
statusiswaiting,pendinglistsconfirm,reviewandpersonitems bynode_idandinstance_key. Aconfirmitem names itstool,side_effectandarguments; reviews and person answers are human tasks, named bytask_id, that also show in My tasks. - Status is
running,waiting,succeeded,failed,timed_outorcancelled(shown as "Canceled"). - Limits. A step test runs for at most 300 seconds, an AI agent's for 900.
A draft test activation lasts 900 seconds by default and at most 3,600. Only
the person who created a step test or a test activation can read, answer,
cancel or delete it; anyone else gets
404 WV-NOT-FOUND. The listen URL carries no secret: requests are signed with the secret handle. - Test policy. An environment without a stored policy is
disabledat revision 0. The demo environmentlocalthatweave platform upcreates isenabled.
| Status | Codes |
|---|---|
| 403 | WV-FORBIDDEN, WV-STEPTEST-DISABLED |
| 404 | WV-NOT-FOUND |
| 409 | WV-STEPTEST-CONFIRM, WV-REVISION |
| 410 | WV-TESTACTIVATION-EXPIRED |
| 422 | WV-STEPTEST-INPUT, WV-SCHEMA-SECRET_VALUE, WV-STEPTEST-CONNECTION, WV-STEPTEST-RELEASE, WV-STEPTEST-KIND, WV-STEPTEST-TOOL-MOCK, WV-TESTACTIVATION-COMPILE, WV-TESTACTIVATION-LIFETIME, WV-ETAG |
| 429 | WV-STEPTEST-BUSY |
| 503 | WV-COMPATIBILITY |
Storage on this computer¶
Every key the new editor keeps on this computer goes through
studio/src/app/editor/state/browser-store.ts. These rules apply once the new
editor ships; until then Studio keeps its drafts and preferences under the keys
it already uses.
| Prefix | Holds |
|---|---|
<profileId>:<subjectHash>:<tenantId>:<projectId>: |
Connected work for one account in one project |
local: |
Local drafts and their test data |
ui: |
Preferences that hold no workflow data, such as pane widths and the new editor preference |
profileIdispand the first 16 hex digits of the SHA-256 of the saved platform's name;subjectHashis the first 16 hex digits of the SHA-256 of the signed-in subject. Without a subject, connected work is kept in memory only.- Keys match
^[A-Za-z0-9:._-]{1,256}$. - Signing out of a platform, or removing it, deletes every key with that
platform's
profileId, in browser storage and in the desktop store.local:andui:keys stay. - Keys from earlier releases move once: local drafts under
local:, pane preferences underui:. Remembered run inputs (weave-studio-run-input:) can't be attributed to an account, so they are deleted. - The desktop app keeps the same keys in the Studio host store under its configuration directory instead of browser storage.
Keyboard map¶
studio/src/app/editor/state/keymap.ts holds the editor's one shortcut table;
the key handler and the shortcuts sheet (?) both read it. Each key combination
appears once per platform, and a row says what it does on each surface: the
canvas, the outline, step details, a Fixed field, the formula editor, the Add a
step panel, the decision table grid and the run log. Mod is Ctrl on Windows
and Linux and Cmd on macOS. Shortcuts never act while you type in a field,
except Ctrl/Cmd+S, Ctrl/Cmd+Enter, undo and redo outside the formula editor, and
= in a Fixed field, which switches an empty one to Mapped. D is not
assigned, because Weave has no deactivated steps, and Ctrl/Cmd+K is reserved.
The last section of the sheet, Differences from n8n, lists each row that differs
as two sentences: what the keys do in Studio, then what they do in n8n.
Try the new editor¶
The new editor is a per-viewer preference, off by default. The key
ui:weave.editorNext holds true or false, and ?editor=next in the URL
turns the new editor on for that page, which is how tests open it. Settings ›
Preferences will offer the same choice as a toggle.