From 6933a362f5974abf825e78c2716082650779ebd5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 23:27:14 +0000 Subject: [PATCH 1/2] Use unique class names for nested workflow classes in codegen When generating nested workflow contexts for inline subworkflow nodes, the nested workflow class name was created using createPythonClassName() directly without checking for collisions with existing class names. This caused issues when a node in a subworkflow had the same name as the outer inline subworkflow node, resulting in identical CodeResourceDefinition values for both the base node class and the nested workflow class. This fix uses workflowContext.getUniqueClassName() and addUsedClassName() to ensure nested workflow class names are unique within the shared classNames registry, similar to how node class names are handled. Co-Authored-By: harrison@vellum.ai --- .../src/generators/nodes/bases/nested-workflow-base.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ee/codegen/src/generators/nodes/bases/nested-workflow-base.ts b/ee/codegen/src/generators/nodes/bases/nested-workflow-base.ts index fe5ff28727..2b014ae82e 100644 --- a/ee/codegen/src/generators/nodes/bases/nested-workflow-base.ts +++ b/ee/codegen/src/generators/nodes/bases/nested-workflow-base.ts @@ -9,7 +9,6 @@ import { NodeAttributeGenerationError } from "src/generators/errors"; import { StrInstantiation } from "src/generators/extensions/str-instantiation"; import { WorkflowProjectGenerator } from "src/project"; import { WorkflowDataNode, WorkflowRawData } from "src/types/vellum"; -import { createPythonClassName } from "src/utils/casing"; export abstract class BaseNestedWorkflowNode< T extends WorkflowDataNode, @@ -68,7 +67,9 @@ export abstract class BaseNestedWorkflowNode< protected generateNestedWorkflowContexts(): Map { const nestedWorkflowLabel = `${this.nodeContext.getNodeLabel()} Workflow`; - const nestedWorkflowClassName = createPythonClassName(nestedWorkflowLabel); + const nestedWorkflowClassName = + this.workflowContext.getUniqueClassName(nestedWorkflowLabel); + this.workflowContext.addUsedClassName(nestedWorkflowClassName); const innerWorkflowData = this.getInnerWorkflowData(); From 74f8ea0bfcaf4f3348fc7c51267403cd93318d71 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 15:45:04 +0000 Subject: [PATCH 2/2] Add test for nested workflow class name collision (APO-2207) Tests that when an internal node has the same name as what the nested workflow class would be named, the nested workflow class gets a unique name. This verifies the fix in nested-workflow-base.ts that uses getUniqueClassName() instead of createPythonClassName() directly. Co-Authored-By: harrison@vellum.ai --- .../inline-subworkflow-node.test.ts.snap | 36 ++++++++++ .../nodes/inline-subworkflow-node.test.ts | 67 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/ee/codegen/src/__test__/nodes/__snapshots__/inline-subworkflow-node.test.ts.snap b/ee/codegen/src/__test__/nodes/__snapshots__/inline-subworkflow-node.test.ts.snap index 68198916d4..d22c88cb74 100644 --- a/ee/codegen/src/__test__/nodes/__snapshots__/inline-subworkflow-node.test.ts.snap +++ b/ee/codegen/src/__test__/nodes/__snapshots__/inline-subworkflow-node.test.ts.snap @@ -158,6 +158,42 @@ class MyNode1Display(BaseTemplatingNodeDisplay[MyNode1]): " `; +exports[`InlineSubworkflowNode > nested workflow class name collision > should generate unique nested workflow class name when internal node has same name 1`] = ` +"from vellum.workflows.nodes.displayable import InlineSubworkflowNode + +from .workflow import MyNodeWorkflow + + +class MyNode(InlineSubworkflowNode): + subworkflow = MyNodeWorkflow +" +`; + +exports[`InlineSubworkflowNode > nested workflow class name collision > should generate unique nested workflow class name when internal node has same name 2`] = ` +"from vellum.workflows import BaseWorkflow + +from .nodes.my_node_workflow import MyNodeWorkflow1 + + +class MyNodeWorkflow(BaseWorkflow): + graph = MyNodeWorkflow1 + + class Outputs(BaseWorkflow.Outputs): + final_output = MyNodeWorkflow1.Outputs.result +" +`; + +exports[`InlineSubworkflowNode > nested workflow class name collision > should generate unique nested workflow class name when internal node has same name 3`] = ` +"from vellum.workflows.nodes.displayable import TemplatingNode +from vellum.workflows.state import BaseState + + +class MyNodeWorkflow1(TemplatingNode[BaseState, str]): + template = """Hello, World!""" + inputs = {} +" +`; + exports[`InlineSubworkflowNode > with state > inline subworkflow node file with state should include all three generic type parameters 1`] = ` "from vellum.workflows.nodes.displayable import InlineSubworkflowNode as BaseInlineSubworkflowNode from vellum.workflows.state import BaseState diff --git a/ee/codegen/src/__test__/nodes/inline-subworkflow-node.test.ts b/ee/codegen/src/__test__/nodes/inline-subworkflow-node.test.ts index ed08e51d2a..2c6229b8fe 100644 --- a/ee/codegen/src/__test__/nodes/inline-subworkflow-node.test.ts +++ b/ee/codegen/src/__test__/nodes/inline-subworkflow-node.test.ts @@ -226,6 +226,73 @@ describe("InlineSubworkflowNode", () => { }); }); + describe("nested workflow class name collision", () => { + /** + * Tests that when an internal node has the same name as what the nested workflow + * class would be named, the nested workflow class gets a unique name. + * This is the APO-2207 scenario where: + * - Outer inline subworkflow node labeled "My Node" → nested workflow class "MyNodeWorkflow" + * - Internal node labeled "My Node Workflow" → would also be "MyNodeWorkflow" + * The fix ensures the nested workflow class gets renamed to avoid collision. + */ + beforeEach(async () => { + const workflowContext = workflowContextFactory({ + absolutePathToOutputDirectory: tempDir, + moduleName: "code", + }); + + // GIVEN an inline subworkflow node labeled "My Node" + // AND an internal node labeled "My Node Workflow" (same as what the nested workflow class would be named) + const nodeData = inlineSubworkflowNodeDataFactory({ + label: "My Node", + nodes: [templatingNodeFactory({ label: "My Node Workflow" }).build()], + }).build(); + + const nodeContext = (await createNodeContext({ + workflowContext, + nodeData, + })) as InlineSubworkflowNodeContext; + + const node = new InlineSubworkflowNode({ + workflowContext, + nodeContext, + }); + + // WHEN we generate the code + await node.persist(); + }); + + it(`should generate unique nested workflow class name when internal node has same name`, async () => { + // THEN the outer node file should reference a unique workflow class name + const outerNodeContent = await readFile( + join(tempDir, "code", "nodes", "my_node", "__init__.py"), + "utf-8" + ); + expect(outerNodeContent).toMatchSnapshot(); + + // AND the nested workflow file should have the unique class name + const workflowContent = await readFile( + join(tempDir, "code", "nodes", "my_node", "workflow.py"), + "utf-8" + ); + expect(workflowContent).toMatchSnapshot(); + + // AND the internal node should keep its original class name + const internalNodeContent = await readFile( + join( + tempDir, + "code", + "nodes", + "my_node", + "nodes", + "my_node_workflow.py" + ), + "utf-8" + ); + expect(internalNodeContent).toMatchSnapshot(); + }); + }); + describe("with state", () => { beforeEach(async () => { const workflowContext = workflowContextFactory({