Neural Inverse is Open Source →
GuidesWorkflow Composer
GuidesOpen Source ContributingWorkflow Composer

Workflow Composer

Open via the Agent Manager (Cmd+Alt+A). A visual node-graph editor for building, editing, and running multi-agent workflows without touching JSON.

Source: src/vs/workbench/contrib/neuralInverse/browser/composer/


Overview

The Workflow Composer replaces the form-based workflow builder with a full SVG canvas editor. It produces IWorkflowDefinition JSON (same format as hand-authored .inverse/workflows/*.json) and feeds directly into the existing WorkflowAgentService execution stack — no changes to the orchestrator, executor, or trigger manager.

No external graph library. Everything is built on SVG + pointer events + VS Code DI.


Architecture

composer/
├── composerModule.ts         — DI registration + barrel exports
├── service.ts                — IWorkflowComposerService interface
├── WorkflowComposerServiceImpl.ts — orchestrates all layers
├── model/
│   ├── composerModel.ts      — reactive in-memory graph state
│   ├── composerHistory.ts    — undo/redo (command pattern)
│   └── composerSerializer.ts — IWorkflowDefinition <-> graph model
├── canvas/
│   ├── workflowCanvas.ts     — SVG mount, pan/zoom, grid, minimap
│   ├── canvasRenderer.ts     — diff-based node/edge SVG rendering
│   ├── canvasLayout.ts       — Sugiyama topological auto-layout
│   └── canvasInteraction.ts  — pointer state machine, keyboard, drag-drop
├── nodes/
│   ├── nodeRegistry.ts       — 6 node types with config schemas + port defs
│   └── nodeRenderer.ts       — SVG icon paths, shape geometry helpers
├── edges/
│   ├── edgeValidator.ts      — cycle detection, port compatibility, fan-in
│   └── edgeRenderer.ts       — cubic bezier paths, animation helpers
└── panels/
    ├── nodePalette.ts        — draggable node catalogue (left sidebar)
    ├── propertyPanel.ts      — context-sensitive config editor (right sidebar)
    ├── triggerPanel.ts       — per-trigger-type configuration views
    └── runPanel.ts           — live execution dashboard

Model Layer

ComposerModel

model/composerModel.ts — the single source of truth for graph state.

interface IComposerNode {
  id: string;
  type: NodeType;        // 'agent' | 'trigger' | 'conditional' | 'transform' | 'output' | 'group'
  label: string;
  position: { x: number; y: number };
  size: { width: number; height: number };
  config: Record<string, unknown>;
  ports: IPortDefinition[];
  enabled: boolean;
  manuallyPositioned?: boolean;
}

interface IComposerEdge {
  id: string;
  sourceNodeId: string;
  sourcePortId: string;
  targetNodeId: string;
  targetPortId: string;
}

All mutations emit onDidChange events keyed by 'nodes' | 'edges' | 'selection' | 'viewport'. The canvas renderer subscribes to these and schedules diff updates via requestAnimationFrame.

ComposerHistory

model/composerHistory.ts — 100-deep undo/redo via command pattern.

CommandWhat it reverses
AddNodeCommandAdds/removes a node
RemoveNodeCommandRemoves/restores node + its edges
MoveNodeCommandMoves/restores node position
AddEdgeCommandAdds/removes an edge
RemoveEdgeCommandRemoves/restores an edge
UpdateNodeConfigCommandConfig change, stores old values per-key
UpdateNodeLabelCommandLabel rename
ToggleNodeEnabledCommandEnabled toggle

beginBatch() / endBatch() groups multiple commands into a single undo step. Used by multi-node delete and duplicate operations.

ComposerSerializer

model/composerSerializer.ts — converts between IComposerModel and IWorkflowDefinition.

  • Serialize: topological order → IWorkflowStep[], trigger node → trigger field, edge dependencies → step.dependsOn[]
  • Deserialize: builds nodes from steps, auto-lays out if no _composerLayout metadata present
  • Node positions stored under _composerLayout in the workflow JSON (underscore prefix = non-functional metadata, ignored by orchestrator)

Canvas Layer

WorkflowCanvas

canvas/workflowCanvas.ts — manages the root SVG element.

Layer order (back to front):

  1. Grid (dot pattern, updates on pan/zoom)
  2. Edges
  3. Nodes
  4. Overlay (drag-edge line, marquee rect)

Pan/zoom:

  • Wheel: zoom at cursor (Ctrl/Cmd + wheel) or pan (plain wheel)
  • Middle-click drag or Space+drag: pan
  • Zoom range: 0.25x–4x
  • zoomToFit(): fits all nodes with padding

Grid: dot pattern rendered as SVG <circle> elements. Spacing doubles at zoom < 0.5x for readability.

Minimap: fixed-position overlay (bottom-right). Populated by CanvasRenderer.

CanvasRenderer

canvas/canvasRenderer.ts — diff-based SVG update engine.

  • Subscribes to ComposerModel.onDidChange
  • Schedules renders via requestAnimationFrame (batches rapid model changes)
  • Viewport virtualization: nodes outside the visible rect + 200px margin are hidden (display: none), not removed
  • Node groups: <g data-node-id="..."> containing body rect/polygon, label, and port circles
  • Edge paths: cubic bezier <path> elements with transparent 16px hit area for click detection

Node shapes by type:

TypeShape
agent, transform, output, groupRounded rect (rx=6)
triggerRect with dashed stroke
conditionalDiamond (rotated square polygon)

Port colors by data type:

Data typeColor
flow#e0a84e (amber)
text#6ba3e8 (blue)
json#85c9a8 (green)
any#aaa

CanvasLayout

canvas/canvasLayout.ts — Sugiyama-style auto-layout.

  1. Assign layers: longest-path from sources (depth by max parent depth)
  2. Minimize crossings: barycenter heuristic, 4 sweeps (2 forward + 2 backward)
  3. Assign positions: fixed horizontal spacing (280px between layers, 40px between nodes in a layer)

Nodes with manuallyPositioned = true are preserved in place unless preserveManualPositions: false is passed.

CanvasInteraction

canvas/canvasInteraction.ts — pointer event state machine.

States:

  • idle
  • panning — middle-click or Space+drag
  • dragging-node — left-click on node body, snaps to 20px grid
  • dragging-edge — mousedown on output port, shows live dashed line
  • marquee — Shift+drag on background, selects enclosed nodes

Keyboard:

KeyAction
Delete / BackspaceDelete selected
Ctrl/Cmd+ASelect all
Ctrl/Cmd+DDuplicate selected
Ctrl/Cmd+ZUndo
Ctrl/Cmd+Shift+Z / Ctrl+YRedo

Drag-drop from palette: listens for drop events on the canvas container. Payload is JSON with nodeType, configOverrides, and label.


Node System

NodeRegistry

nodes/nodeRegistry.ts — defines all node types.

Each type has:

  • ports: input/output port definitions with data types
  • defaultConfig: initial config values
  • configSchema: field definitions for the property panel
  • defaultSize, color, icon

6 built-in types:

TypePortsUse
trigger1 out (flow)Starts the workflow
agent1 in (flow), 1 out (text)LLM agent step
conditional1 in, 2 out (true/false)Branch on output
transform2 in, 1 outMerge/map outputs
output1 inTerminal sink
groupnoneVisual container

Port compatibility:

Source typeAccepts target
flowflow, any
texttext, any
jsonjson, any, text
anyall types

Edge Validation

edges/edgeValidator.ts — all connection rules enforced before an edge is added.

Checks in order:

  1. No self-connection
  2. Source must be output port, target must be input port
  3. Port type compatibility
  4. No duplicate edge
  5. Max fan-in: agents = 1, transforms = 10, outputs = 5
  6. Cycle detection (DFS from source checking if target is reachable back)

validateGraph() runs the full check across all edges and nodes, returning categorized errors and warnings. Used by the toolbar Validate button and before runCurrent().


Panels

NodePalette (left sidebar)

panels/nodePalette.ts

  • Categories: Triggers, Agents (from AgentStoreService.getAgents()), Logic, Output
  • Search filters across label, description, and category
  • Items are HTML draggable=true — drop on canvas fires CanvasInteraction.onDrop
  • Drag payload: JSON.stringify({ nodeType, configOverrides, label })

PropertyPanel (right sidebar)

panels/propertyPanel.ts

Renders when a node is selected. Field types supported:

Schema typeRenders as
string<input type="text">
number<input type="number">
boolean<input type="checkbox">
select<select> with options
textarea<textarea> (monospace, resizable)
toolsScrollable checklist from _getToolNames()

Fields with visibleWhen are hidden unless the referenced field matches the specified value. All changes dispatch undoable commands via ComposerHistory.

Multi-select shows only common fields (enabled toggle).

TriggerPanel

panels/triggerPanel.ts

Rendered inside the property panel for trigger nodes. Shows a human-readable summary of the trigger config and a Test Trigger button (onTestTrigger event).

RunPanel (bottom drawer)

panels/runPanel.ts

Appears when runCurrent() is called. Polls WorkflowAgentService.getRun() every 500ms.

Displays:

  • Per-step status dot (animated amber for running)
  • Tool call summary (last 5, expandable)
  • Output log (last 20 lines)
  • Elapsed time (live timer)
  • Cancel button

Service

IWorkflowComposerService (DI key: workflowComposerService) is the single entry point.

interface IWorkflowComposerService {
  openWorkflow(id: string): Promise<void>;
  createNew(templateId?: string): void;
  save(): Promise<void>;
  runCurrent(): Promise<string>;    // returns run ID
  undo(): void;
  redo(): void;
  autoLayout(): void;
  validate(): IGraphValidationResult;
  mount(container: HTMLElement): void;
}

mount(container) builds the full composer UI into the given DOM node:

  • Left: NodePalette
  • Center: toolbar + WorkflowCanvas + RunPanel
  • Right: PropertyPanel

Registered as InstantiationType.Delayed in composerModule.ts.


Serialization Contract

The composer stores layout in workflow JSON under _composerLayout:

{
  "id": "scaffold-component",
  "trigger": "manual",
  "steps": [...],
  "_composerLayout": {
    "nodes": {
      "__trigger__": { "x": 60, "y": 200, "width": 160, "height": 70 },
      "step-abc": { "x": 400, "y": 180, "width": 180, "height": 80 }
    },
    "viewport": { "x": 0, "y": 0, "zoom": 1 }
  }
}

The orchestrator ignores any key starting with _. This keeps the composer metadata non-breaking for workflows consumed programmatically.


Contributing

Adding a New Node Type

  1. Add to NodeType union in model/composerModel.ts
  2. Register definition in nodes/nodeRegistry.ts — ports, config schema, defaultSize, color, icon
  3. Add shape handling in canvas/canvasRenderer.ts _createNodeElement()
  4. Add serialization mapping in model/composerSerializer.ts _buildSteps() and _createNodeFromStep()
  5. Add port compatibility rules if the new data type is novel (edges/edgeValidator.ts)

Adding a Config Field Type

Add to INodeConfigField.type in nodes/nodeRegistry.ts, then implement the renderer in panels/propertyPanel.ts _createField().

Extending the Palette

The palette auto-populates from NodeRegistry.getAllDefinitions() and AgentStoreService.getAgents(). New node types appear automatically once registered.

Key files

FilePurpose
model/composerModel.tsGraph state, event emitters
model/composerHistory.tsUndo/redo commands
model/composerSerializer.tsWorkflow JSON roundtrip
canvas/workflowCanvas.tsSVG root, pan/zoom/grid
canvas/canvasRenderer.tsSVG diff rendering
canvas/canvasLayout.tsTopological auto-layout
canvas/canvasInteraction.tsAll pointer + keyboard input
nodes/nodeRegistry.tsNode type definitions
edges/edgeValidator.tsConnection validation
panels/propertyPanel.tsConfig editing UI
WorkflowComposerServiceImpl.tsService orchestrator

Was this page helpful?