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 dashboardModel 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.
| Command | What it reverses |
|---|---|
AddNodeCommand | Adds/removes a node |
RemoveNodeCommand | Removes/restores node + its edges |
MoveNodeCommand | Moves/restores node position |
AddEdgeCommand | Adds/removes an edge |
RemoveEdgeCommand | Removes/restores an edge |
UpdateNodeConfigCommand | Config change, stores old values per-key |
UpdateNodeLabelCommand | Label rename |
ToggleNodeEnabledCommand | Enabled 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 →triggerfield, edge dependencies →step.dependsOn[] - Deserialize: builds nodes from steps, auto-lays out if no
_composerLayoutmetadata present - Node positions stored under
_composerLayoutin 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):
- Grid (dot pattern, updates on pan/zoom)
- Edges
- Nodes
- 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:
| Type | Shape |
|---|---|
agent, transform, output, group | Rounded rect (rx=6) |
trigger | Rect with dashed stroke |
conditional | Diamond (rotated square polygon) |
Port colors by data type:
| Data type | Color |
|---|---|
flow | #e0a84e (amber) |
text | #6ba3e8 (blue) |
json | #85c9a8 (green) |
any | #aaa |
CanvasLayout
canvas/canvasLayout.ts — Sugiyama-style auto-layout.
- Assign layers: longest-path from sources (depth by max parent depth)
- Minimize crossings: barycenter heuristic, 4 sweeps (2 forward + 2 backward)
- 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:
idlepanning— middle-click or Space+dragdragging-node— left-click on node body, snaps to 20px griddragging-edge— mousedown on output port, shows live dashed linemarquee— Shift+drag on background, selects enclosed nodes
Keyboard:
| Key | Action |
|---|---|
Delete / Backspace | Delete selected |
Ctrl/Cmd+A | Select all |
Ctrl/Cmd+D | Duplicate selected |
Ctrl/Cmd+Z | Undo |
Ctrl/Cmd+Shift+Z / Ctrl+Y | Redo |
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 typesdefaultConfig: initial config valuesconfigSchema: field definitions for the property paneldefaultSize,color,icon
6 built-in types:
| Type | Ports | Use |
|---|---|---|
trigger | 1 out (flow) | Starts the workflow |
agent | 1 in (flow), 1 out (text) | LLM agent step |
conditional | 1 in, 2 out (true/false) | Branch on output |
transform | 2 in, 1 out | Merge/map outputs |
output | 1 in | Terminal sink |
group | none | Visual container |
Port compatibility:
| Source type | Accepts target |
|---|---|
flow | flow, any |
text | text, any |
json | json, any, text |
any | all types |
Edge Validation
edges/edgeValidator.ts — all connection rules enforced before an edge is added.
Checks in order:
- No self-connection
- Source must be output port, target must be input port
- Port type compatibility
- No duplicate edge
- Max fan-in: agents = 1, transforms = 10, outputs = 5
- 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 firesCanvasInteraction.onDrop - Drag payload:
JSON.stringify({ nodeType, configOverrides, label })
PropertyPanel (right sidebar)
panels/propertyPanel.ts
Renders when a node is selected. Field types supported:
| Schema type | Renders as |
|---|---|
string | <input type="text"> |
number | <input type="number"> |
boolean | <input type="checkbox"> |
select | <select> with options |
textarea | <textarea> (monospace, resizable) |
tools | Scrollable 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
- Add to
NodeTypeunion inmodel/composerModel.ts - Register definition in
nodes/nodeRegistry.ts— ports, config schema, defaultSize, color, icon - Add shape handling in
canvas/canvasRenderer.ts_createNodeElement() - Add serialization mapping in
model/composerSerializer.ts_buildSteps()and_createNodeFromStep() - 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
| File | Purpose |
|---|---|
model/composerModel.ts | Graph state, event emitters |
model/composerHistory.ts | Undo/redo commands |
model/composerSerializer.ts | Workflow JSON roundtrip |
canvas/workflowCanvas.ts | SVG root, pan/zoom/grid |
canvas/canvasRenderer.ts | SVG diff rendering |
canvas/canvasLayout.ts | Topological auto-layout |
canvas/canvasInteraction.ts | All pointer + keyboard input |
nodes/nodeRegistry.ts | Node type definitions |
edges/edgeValidator.ts | Connection validation |
panels/propertyPanel.ts | Config editing UI |
WorkflowComposerServiceImpl.ts | Service orchestrator |