Quickstart¶
Run Studio, point it at a Kneo Agent Platform, and import your first workflow.
Scope
[0.9.0]. Import, from n8n or Dify → govern → edit → run → export: you can change a spec in Studio, run it on the platform and read the result back, and take the governed result out as YAML — on your own machine or on a shared host under a login mode. The Hand-off page converts the export into the inline JSON the Dashboard's Launch accepts, with the paste steps beside it, and reports up front whether the target environment can actually receive it. The analysis key can be rotated out rather than kept forever (see hosting). The debug loop is mostly here[0.7.0]. A run's graph, its trace, its checkpoints and their diff are served and reachable from the Debug page, and a run can be stopped. What arrives later is replay and resume, and pushing to the Dashboard without the paste. See the roadmap; this page grows with them.This block listed all six debug operations as "what arrives later" — including the four that shipped in 0.7.0 — and carried no cut stamp, so it read as current scope on the page a new user starts from
[docs-consistency/F-01, fixed 0.8.0 A1]. It is the second time this banner has outlived the code; the first is recorded below.test_the_scope_block_does_not_defer_what_ships.pynow holds it against the routes that answer.This said "0.4.0 scope" and "what arrives later is test runs —
POST …/test-runsis published and answers a501" until[2026-09-02], after the route began dispatching. It was the start page telling readers that the release's headline feature did not exist (touchpoint 2 round 1, F-03).
Run it¶
Studio serves the BFF and the built SPA on http://127.0.0.1:8091.
Which Studio this page describes.
pip install kneo-studioinstalls the latest release, 0.9.0, which is what this page describes. On 0.8.0 and earlier,examples/n8n/chat-agent.jsondraws a blockingproprietary_nodecard for its chat trigger, and a Dify import answers501 not_implemented.
Both halves of that address are configurable — KNEO_STUDIO_HOST (default
127.0.0.1) and KNEO_STUDIO_PORT (default 8091). The port is worth knowing
if something else already owns 8091; the host is subject to the rule in the next
paragraph.
It will not start on a routable address without authentication. Under
auth_mode = none — still the default — every request is the local owner with
full rights over your working directory, so binding 0.0.0.0 would publish
exactly that. Studio refuses rather than warns, and names the remedy: bind
loopback, or set KNEO_STUDIO_AUTH_MODE to static or oidc. Those modes
shipped in 0.2.0; the refusal is not a placeholder for them, it is what keeps the
local default local.
The working directory is the project root. Point somewhere else with
KNEO_STUDIO_PROJECT_ROOT.
Behind a reverse proxy¶
Studio checks the Host and Origin of every request, so a proxy that presents
a different name than the one Studio binds is rejected until you say the name is
expected:
KNEO_STUDIO_ALLOWED_HOSTS |
comma-separated host names to accept in addition to the loopback defaults |
KNEO_STUDIO_CORS_ORIGINS |
comma-separated origins allowed to call /api from a browser; empty by default, which allows none |
KNEO_STUDIO_TRUSTED_PROXIES |
the addresses your proxy connects from; without it, a forwarded client address is not trusted — see hosting |
Neither widens what the loopback rule above permits: with auth_mode = none,
Studio still refuses to bind a routable address. They are documented here
because they exist and are enforced today — this page described neither, and a
proxy hop failing a Host check with no setting to reach for is the kind of
thing you should not have to read the source to solve.
KNEO_STUDIO_CORS_ORIGINS is needed today, by anyone running the frontend
dev server. npm run dev serves the SPA from localhost:5174 and proxies
/api to the BFF, so the browser's Origin is the dev server while the
request arrives at 127.0.0.1:8091. The same-origin rule refuses that mismatch
on every mutating call — import, artifact creation and every other
POST/PUT/PATCH/DELETE answer 403 cross_origin_refused while reads work
fine, which is a confusing way to discover it. Export is a GET, so it keeps
working: the check applies to mutating methods only.
This listed export among the refused calls until [2026-09-06], in the same
sentence that says the rule fires on mutating ones — a list that refuted itself,
on the page that is the only place a reader learns this boundary.
(This paragraph said these settings "matter once static or oidc land and a
served instance becomes startable" — presenting a variable the documented
development workflow needs now as one for later. The README's own
npm run dev line pointed at the workflow without mentioning it. Two pages
each defensible alone, and jointly a dead end.)
KNEO_STUDIO_RELOAD=1 restarts the BFF on source changes. It is a development
convenience and belongs nowhere near a shared host.
Connect it to a platform¶
Studio talks to kneo_serv through a kneo_client profile. If you already
have one, skip to naming it below.
A profile is a section in ~/.config/kneo/client.toml:
[laptop]
url = "https://kneo.example.com"
api_key = "…"
auth_scheme = "bearer" # or "kneo_api_key" for the X-Kneo-Api-Key header
timeout = 30.0
Or, without a file at all — the environment is read after the file and overrides it, so this is a complete profile too:
Going the environment route, the profile name is informational: with no
client.toml, any name resolves to those variables. Pick one you will
recognise in a log.
Then name it for Studio:
export KNEO_STUDIO_DEFAULT_PROFILE=laptop # a kneo_client profile name
export KNEO_STUDIO_DEFAULT_PLATFORM_ENVIRONMENT=dev # optional, defaults to dev
kneo-studio
That is the whole binding. The profile supplies the URL and the credential; Studio never stores either, and the profile name itself never reaches the browser.
Without it, Studio still runs. The project is created unbound: you can import workflows and read the generated spec and its gap cards — all of that is local. Acknowledging a card needs an artifact, and an artifact needs the binding. What needs a platform is governance (validate, compile, policy report), and the UI says the project is unbound rather than failing at the first attempt.
Starting unbound is not a dead end. Set KNEO_STUDIO_DEFAULT_PROFILE and
start Studio again; the project picks up the binding on that start, and drafts
you already imported become governable. Nothing is lost and nothing needs
deleting — you are back on the path above.
The reverse is deliberately not automatic. A project that is already bound
is never silently retargeted by an environment change, because its alias
generation is a snapshot the artifacts governed under it depend on. Changing a
live binding is an operator action at the host — the same bar as editing the
profile store — and it is env rotate:
That adds a generation rather than moving one, so it goes live for new work
immediately while an artifact frozen at the old generation still resolves the
binding it was governed against. Once nothing is running against the old one,
kneo-studio env retire --project default --alias default --generation N stops it
resolving for new work. The full family is
env add|rotate|retire|rebind; --help on each explains what it takes.
This has been here since 0.3.0, and until it landed a mistyped profile name bound a project permanently — nothing wrote the retirement column, so there was no way to correct a binding short of editing the state database. The previous advice here was to retarget "before you have artifacts worth keeping". You no longer have to.
What the zero-config binding grants¶
Deliberately, as little as possible. The alias is created as default
with transport remote, and
nothing else is enabled:
| Out of the box | |
|---|---|
| Transport | remote, always |
| Host allowlist | empty |
| Secret-ref namespace | none |
| Operator registry | empty |
A kneo_client profile carries only a name, URL, key, scheme and timeout — it
cannot express a transport or a server root. So there is nothing to infer
shared_filesystem from, and Studio does not guess: inferring it would hand a
spec filesystem reach on the strength of an assumption.
The practical consequence, which is the same under either transport (and has been since 0.2.0):
- Path-free specs and overlays work. Layering happens in memory, so an overlay never needs to be a path the platform can see.
- Filesystem literals are refused —
skills[].source,mcp_servers.*.cwd,ca_bundle,client_cert,client_key. There is no bounded dependency closure yet to materialise a skill tree into, so a spec that names a local path would be asking the platform to read a file that is not there. The closure is a later design-first release — it needs directory semantics, size limits, traversal rules, quotas and staging decided together — so no cut is promised for it; see the roadmap. - Secret references pass through untouched.
kneo_servresolves them on its own host, against its own secret material. Studio never expands one — on a remote deployment the value does not exist here at all. - Anything that executes needs an operator's approval. An MCP
commandis arbitrary process execution on the platform host, so it is permitted only as{"$registry": "<name>"}naming an entry the operator approved for that alias. An empty registry — the default — approves nothing.
Registry entries make a spec non-portable: they resolve against this environment, and Studio refuses to export an artifact that depends on one.
Where Studio keeps things¶
Two places, and they are deliberately separate:
$KNEO_STUDIO_PROJECT_ROOT (default: the working directory) |
your specs, overlays and imported drafts, under .kneo-studio/ — files you edit, commit and back up like source |
$XDG_STATE_HOME/kneo-studio/ (default ~/.local/state/kneo-studio/) |
studio.db, the snapshot store and the analysis keyring (analysis-keyring.json) — Studio's own state: projects, principals, analyses, artifacts, audit |
The database is not in the project root, which surprises people two ways:
copying a project directory does not carry its artifacts, and running Studio in
a different directory still finds the same projects. Point XDG_STATE_HOME
somewhere else to keep instances separate, and include that directory when you
back up — an artifact's governance history lives there, not beside the YAML.
Studio refuses to start if the state store is inside the project root, which would make a project directory contain the database describing it.
Storage¶
Studio caps how much it will hold. On a laptop that is 5 GiB, built in — you
do not have to choose a number, and Studio still cannot fill your disk by
accident. Override it with KNEO_STUDIO_DEPLOYMENT_CAP (bytes).
On anything shared, the cap must be declared and Studio refuses to start
without it. "Shared" means any of: KNEO_STUDIO_AUTH_MODE other than none,
a non-loopback KNEO_STUDIO_HOST, a configured KNEO_STUDIO_WORKSPACE_BASE, or
a principal other than the local owner in the store. The reasoning is narrow:
the built-in number was chosen for one person's laptop, and on a shared instance
the person who would exceed it is not the person who chose it. The startup error
names the setting and every reason that triggered it, so you fix it once rather
than one restart at a time.
Request size¶
Studio refuses a request body over 4 MiB with 413 request_too_large,
before it reads or parses the body and before it works out who is asking. That
bound is about protecting the server's memory, so it deliberately sits above the
limits that describe content — a workflow source, n8n or Dify, over 1 MiB is
still refused as source_too_large, which tells you what to fix.
Raise it with KNEO_STUDIO_MAX_REQUEST_BYTES (bytes) if a legitimate payload is
larger. An unreadable value falls back to the default rather than preventing
startup.
Login attempts¶
Three more bounds, on the one thing an unauthenticated caller can ask Studio to do that is genuinely expensive. The first two exist because 0.3.0's audit measured what happened without them: 32 concurrent login attempts took an unrelated request's median response time from 0.7 ms to 1.2 seconds, and 300 unauthenticated requests to the OIDC start route left 300 rows in the database that nothing reclaimed.
- Password checks run two at a time, in worker threads. A password hash is
deliberately expensive — 64 MiB of memory each — so this is what stops a stream
of attempts from slowing down everyone who is already signed in. Attempts wait
briefly for a slot and are answered
503 login_capacity_exhaustedif the wait is long. Raise withKNEO_STUDIO_LOGIN_VERIFY_CONCURRENCYif you have the cores and memory for it. Underoidcthe same slots and the same setting cover the token verification, whose signing-key fetch waits on your identity provider; a sign-in that finds no slot is sent back to the login page with the same code. -
Ten failed attempts on one account from one address in a minute, then
429 too_many_failed_attemptsfor the rest of that minute. Counted per account and per address, so nobody can lock a colleague out by guessing at their name, and a correct password clears the count immediately. Change withKNEO_STUDIO_LOGIN_FAILURE_LIMIT. -
Twenty OIDC logins started from one address in a minute, then
429 too_many_login_startswithRetry-After: 60. This one counts per address only — not per account, because beginning a login does not name an account yet. Change withKNEO_STUDIO_LOGIN_START_LIMIT. Two things worth knowing before you tune it: if your proxy does not forward the client address, every user counts as one client, so checkKNEO_STUDIO_TRUSTED_PROXIESfirst; and the count is held in memory per process, so running several replicas allows the limit each.
On oidc, beginning a login also stores a short-lived row. Studio keeps at most
1000 of those in flight and reclaims expired ones on every new login, answering
503 too_many_login_attempts at the cap; KNEO_STUDIO_MAX_OIDC_ATTEMPTS changes
the number. That cap and the start limit above are not the same bound and do not
replace each other: the cap protects the database, and because it is shared, one
caller filling it would refuse everyone's login — the start limit is what keeps
one address from doing that. The sign-in link carries the page you were on, so you
come back to it; that return_to path is capped at 512 bytes and ignored silently
above it — no real in-app path is close.
Every one of these falls back to its default on an unreadable value, for the same reason the request bound does. What each bound does and does not protect against — including why there is deliberately no account lockout — is in auth_design.
Import a workflow¶
Open http://127.0.0.1:8091. On Import, pick the Source — n8n, or Dify
(see Importing a Dify app) — choose the Workflow export file,
and on a bound project give the Environment alias (default out of the box).
Then press Import. The rest of this page is the same for both sources.
If this is your first import, use examples/n8n/chat-agent.json.
Every n8n trigger becomes a card, because a Kneo spec has no trigger concept. For a
webhook, schedule, event or chat trigger that card — Trigger must be recreated
outside Kneo (entry_mechanism_not_carried) — is manual: it must be
acknowledged before export. A manual trigger's card is info. For
chat-agent.json the clean outcome is that one manual card plus one info note,
saying the agent's tool loop runs with n8n's default iteration bound of 10, and
without knowing that it is indistinguishable from something having gone wrong.
Do not start from hello-agent.json: n8n itself refuses to run it, so it blocks
with source_cannot_run. The examples
directory also says which workflows convert,
which is worth two minutes before importing something real: a Kneo spec is
agent-centric, so a workflow that is mostly ordinary steps has little to map on
to.
Studio parses it, normalises it to a Kneo v1 spec, and shows you:
- the draft spec it produced (YAML is the source of truth);
- gap cards for everything it could not convert faithfully.
Create artifact from this draft opens it in the Spec editor, which is where you get the graph, rendered from the platform's own compiled topology rather than from the YAML, so the picture and the spec cannot disagree, and the platform's verdict. Both need a bound project.
Gaps are not warnings to skim. A manual gap must be acknowledged before the
artifact can be exported, and a blocking one — an unbounded loop, say — has to
be resolved.
Clearing them is done on the Gaps page (the editor's Review gaps links there), and who may do it depends on your role in the project:
- A
manualcard needs Acknowledge this gap. The acknowledgement is recorded against an artifact, and is for an editor or the owner. - A
blockingcard cannot be acknowledged away. Fix it in the draft and have a project owner Attest that the edit addressed it, or fix the source and re-import. - Withdraw (an attestation) and Reassociate (a renamed draft's analysis) are owner actions too.
A role that cannot use a control sees a sentence saying who can, not the button.
Known limitation — how much of n8n's behaviour Studio models¶
To convert faithfully, Studio has to answer questions about the source: which
node n8n would start from, which nodes that start actually reaches, whether two
triggers run the same program, and whether an attached model is one it
understands. Studio's answers are hand-written against a pinned n8n
(n8n@1.62.1), and they are known to be incomplete.
Nine rounds of external review found nine cases where an answer was wrong — a
disconnected trigger, two webhooks entering a chain at different points, a
first-party model whose node name lacks Chat. Every one is fixed. The
limitation is not any of them; it is that the answers were encoded case by
case rather than derived from n8n's own rules, so the next unusual shape may
find another.
What that means when you import:
- Ordinary workflows convert, and the gap cards are the contract. A card says what could not be carried; no card means Studio believes it converted faithfully.
- Studio fails toward refusing. An unrecognised node, a source with no start,
triggers that disagree, an agent the pinned n8n itself fails on every item (no
enabled model, a model its executor refuses, an
autoprompt with nochatInputto read —source_cannot_run, whose fix belongs in the source), and a fan-out, one output wired to several nodes, which the source runs in full while a Kneo graph step follows one edge (branching_shape, since 0.9.0) — each is a blocking card rather than a quiet conversion. For n8n that includes a trigger wired to two steps, and one step reached by two connections, since n8n runs a step once for every connection that reaches it. A connection that leads back to a step is a loop, even without a loop node, and is blocking until you pick itsmax_steps. A loop with no step in it, such as a disabled node wired back to itself, is blocking too, because the converted workflow cannot carry it. So is a Switch with Send data to all matching outputs on, which can run several of its outputs at once. You will more often be told "I cannot convert this" than handed something wrong. - Studio converts first-party n8n only, and that is a decision rather than a
backlog.
n8n-nodes-baseand@n8n/n8n-nodes-langchain; every other package is refused with a card, community language models included. n8n's ecosystem is largely community nodes, so this refuses a great deal — the alternative is converting a node whose behaviour nobody has read, which means inventing it or dropping it. If you need one, its card says to rebuild the behaviour as a Kneo step; no version of Studio will convert it without a mapping you supply. - A refused first-party node is a different problem, and the card says so.
n8n-nodes-base.setis inside the pinned release; what Studio lacks is a Kneo construct to convert it into. Those cards do not tell you a version is the boundary, because it is not — changing the pin would not help. - Three more answers were corrected in 0.5.0, each a silent conversion until
then. A node's error output ("On Error: Continue (using error output)")
became an unconditional edge — a pager wired to failures ran on every run; it
is now a blocking card —
error_route_not_carriedsince 0.9.0,error_outputbefore — and the edge is labellederror. A model node'stemperature/maxTokensand an agent'smaxIterationswere dropped without a word; they are a manualparameters_not_carriedcard naming each value the author set and the Kneo field it belongs in. Since 0.9.0 a default n8n applies without the author writing it — the iteration bound of 10 — is aninfonote instead. And a sticky note — a canvas comment — drew a blocking card telling you to rebuild it as a step; it is aninfocard now. Because these change what the import reports for the same bytes, the importer and analyzer contract numbers moved — the current ones are theversionandanalyzer_versionon any analysis you read back, so they are not repeated here, where they would go stale: re-import a workflow you imported before 0.5.0 and you get a new analysis with the corrected cards, not the old one back. 0.9.0 moved them again, and renamed seven card kinds (trigger→entry_mechanism_not_carried,no_source_start→no_entry, and five more). A card whose id changed carries no acknowledgement across, so acknowledge it again after re-importing. - Always read the exported spec before you deploy it. That is good practice regardless; here it is the backstop for a class of defect that, by construction, this project cannot yet claim to have exhausted.
0.3.0 derived the node maps from n8n's pinned sources — rather than extending them
one case at a time — and the reasoning is recorded in
its tracker in full. The start, reach and
executor rules above are still read case by case, which is how 0.9.0 came to add
source_cannot_run. (This said "is 0.3.0's work" for two releases
after it landed — a reader lost nothing by it, which is why it outlived the cut.)
Govern it, then take it out¶
An import produces a draft. To govern it you create an artifact, which freezes the exact bytes — base, overlays and overrides — so that what gets validated is what gets exported, and an edit afterwards cannot change either.
From there Studio runs the platform's own validate, compile(strict) and
policy_report against that frozen document, and renders the compiled graph.
Export is the way out:
It returns a JSON envelope — {artifact_id, base_ref, spec_yaml, filename},
where filename is a suggested save name — whose
spec_yaml field holds the exact layered YAML. Redirecting this URL straight
into a .yaml file gives you the envelope, not the spec; take .spec_yaml from
it (… | jq -r .spec_yaml > spec.yaml).
The envelope is produced only after re-running the full gate on the precise body it is about to hand you. A stored verdict would go stale the moment an input moved, so there isn't one. It refuses, with the reason, when:
- an analysis is blocked, stale or unknown, a
blockinggap is unresolved, or amanualgap is unacknowledged (409 artifact_not_ready). The message is one sentence per reason — "1 manual gap(s) need acknowledging on the Gaps board." — and the tokens (artifact_unacknowledged_gaps,artifact_not_ready,analysis_<state>) are underdetails.reasonsfor code, besidedetails.gaps.chat-agent.jsonmeets this on its first export, until its trigger card is acknowledged; - the spec resolves something local to this environment — an operator
registry entry — so it would not mean the same thing anywhere else
(
artifact_not_portable); - the fresh gate returns a negative verdict (
artifact_governance_failed), or the platform itself rejects the spec, in which case its own status and code are forwarded rather than flattened.
The response is never cached: an export is an attestation about one moment.
Run it on the platform, before you hand it over¶
An artifact can be run, in this project's environment, exactly as export would hand it off. Open Test run — from an artifact's editor or Hand-off, the rail's link opens it for that artifact; otherwise pick one — give it an input, and start it:
POST /api/projects/{project}/test-runs {"artifact_id": "...", "input": "..."}
GET /api/projects/{project}/test-runs/{run_id}
You are shown two statuses, and they answer different questions. Studio's
says what Studio knows about the dispatch — pending means it has not yet
confirmed the platform accepted the run; started means it holds exactly one
platform run id; failed_to_start means the platform refused before anything
ran; unknown means recovery asked and could not prove a single run; abandoned
means recovery gave up after repeated attempts. Neither of the last two is
re-issued. The
platform's — queued, running, completed, failed, with the output and token
usage — appears underneath, and only for a run that started. Studio never
mirrors one into the other, so a queued run is never shown as "running".
Refresh reads it back; nothing polls on your behalf.
A second click is a new run, never a re-issue of the first. There is no
replay in this release. If a start fails after the run row was written, the
refusal names that run and the page opens it — the address carries the run
(…/test-run/{artifact}?run={run_id}), so a reload or a copied link still reads
it, and its outcome is settled by recovery rather than by you pressing the
button again.
Check the hand-off before you make it¶
The Hand-off page reads the artifact's frozen binding and reports whether the
environment it names can actually receive the spec — the policy the target
applies, and its credential references counted by status: present, missing, or
with no status Studio can read. A default platform lists its built-in providers
even when none is configured, so most read missing. Values are never shown. Three answers, and the page says which it got: available;
unavailable, which means the alias has no usable profile configured
behind it — the zero-config first run, not an error; and a refusal, where the
platform answered and its own status and code are forwarded unchanged. A
platform that cannot be reached at all is neither of the first two: it is an
error, platform_unreachable, because the absence of an answer is not the same
as an answer. Doing this before the paste is what turns a failed launch into a
question you can answer while you still have the artifact in front of you.
This said an unreachable environment "is reported as unavailable" until
[2026-09-02] — the sentence collapsed a transport failure into the
no-profile state, which the product, the UI and the API contract all keep
apart (touchpoint 2 round 2, F-03).
Known limitation — some platform-valid specs are refused before submission¶
Studio runs its own capability gate before it submits anything, and that gate classifies every field of your spec against a hand-written manifest. A field the manifest does not list is unclassified, and unclassified is refused — the gate fails closed on purpose, because a field nobody has classified might be one that runs code on a host you do not own.
The cost of failing closed is that the manifest has drifted behind the platform's
own models. Specs the platform accepts are currently refused by Studio, with
409 spec_capability_refused on all four govern routes — validate, compile,
policy_report and graph — and there is no override.
How wide was it: every one of the 25 example specs shipped with kneo_serv 1.2.0 was
refused — 25 of 25, including the sequential and graph ones, measured 2026-08-18 by
running Studio's own scanner over kneo_serv/examples/*.yaml.
Re-measured 2026-08-19, after the manifest was derived from the platform's models: 19 of 25. The six that now pass are the ones that were failing purely on classification drift. Most of what remains is not drift at all:
- 8 of the 19 carry a
components.functions.*.implementationrefusal — Python that runs on the platform host. That isEXECUTE, and a literal value there is refused regardless of what an operator has approved: approval attaches to a registry entry, and the only form the scanner admits is a{"$registry": "<name>"}reference to one. These specs get through by rewriting the field as that reference — not by approving the literal, which nothing can approve. (This paragraph said the opposite until 0.6.0 S13; a user following it would have asked for a standing privilege widening on a shared platform and got the identical refusal back.) - 2 carry an
mcp_servers.*.urlrefusal —NETWORK, refused against an empty host allowlist, same reasoning. - The remaining 9 are a mix, and two of their families are not in the table below.
Three overlay specs refuse on
policy_defaults.*and one project-config spec onproject.*,service.*,retention.*andenvironments.*.policy_enforcement.*— neither is the table's top-levelpoliciesrow, which is a different path. The others are classified refusals working as intended rather than drift:tools.*.implementationandmcp_servers.*.command/.argsareEXECUTEliterals,skills.*.sourceisFILESYSTEM, andagent.guardrails.*is the free-form-map row the table does name.
The three counts above were 12, one and "the rest are the two groups named in
the table" until [2026-09-06], and none of the three measured: the first two were
wrong at every counting, and the third was false for the project-config and
policy_defaults families. Corrected by re-running the shipped scanner over the corpus
the paragraph names, and test_published_capability_limits.py now measures the
breakdown as well as the total — the numbers above cannot rot silently again.
The axis is the field, not the workflow family, which is what made the reach so wide:
a single unclassified field anywhere refuses the whole document, and two of the most
common ones sat in the agent block that nearly every real spec has.
| What is in your spec | Status |
|---|---|
agent.strategy as an object — the form the platform documents (type, max_iterations, …) |
accepted |
agent.strategy as a bare string |
accepted |
agent.runtime_preferences.allowed_modes entries |
accepted |
workflow.participants.* — the concurrent, group-chat and handoff families |
accepted |
A human-in-the-loop component — all nine components.humans fields |
accepted |
…including components.humans.*.context, which is classified as data |
accepted |
tools.*.permissions.* — allow_shell and the other permission flags |
refused |
agent.memory, agent.history, agent.guardrails, agent.defaults, top-level policies, agent.model.extra, tools.*.mcp — free-form maps the platform does not model |
refused |
Verified differentially against kneo_serv 1.2.0: each refused item above is
accepted by the platform's own models. This is pre-existing behaviour, present
since 0.2.0 and not introduced by a later patch.
This table said "a sequential or graph workflow — accepted" until 2026-08-18, and that was wrong in the direction that matters: a user whose sequential workflow came back
409 spec_capability_refusedhad no way to read their refusal as the known limitation. It also described the humans row as "all nine fields unclassified", which is wrong in the other direction —contextpasses. Found by 0.3.0's Phase-1 audit (DOCS-1) by running the shipped scanner rather than by reading the manifest. Every row now carries an invisible probe marker, andtest_published_capability_limits.pychecks each one against the code, so a row that stops being true reddens a gate instead of misinforming somebody.
What to do meanwhile. A spec whose every field the manifest classifies governs and exports normally — in practice that means hand-written specs that stay inside the narrow subset above, not exports from the platform's own examples. There is no flag that relaxes the gate, by design.
The fix is a design change, not a longer list. A hand-maintained manifest tracking a schema it cannot see is the same failure mode that produced this drift in the first place, so the manifest is derived from the platform's own models, with a gate that reddens when the two disagree — shipped in 0.3.0. (This said "is being derived … that work is 0.3.0" for two releases after it landed.)
What this means for the editor¶
Saving is not gated. Verdicts are. The editor writes any text you type: a spec is a file, and refusing to save an invalid one would lose your work mid-thought and push you into a different editor. So the gate above is met at Preview, at the three platform verdicts, and at export — never at Save. If your session expires mid-edit, Studio shows the sign-in page, and once you are signed in the unsaved edit is restored in its editor.
That is deliberate, and it is the honest reading of the limitation: you can edit and keep
any spec, and Studio will tell you it cannot judge one outside the classified subset. A
409 spec_capability_refused from Preview is that limitation, not a fault in your file
and not an outage.
There is no override, and that is the design. A flag relaxing the gate would let an unclassified field — possibly one that runs code on a host you do not own — reach the platform unexamined, which is the whole thing failing closed prevents. What is on offer instead:
- keep editing and saving; the file is yours and Studio is not holding it;
- ask whoever runs your platform to widen the environment's allowances, if the refusal names a capability rather than an unclassified field;
- re-import the corrected source, which gets you a machine verdict rather than a human one.
The editor's own refusal message says this at the point you meet it, rather than leaving you to find this page.
Next¶
- Importing a Dify app: import_dify.md.
- Debugging a run: debugging.
- Running a shared instance: hosting.
The reading order says which task each page covers; separate n8n-import and gap-card pages are backlog.