Skip to content

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
Broccolito merged 2 commits into
mainfrom
fix/workflow-from-chat-unsavable
Sep 12, 2026
Merged

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
Broccolito merged 2 commits into
mainfrom
fix/workflow-from-chat-unsavable

Conversation

@Broccolito

Copy link
Copy Markdown
Collaborator

D12 — a workflow created from a chat could not be saved at all

workflow::validate (the function POST /workflows/save calls) applies two parameter rules:

  • an optional parameter must carry a default"Optional parameters missing default values in the workflow: X. Please provide defaults."
  • every parameter must be referenced by a {{ … }} 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.md asks for parameters and 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):

document before after
unreferenced optional, no default HTTP 400 Optional parameters missing default values in the workflow: output_format. Please provide defaults. unchanged — still 400
unreferenced optional, with default HTTP 400 \nUnnecessary parameter definitions: output_format. 400, and the message now says what to do
the same two parameters, one of them referenced from prompt saved

The pre-fix text of the second message was read off origin/main's validator directly: the new test an_unreferenced_parameter_is_still_refused_and_the_refusal_says_why failed on it with Unnecessary parameter definitions: output_format. and nothing after the period.

Which half I changed, and why

The generator. Agent::create_workflow now 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, via parse_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 /workflow and the model's own generate all come through that one function — which is what tests/workflow_capture_parity.rs exists 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 keyed gene_symbol reports "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:

Unnecessary parameter definitions: output_format. Nothing in the workflow refers to it,
so a value for it would be collected and then discarded — write {{ output_format }} into
`prompt` or `instructions`, or remove the definition. Giving it a `default` does not help.

Both arms also sort their names now. They come out of a HashSet difference, 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 is

No default — this workflow will not focus one. Chats it starts search every base above, and a write that names no base asks which one to use.

The 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

test defect fails on origin/main with
crates/biorouter/tests/workflow_capture_parity.rs::a_captured_workflow_never_declares_a_parameter_the_document_does_not_use D12, generator left: ["gene_symbol", "output_format"] right: ["gene_symbol"]
crates/biorouter/tests/workflow_capture_parity.rs::an_unreferenced_parameter_is_still_refused_and_the_refusal_says_why D12, refusal text and 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" D14, component no resource-picker-default-summary node
WorkflowResourcePicker.test.tsx › "names the default on the card when one is set" D14, component same
WorkflowFormFields.test.tsx › Knowledge bases card › "says the workflow will have no default when none is named" D14, wiring + wording same
WorkflowFormFields.test.tsx › Knowledge bases card › "names the default when one is named" D14, wiring same

The frontend four were confirmed red by checking WorkflowResourcePicker.tsx and WorkflowFormFields.tsx back out from origin/main and 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 on versa_azure / gpt-5.5-2026-04-24:

  1. a chat asking for APOE's disease associations;
  2. Chat summary → Make workflow — the card read Default: Soul, and after clearing the Default control it read the no-default sentence above;
  3. Create workflow → saved;
  4. Workflows → Use workflow → Trust and execute → the parameter dialog asked for gene_symbol and offered detail_level at its default brief → 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 extensions block and the rest are unchanged):

version: 1.0.0
title: Brief gene disease association summary
description: Generates a short markdown summary of disease associations for a named gene from general biomedical knowledge. Useful for quick orientation before deeper sourced lookup.
instructions: |-
  Summarise the disease associations for the requested gene at the requested level of detail, using concise biomedical language. If the user asks for a brief summary, return exactly three short markdown bullets, prioritising the best-established disease links and noting important allele, variant, or mechanism context when relevant.

  Do not use tools unless the user explicitly asks for sourced, current, or database-backed evidence. If answering from general knowledge, avoid fabricating certainty: state when an association is indirect, risk-modifying, or context-dependent.
prompt: Summarise the disease associations for the gene {{gene_symbol}} at a {{detail_level}} level of detail, as a short markdown bullet list. Do not use tools unless I ask for sourced evidence.
knowledge_bases:
  visible:
  - soul
settings:
  biorouter_provider: versa_azure
  biorouter_model: gpt-5.5-2026-04-24
  temperature: 0.0
activities:
- Summarise a gene
- Three bullet summary
- List major diseases
- Explain variant effects
author:
  contact: wgu
parameters:
- key: gene_symbol
  input_type: string
  requirement: user_prompt
  description: HGNC gene symbol to summarise, for example APOE, TP53, or BRCA1
- key: detail_level
  input_type: select
  requirement: optional
  description: How much detail the summary should include
  default: brief
  options:
  - brief
  - standard
  - detailed

Note knowledge_bases carries visible and no default — 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": 80 for a number parameter, WorkflowParameter.default is Option<String>, and serde failed the whole array

WARN biorouter::agents::agent: Dropping generated workflow parameters that did not parse:
invalid type: integer `80`, expected a string

— 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 pass
  • cargo test -p biorouter --lib4014 passed, 0 failed
  • cargo test -p biorouter-server --lib665 passed, 0 failed
  • cargo test -p biorouter --test workflow_capture_parity4 passed (2 of them red on origin/main)
  • npx vitest run src/components/workflows139 passed across 8 files
  • npm run lint:check — typecheck, eslint, themes, 332 contrast assertions, token mirrors — all pass
  • npx prettier --check on the four files touched — clean

🤖 Generated with Claude Code

"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".
@Broccolito

Copy link
Copy Markdown
Collaborator Author

The one red check is main's, not this branch's

test (ubuntu-latest) fails in biorouter-server --test session_store_survives_a_relocated_path_root:

thread 'test_sandbox::tests::the_session_database_is_not_the_developers' panicked at
crates/biorouter-server/tests/../src/test_sandbox.rs:132:9:
expected the per-process sandbox, got /tmp/.tmpNGR7UD/data
test result: FAILED. 2 passed; 1 failed

That binary does not exist on this branch — it arrives from main, in 931d1df0 (#282), and the branch point here is 47926672. It is red on main itself, with the identical panic and the identical message: run 34686706878 at 055cb087 (Merge pull request #280), job test (ubuntu-latest).

#286 already owns the fix, so nothing is changed here for it. test (macos-latest) and test (windows-latest) pass, as does every other check, including Unit tests (vitest) and Static checks, which are the ones that cover the code this PR touches.

@Broccolito
Broccolito merged commit a16a0cb into main Sep 12, 2026
16 checks passed
@Broccolito
Broccolito deleted the fix/workflow-from-chat-unsavable branch September 12, 2026 12:07
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant