fix(workflow): a workflow captured from a chat can be saved (D12), and the KB card says when it will have no default (D14) - #284
Merged
Conversation
"Create workflow from this chat" produced documents no route would
accept. `workflow::validate` refuses a parameter the document refers to
nowhere ("Unnecessary parameter definitions") and separately refuses an
`optional` parameter with no `default` ("Optional parameters missing
default values"). Each rule is right on its own; together they leave an
unreferenced optional parameter with no accepted form at all — removing
its default trades one refusal for the other — so the save failed both
ways and the feature could not complete.
The generator is the half that is wrong. `workflow.md` asks for
parameters and for their `{{ key }}` references as two separate
instructions, and a model that obeys the first and forgets the second
emits a parameter the run would collect a value for and then discard.
`Agent::create_workflow` now drops those, reading "referenced" exactly
the way the validator reads it — every `{{ … }}` in the serialized
document — so the two can never disagree. It is pruned in the generator
rather than in each of the three capture surfaces, because the route,
the CLI's `/workflow` and the model's own `generate` all come through
that one function.
Neither validator is relaxed. The "unnecessary" arm is the only thing
that catches a key typo, and it reports both halves of the mismatch. What
changes is that the message now names the fix instead of only the fault,
since the obvious guess — give it a default — is the move that trades one
refusal for the other. Both arms also sort their names: they come out of
a `HashSet` difference, so an unsorted join gave the same fault a
different message on each run.
The "Knowledge bases" card also now states, on the card, whether the
workflow will have a default base. Which base is the default is marked
only on a row inside a closed popover, so a chat with no pinned primary —
captured correctly as `default: null` — looked exactly like a card whose
default had gone missing, while the card's own description promised "which
one is focused by default".
Collaborator
Author
The one red check is
|
Broccolito
added a commit
that referenced
this pull request
Sep 12, 2026
Clean — no overlap with this branch's five files. main moved twice during verification; the rule recorded in the previous merge commit is unaffected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
D12 — a workflow created from a chat could not be saved at all
workflow::validate(the functionPOST /workflows/savecalls) applies two parameter rules:optionalparameter must carry adefault— "Optional parameters missing default values in the workflow: X. Please provide defaults."{{ … }}somewhere in the document — "Unnecessary parameter definitions: X."Each is right on its own. Together they close over a parameter the document refers to nowhere: with no default the first arm fires, with a default the second does. There is no value of that parameter for which the save succeeds.
"Create workflow from this chat" emitted exactly that.
crates/biorouter/src/prompts/workflow.mdasks forparametersand for their{{ key }}references as two separate instructions, and a model that obeys the first and forgets the second produces an unsavable document.Measured, 2026-09-12. Posting the two documents to a running daemon (
POST /workflows/save, desktop dev build):optional, nodefaultOptional parameters missing default values in the workflow: output_format. Please provide defaults.optional, withdefault\nUnnecessary parameter definitions: output_format.promptThe pre-fix text of the second message was read off
origin/main's validator directly: the new testan_unreferenced_parameter_is_still_refused_and_the_refusal_says_whyfailed on it withUnnecessary parameter definitions: output_format.and nothing after the period.Which half I changed, and why
The generator.
Agent::create_workflownow drops parameters the document refers to nowhere. A parameter nothing reads is a question the run would ask and then discard, so keeping it is misleading even where it is accepted; and this is the same stance the function already takes towards a parameter list that does not parse — keep the expensive part, lose the part that cannot work, say so in the log. "Referenced" is read exactly the way the validator reads it (every{{ … }}in the serialized document, viaparse_workflow_content), so the two cannot disagree about what the word means. The prune lives in the generator, not in each of the three capture surfaces, because the HTTP route, the CLI's/workflowand the model's owngenerateall come through that one function — which is whattests/workflow_capture_parity.rsexists to hold.Not the validator. I did check whether the second message is too strict, as asked. I kept it, for two reasons. It is the only thing that catches a key typo, and it catches it as a pair:
{{ gene }}in the prompt beside a parameter keyedgene_symbolreports "Missing definitions … gene" and "Unnecessary … gene_symbol", which together name both halves of the mismatch; permitting a defaulted-but-unreferenced parameter would silence the half that names the typo'd definition. And a workflow is a shared document — an accepted dead parameter is dead weight in every copy of it.What did change is the message. It named the fault and stopped, and the obvious guess at a fix — give the parameter a default — is precisely the move that trades one refusal for the other. It now names the fix:
Both arms also sort their names now. They come out of a
HashSetdifference, so an unsorted join gave the same fault a different message on each run.D14 — the Knowledge bases card never said the workflow would have no default
Which base is the default is marked only on a row inside a closed popover. So a chat with no pinned primary — captured as
default: null, correctly, and #256's rule which this does not change — produced a card indistinguishable from one whose default had gone missing, while its own description promised "which one is focused by default".The picker now states it on the card itself, without opening anything:
Default: <base>when one is named, and otherwise the sentence the caller supplies. For knowledge bases that isThe line is suppressed when nothing is selected (the trigger already says "No KBs selected") and for pickers that have no default control at all (Skills, Extensions).
Fail-before tests
origin/mainwithcrates/biorouter/tests/workflow_capture_parity.rs::a_captured_workflow_never_declares_a_parameter_the_document_does_not_useleft: ["gene_symbol", "output_format"] right: ["gene_symbol"]crates/biorouter/tests/workflow_capture_parity.rs::an_unreferenced_parameter_is_still_refused_and_the_refusal_says_whyand now says how to fix it, naming the key: \nUnnecessary parameter definitions: output_format.WorkflowResourcePicker.test.tsx› "says there is no default, on the card, without opening the popover"resource-picker-default-summarynodeWorkflowResourcePicker.test.tsx› "names the default on the card when one is set"WorkflowFormFields.test.tsx› Knowledge bases card › "says the workflow will have no default when none is named"WorkflowFormFields.test.tsx› Knowledge bases card › "names the default when one is named"The frontend four were confirmed red by checking
WorkflowResourcePicker.tsxandWorkflowFormFields.tsxback out fromorigin/mainand re-running: 2 failed | 6 passed in the picker file, 2 failed | 39 passed in the form-fields file. With the fix, all 8 and all 41 pass.Verified in the running app, not only by unit test
Dev GUI on this branch's own
biorouterd, sandboxed config, real chat onversa_azure/gpt-5.5-2026-04-24:Default: Soul, and after clearing the Default control it read the no-default sentence above;gene_symboland offereddetail_levelat its defaultbrief→ the prompt rendered as "Summarise the disease associations for the gene TP53 at a brief level of detail…" and the run returned the three bullets the instructions demand.The YAML that now saves (trimmed to the parts at issue; the
extensionsblock and the rest are unchanged):Note
knowledge_basescarriesvisibleand nodefault— the exact state D14 is about, now stated on the card.Found in passing, not fixed here
A second, independent way the same modal loses its parameters, watched live on the second capture: the model wrote
"default": 80for anumberparameter,WorkflowParameter.defaultisOption<String>, and serde failed the whole array —— so every parameter was dropped, not just the offending one. Out of scope for this change and filed as a separate task; it has a trap of its own (a dropped-but-still-referenced parameter lands on the validator's other arm, "Missing definitions").
Gates run
cargo fmt --check— clean./scripts/clippy-lint.sh— all baseline checks passcargo test -p biorouter --lib— 4014 passed, 0 failedcargo test -p biorouter-server --lib— 665 passed, 0 failedcargo test -p biorouter --test workflow_capture_parity— 4 passed (2 of them red onorigin/main)npx vitest run src/components/workflows— 139 passed across 8 filesnpm run lint:check— typecheck, eslint, themes, 332 contrast assertions, token mirrors — all passnpx prettier --checkon the four files touched — clean🤖 Generated with Claude Code