From a2eecbdaef13a65d6f85ca93012cb36875f6aee1 Mon Sep 17 00:00:00 2001 From: maria-codesignal Date: Thu, 26 Feb 2026 12:08:56 -0500 Subject: [PATCH 1/4] Remove end node --- apps/web/index.html | 3 --- apps/web/src/app/workflow-editor.ts | 11 +++-------- apps/web/src/data/help-content.ts | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/web/index.html b/apps/web/index.html index 6aba8e9..6b6164e 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -39,9 +39,6 @@

Nodes

User Approval
-
- End -
diff --git a/apps/web/src/app/workflow-editor.ts b/apps/web/src/app/workflow-editor.ts index a639af7..65aafe8 100644 --- a/apps/web/src/app/workflow-editor.ts +++ b/apps/web/src/app/workflow-editor.ts @@ -1722,7 +1722,6 @@ export class WorkflowEditor { case 'approval': return { prompt: 'Review and approve this step.', collapsed: true }; case 'start': - case 'end': return { collapsed: true }; default: return { collapsed: true }; @@ -1802,7 +1801,7 @@ export class WorkflowEditor { if (!node.data) node.data = {}; if (node.data.collapsed === undefined) { - node.data.collapsed = node.type === 'start' || node.type === 'end'; + node.data.collapsed = node.type === 'start'; } const hasSettings = this.nodeHasSettings(node); el.classList.toggle('expanded', !node.data.collapsed); @@ -1954,7 +1953,6 @@ export class WorkflowEditor { return `${escapeHtml(name)}`; } if (node.type === 'start') return 'Start'; - if (node.type === 'end') return 'End'; if (node.type === 'if') return 'Condition'; if (node.type === 'approval') return 'User Approval'; return `${node.type}`; @@ -2313,13 +2311,11 @@ export class WorkflowEditor { } if (node.type !== 'start') { - const inputTooltip = node.type === 'end' ? 'End input' : 'Input'; - const portIn = this.createPort(node.id, 'input', 'port-in', inputTooltip, this.getNodeHeaderPortTop(node)); + const portIn = this.createPort(node.id, 'input', 'port-in', 'Input', this.getNodeHeaderPortTop(node)); el.appendChild(portIn); } - if (node.type !== 'end') { - if (node.type === 'if') { + if (node.type === 'if') { const conditions = this.getIfConditions(node); if (this.shouldAggregateCollapsedIfPorts(node)) { const aggregateConditionPort = this.createPort( @@ -2399,7 +2395,6 @@ export class WorkflowEditor { const outputTooltip = node.type === 'start' ? 'Next step' : 'Output'; el.appendChild(this.createPort(node.id, 'output', 'port-out', outputTooltip, this.getNodeHeaderPortTop(node))); } - } } createPort( diff --git a/apps/web/src/data/help-content.ts b/apps/web/src/data/help-content.ts index 7ad99cd..82f1a24 100644 --- a/apps/web/src/data/help-content.ts +++ b/apps/web/src/data/help-content.ts @@ -12,7 +12,7 @@ export const helpContent = `

Overview

-

The Agentic Workflow Builder lets you compose agent flows with Start, Agent, Condition, Approval, and End nodes. Drag nodes, connect them, configure prompts, and run against the server-side workflow engine.

+

The Agentic Workflow Builder lets you compose agent flows with Start, Agent, Condition, and Approval nodes. Drag nodes, connect them, configure prompts, and run against the server-side workflow engine.

From 1770579869abd63cffb6d94de69024f773126a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Galv=C3=A3o?= Date: Thu, 26 Feb 2026 15:54:10 -0300 Subject: [PATCH 2/4] fix(workflow): restore start output and complete end-node cleanup --- CLAUDE.md | 2 +- README.md | 2 +- apps/web/src/app/workflow-editor.test.ts | 33 ++++++++++++++++++++++++ packages/types/src/index.ts | 2 +- packages/workflow-engine/src/index.ts | 22 +++++++++------- 5 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/app/workflow-editor.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index db2645e..1e0792e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Visual workflow builder for agentic LLM pipelines. Users drag-and-drop nodes (Start, Agent, If/Else, Approval, End) onto a canvas, connect them, configure LLM prompts and branching logic, then execute workflows server-side against OpenAI. Run results are persisted as JSON audit trails. +Visual workflow builder for agentic LLM pipelines. Users drag-and-drop nodes (Start, Agent, If/Else, Approval) onto a canvas, connect them, configure LLM prompts and branching logic, then execute workflows server-side against OpenAI. Run results are persisted as JSON audit trails. ## Monorepo Layout diff --git a/README.md b/README.md index 609b0b4..ec55175 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Agentic Workflow Builder -Agentic Workflow Builder is a web app for visually composing, executing, and auditing LLM workflows. Drag Start, Agent, If/Else, Approval, and End nodes onto the canvas, connect them with Bezier edges, configure prompts inline, and run the flow through a server-side engine that records every step for later review. +Agentic Workflow Builder is a web app for visually composing, executing, and auditing LLM workflows. Drag Start, Agent, If/Else, and Approval nodes onto the canvas, connect them with Bezier edges, configure prompts inline, and run the flow through a server-side engine that records every step for later review. ## Repository Layout diff --git a/apps/web/src/app/workflow-editor.test.ts b/apps/web/src/app/workflow-editor.test.ts new file mode 100644 index 0000000..432fa0d --- /dev/null +++ b/apps/web/src/app/workflow-editor.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { WorkflowEditor } from './workflow-editor'; + +describe('WorkflowEditor renderPorts', () => { + it('renders an output port for start nodes', () => { + const renderPorts = ( + WorkflowEditor.prototype as unknown as { + renderPorts: ( + node: { id: string; type: string }, + el: { appendChild: (port: { handle: string }) => void } + ) => void; + } + ).renderPorts; + const createPort = vi.fn((_nodeId: string, handle: string) => ({ handle })); + const appended: Array<{ handle: string }> = []; + const element = { + appendChild: (port: { handle: string }) => { + appended.push(port); + } + }; + + renderPorts.call( + { createPort }, + { id: 'node_start', type: 'start' }, + element + ); + + expect(createPort).toHaveBeenCalledTimes(1); + expect(createPort).toHaveBeenCalledWith('node_start', 'output', 'port-out'); + expect(appended).toEqual([{ handle: 'output' }]); + }); +}); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c6bbd73..8cef669 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,4 +1,4 @@ -export type NodeType = 'start' | 'agent' | 'if' | 'approval' | 'end' | string; +export type NodeType = 'start' | 'agent' | 'if' | 'approval' | string; export interface BaseNodeData { collapsed?: boolean; diff --git a/packages/workflow-engine/src/index.ts b/packages/workflow-engine/src/index.ts index 77f3c71..3d157e3 100644 --- a/packages/workflow-engine/src/index.ts +++ b/packages/workflow-engine/src/index.ts @@ -260,17 +260,25 @@ export class WorkflowEngine { } private normalizeGraph(graph: WorkflowGraph): WorkflowGraph { + const removedNodeIds = new Set(); const nodes = Array.isArray(graph.nodes) - ? graph.nodes.map((node) => { - if (node.type === 'input') { - return { ...node, type: 'approval' }; + ? graph.nodes.flatMap((node) => { + const normalizedNode = node.type === 'input' ? { ...node, type: 'approval' } : node; + if (normalizedNode.type === 'end') { + removedNodeIds.add(normalizedNode.id); + return []; } - return node; + return [normalizedNode]; }) : []; return { nodes, - connections: Array.isArray(graph.connections) ? graph.connections : [] + connections: Array.isArray(graph.connections) + ? graph.connections.filter( + (connection) => + !removedNodeIds.has(connection.source) && !removedNodeIds.has(connection.target) + ) + : [] }; } @@ -339,8 +347,6 @@ export class WorkflowEngine { this.waitingForInput = true; this.log(node.id, 'wait_input', 'Waiting for user approval'); return undefined; - case 'end': - return undefined; default: this.log(node.id, 'warn', `Unknown node type "${node.type}" skipped`); } @@ -456,8 +462,6 @@ export class WorkflowEngine { return 'condition node'; case 'approval': return 'approval node'; - case 'end': - return 'end node'; default: return `${node.type} node`; } From 36d5147493a7ac888a28a98f71ec62de8b1fbb3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Galv=C3=A3o?= Date: Thu, 26 Feb 2026 15:59:33 -0300 Subject: [PATCH 3/4] test(web): align start-port regression assertion with tooltip args --- apps/web/src/app/workflow-editor.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/workflow-editor.test.ts b/apps/web/src/app/workflow-editor.test.ts index 432fa0d..5fcaadc 100644 --- a/apps/web/src/app/workflow-editor.test.ts +++ b/apps/web/src/app/workflow-editor.test.ts @@ -21,13 +21,13 @@ describe('WorkflowEditor renderPorts', () => { }; renderPorts.call( - { createPort }, + { createPort, getNodeHeaderPortTop: () => 24 }, { id: 'node_start', type: 'start' }, element ); expect(createPort).toHaveBeenCalledTimes(1); - expect(createPort).toHaveBeenCalledWith('node_start', 'output', 'port-out'); + expect(createPort).toHaveBeenCalledWith('node_start', 'output', 'port-out', 'Next step', 24); expect(appended).toEqual([{ handle: 'output' }]); }); }); From 86d0637aaea9215eceb0faca2613c1b4a520f5b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Galv=C3=A3o?= Date: Thu, 26 Feb 2026 16:01:54 -0300 Subject: [PATCH 4/4] refactor(engine): remove remaining legacy end-node normalization --- packages/workflow-engine/src/index.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/workflow-engine/src/index.ts b/packages/workflow-engine/src/index.ts index 3d157e3..1ecd789 100644 --- a/packages/workflow-engine/src/index.ts +++ b/packages/workflow-engine/src/index.ts @@ -260,25 +260,17 @@ export class WorkflowEngine { } private normalizeGraph(graph: WorkflowGraph): WorkflowGraph { - const removedNodeIds = new Set(); const nodes = Array.isArray(graph.nodes) - ? graph.nodes.flatMap((node) => { - const normalizedNode = node.type === 'input' ? { ...node, type: 'approval' } : node; - if (normalizedNode.type === 'end') { - removedNodeIds.add(normalizedNode.id); - return []; + ? graph.nodes.map((node) => { + if (node.type === 'input') { + return { ...node, type: 'approval' }; } - return [normalizedNode]; + return node; }) : []; return { nodes, - connections: Array.isArray(graph.connections) - ? graph.connections.filter( - (connection) => - !removedNodeIds.has(connection.source) && !removedNodeIds.has(connection.target) - ) - : [] + connections: Array.isArray(graph.connections) ? graph.connections : [] }; }