Skip to content

fix(web): allow duplicate Output node variables on mutually exclusive branches - #39785

Open
lyfuci wants to merge 2 commits into
langgenius:mainfrom
lyfuci:fix/end-output-duplicate-branch-aware
Open

fix(web): allow duplicate Output node variables on mutually exclusive branches#39785
lyfuci wants to merge 2 commits into
langgenius:mainfrom
lyfuci:fix/end-output-duplicate-branch-aware

Conversation

@lyfuci

@lyfuci lyfuci commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Output (End) nodes on mutually exclusive branches may reuse the same output variable name.

Since v1.15.0 (#35511) the editor rejects any two Output nodes that share an output variable name.
The check exists for a good reason — per #35510 item 3, two Output nodes that both emit the same name
silently overwrite each other — but it ignores graph topology. A workflow whose true and false
branches each end in their own Output node therefore cannot be published, even though only one of those
Output nodes can ever run.

This makes the check branch-aware instead of removing it: a duplicate is reported only when the two
Output nodes can actually run in the same execution.

Worth noting that the backend already treats same-named outputs as one variable —
WorkflowToolConfigurationUtils.get_workflow_graph_output collapses them, with "Later end nodes
override duplicated variable definitions."
For mutually exclusive branches that is exactly the schema a
user wants: a single result regardless of which branch ran. Nothing server-side rejects the duplicate,
so the editor was the only thing blocking it.

Fixes #38440

Credit to @EvanYao826, who diagnosed this in #38488 and correctly identified
getDuplicateEndOutputMessages as the cause. That PR removes the check outright, which also drops the
protection for Output nodes on parallel branches (#35510 item 3) and for a variable declared twice
inside one Output node; this takes the narrower route instead.

How it works

Exclusive branching is already visible in the graph: edges leaving one node through distinct
sourceHandles
are alternatives, whereas several edges leaving the same handle are a parallel
fan-out where every target runs. That covers every branching node in the editor without a node-type
allow-list — If/Else (true / case ids / false), Question Classifier (class ids), error handling
(source vs fail-branch) and Human Input (action ids vs __timeout).

getDuplicateEndOutputVariables walks the graph from its entry nodes and records, for each node, the
sets of branch decisions that lead to it (branch node -> handle taken). Two Output nodes are mutually
exclusive when every way of reaching one contradicts every way of reaching the other. Tracking whole
decision sets rather than one branch at a time matters, because failure branches often merge into a
single shared error Output node — checking branches independently reports a false positive there.

Details:

  • Sets are de-duplicated and the walk runs to a fixpoint, so cyclic graphs terminate. A node exceeding
    the tracked combination limit falls back to "may run together", keeping the warning rather than
    dropping a real conflict.
  • The choice of entry node is modelled as one more branch, so Output nodes belonging to different
    triggers do not collide.
  • Temporary edges (data._isTemp, injected by handleNodeSelect while highlighting a node's variable
    dependencies) are skipped. They use a source_tmp handle and would otherwise invent paths that bypass
    real branches, making the warning appear and disappear as nodes are selected.
  • When no variable name is repeated at all, no graph analysis runs, so the common case stays cheap
    inside the existing useMemo.

Preserved behaviour: Output nodes on parallel branches sharing a name are still reported, a single Output
node declaring the same name twice is still reported, and blank names are still ignored.

Screenshots

No visual change — this is validation logic, so the only observable difference is that a workflow which
previously refused to publish now publishes. The failing state is captured in #38440. Repro and
verification steps below instead.

How to verify

  1. Create a workflow: Start → If/Else, with the true branch going to one Output node and the false
    branch to another.
  2. Give both Output nodes an output variable with the same name, e.g. result.
  3. Before: publishing fails with Duplicate Output node variable "result"..., and the workflow cannot be
    test-run either, since useWorkflowRunValidation gates on the same checklist.
  4. After: publishing and running both succeed.
  5. Still reported, as before: pointing Start at two Output nodes directly (a parallel fan-out, where
    both run) with the same variable name, and a single Output node declaring the same name twice.

Changes

  • web/app/components/workflow/utils/end-output-conflicts.ts (new): pure
    getDuplicateEndOutputVariables(nodes, edges) returning the conflicting variable names per Output
    node id.
  • web/app/components/workflow/hooks/use-checklist.ts: getDuplicateEndOutputMessages delegates to
    that helper and only maps results to i18n messages; both call sites pass edges.
  • web/app/components/workflow/utils/__tests__/end-output-conflicts.spec.ts (new): 20 unit tests —
    if/else, success vs fail-branch, Human Input timeout, several classifier classes merging into one
    Output node, a shared fallback Output node behind nested branches, if/else chains, branch bypass,
    multiple entry nodes, parentId scopes, cyclic graphs, temporary edges, disconnected Output nodes,
    blank names, partial name overlap.
  • web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts: added a checklist-level test
    for the How can different end nodes output the same key variable? #38440 topology. The existing should detect duplicate output variables across end nodes test
    is unchanged and still passes, since its graph is a parallel fan-out.

One thing left for maintainers to decide: errorMsg.duplicateOutputVariable still reads "Output node
variable names must be unique."
That is accurate whenever the warning now fires, but it slightly
overstates the rule. I left the copy alone rather than machine-translate a reworded string into 22
locales — happy to update it if you tell me the wording you want.

Checklist

  • This change requires a documentation update: no
  • I understand that this PR may be closed in case there was no previous discussion or issues.
  • I've added a test for each change that was introduced, and I tried as much as possible to make a
    single atomic change.
  • I've updated the documentation accordingly. (not applicable)
  • Frontend-only change. pnpm exec vp staged is clean; note it has to run from the repo root, since
    the staged config lives in the root vite.config.ts rather than web/. Also ran
    vp test app/components/workflow (600 files / 3145 tests, all passing).

From Claude Code

… branches

Publishing was rejected whenever two Output (End) nodes declared the same
output variable name, regardless of graph topology. When those nodes sit on
mutually exclusive branches only one of them runs per execution, so reusing a
variable name there is legitimate and should not block publishing.

Move the graph reasoning into getDuplicateEndOutputVariables, which walks the
graph from its entry nodes and records the branch decisions that lead to each
node. Two Output nodes are exclusive when every way of reaching one contradicts
every way of reaching the other, so a duplicate is reported only when both can
actually run in the same execution. This keeps the existing protection for
Output nodes on parallel branches and for a variable declared twice by a single
Output node.

Temporary dependency-highlight edges are skipped, since their source_tmp handles
would otherwise invent paths that bypass real branches.

Fixes langgenius#38440
@lyfuci
lyfuci requested review from iamjoel and zxhlyh as code owners July 30, 2026 03:36
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jul 30, 2026
@github-actions github-actions Bot added the web This relates to changes on the web. label Jul 30, 2026

@iamjoel iamjoel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When mutually exclusive branches define the same Output variable with different types—for example, result is a string on the true branch and an object on the false branch—the current implementation tracks only the variable name and allows the duplicate because the branches are mutually exclusive.
However, the workflow tool output schema merges same-named definitions, with the later End node overriding the earlier type. As a result, one branch may return a value that does not match the published tool schema, potentially breaking consumers.
Please only allow same-named outputs when their value_types are compatible. Otherwise, retain a validation error or introduce a dedicated output-schema conflict error, with a regression test covering mismatched types such as string and object.

… types

Allowing a reused output variable name on mutually exclusive branches is only
safe when the declared types agree. The published output keeps a single
definition per name — get_workflow_graph_output lets the later Output node win —
so a branch declaring `result` as an object while another declares it as a
string could return a value that does not match the schema consumers were given.

Track value_type alongside the variable name and report a dedicated
conflictingOutputVariableTypes error for that case, keeping the existing
duplicate-name error for Output nodes that can run in the same execution. Type
compatibility mirrors filterVar / filterVarByType: `any` matches everything, and
an absent type cannot prove a clash.
@lyfuci

lyfuci commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Good catch, thank you — you're right, and it's the same merge behaviour I cited as an argument for the
change, so I should have followed it through to the type. Fixed in 0044aaf.

value_type is now tracked alongside the variable name, and the two failure modes are reported
separately:

  • duplicateName — Output nodes that can run in the same execution share a name, so a value is lost.
    Unchanged behaviour.
  • conflictingTypes — Output nodes on mutually exclusive branches reuse a name but declare different
    types. No value is lost, but the published output keeps one definition per name, so a branch could
    return something the schema does not describe.

Reusing a name across exclusive branches is still allowed when the types agree.

I added a dedicated error rather than reusing the duplicate-name one, since the fix a user needs is
different: with the old copy ("variable names must be unique") they'd rename the variable, when what they
actually want is to align the types.

errorMsg.conflictingOutputVariableTypes:
  Output node variable "{{variable}}" is declared as {{types}} on branches that never run together.
  Output nodes reusing a variable name must declare the same type, because the published output keeps
  one definition per name.

Type compatibility mirrors the existing filterVar / filterVarByType convention rather than
introducing a second rule — any matches everything, otherwise the types must be equal. Two cases I
chose deliberately, happy to change either:

  • A missing value_type does not report a conflict. It's optional on Variable, only written when a
    variable is picked through the reference picker (var-list.tsx sets value_type = varInfo?.type), so
    graphs saved earlier or a reference the editor could not resolve would otherwise get blocked with no
    way for the user to see why.
  • integer and number are treated as distinct, since that is what the existing convention does. Say
    the word if you'd rather they were interchangeable.
  • When both problems apply at once, duplicateName is reported, since a lost value is the more serious
    one.

Regression tests added, including the string/object case you asked for:

topology expected
exclusive branches, result as string vs object conflictingTypes, types [object, string]
exclusive branches, result as string on both no conflict
exclusive branches, any vs object no conflict
exclusive branches, one side with no declared type no conflict
parallel fan-out, string vs object duplicateName (lost value wins)

25 unit tests plus 2 checklist-level tests; pnpm check clean, and the app/components/workflow suite
passes locally (600 files / 3151 tests).

One thing to flag: I added the new key to en-US and zh-Hans only, expecting
trigger-i18n-sync.ymltranslate-i18n-claude.yml to fill the remaining locales after merge, since
that workflow watches web/i18n/en-US/*.json. If you'd rather the PR carry all 23 up front, tell me and
I'll add them.

@lyfuci
lyfuci requested a review from iamjoel July 30, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files. web This relates to changes on the web.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How can different end nodes output the same key variable?

2 participants