This page covers the frontend workflow editor canvas: how the visual canvas is structured, how nodes are rendered, and how user interactions (add, move, connect, delete, duplicate, run) are processed. It covers NodeView.vue (the top-level page view), Canvas.vue (the Vue Flow host), CanvasNode.vue (per-node rendering), useCanvasOperations (the mutation composable), useCanvasMapping (the data transformation layer), useWorkflowsStore (workflow state), useRunWorkflow, and useWorkflowSaving.
For the Node Detail View (NDV) that opens when you double-click a node, see 6.3. For frontend state management and the Pinia store architecture, see 6.1. For the backend Workflows API and WorkflowService, see 3.1. For the AI Builder that modifies the canvas programmatically, see 5.1.
The canvas is hosted inside NodeView, which acts as the page-level orchestrator. WorkflowCanvas sits between NodeView and Canvas; it invokes useCanvasMapping to produce the typed CanvasNode[] / CanvasConnection[] arrays consumed by Vue Flow.
Canvas Component Tree
Sources: packages/frontend/editor-ui/src/app/views/NodeView.vue18-157 packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue60-70 packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/nodes/CanvasNode.vue1-40 packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/nodes/render-types/CanvasNodeDefault.vue1-22
NodeView (packages/frontend/editor-ui/src/app/views/NodeView.vue) is the Vue component mounted for routes like VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW, and VIEWS.EXECUTION_DEBUG. Its responsibilities:
initializeWorkspace(data) when a workflow is loaded, and resetWorkspace() when navigating away.useCanvasOperations and wires them to canvas events.isCanvasReadOnly and canExecuteOnCanvas.credentialsStore.fetchAllCredentialsForWorkflow.useClipboard({ onPaste: onClipboardPaste }).nodeViewEventBus for importWorkflowData, importWorkflowUrl, and openChat.isCanvasReadOnly is true when any condition holds:
| Condition | Source |
|---|---|
| Demo route | route.name === VIEWS.DEMO |
| Source control branch is read-only | sourceControlStore.preferences.branchReadOnly |
| Collaboration lock held by another user | collaborationStore.shouldBeReadOnly |
| Insufficient workflow/project permissions | !(workflowPermissions.update ?? projectPermissions.workflow.update) |
| Workflow is archived | editableWorkflow.value.isArchived |
| AI Builder is streaming a workflow build | builderStore.streaming && !builderStore.isHelpStreaming |
Sources: packages/frontend/editor-ui/src/app/views/NodeView.vue288-302
Canvas.vue (packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue) wraps VueFlow and bridges n8n's domain model to Vue Flow's graph model.
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | CanvasNode[] | [] | Mapped node data from useCanvasMapping |
connections | CanvasConnection[] | [] | Mapped edge data |
readOnly | boolean | false | Disables all editing interactions |
executing | boolean | false | Shown during workflow run |
loading | boolean | false | Delays rendering until canvas is ready |
hideControls | boolean | false | Hides zoom/fit controls |
keyBindings | boolean | true | Enables keyboard shortcuts |
eventBus | EventBus<CanvasEventBusEvents> | created internally | For external event injection |
Sources: packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue135-162
The canvas toggles between selection and panning modes based on held keys:
| Mode | Activation Keys | Mouse Behaviour |
|---|---|---|
| Selection (default) | None held | Drag = box-select; middle button = pan |
| Panning | Space or Ctrl/Cmd | Left drag = pan |
On mobile, panning is always active. The panningKeyCode ref defaults to [' ', controlKeyCode]; selectionKeyCode is set to null when panning is active to prevent box-select conflicts.
Sources: packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue221-250
| Composable | Role |
|---|---|
useVueFlow(props.id) | Core Vue Flow instance ā fitView, zoom, viewport, selected nodes |
useCanvasLayout | Auto-layout algorithm (tidyUp) |
useCanvasTraversal | Graph traversal: getIncomingNodes, getDownstreamNodes, etc. |
useCanvasNodeHover | Hover state tracking |
useContextMenu | Right-click context menu |
useViewportAutoAdjust | Adjusts viewport when nodes are added/removed |
Sources: packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue174-212
Canvas.vue subscribes to an EventBus<CanvasEventBusEvents> (passed via the eventBus prop and also the global canvasEventBus):
| Event | Effect |
|---|---|
fitView | Calls VueFlow fitView() |
tidyUp | Calls useCanvasLayout().layout() |
nodes:select | Selects nodes by ID; optionally pans into view |
saved:workflow | Triggers NPS survey check |
useCanvasMapping (packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.ts) transforms INodeUi[] + IConnections + a Workflow object into the CanvasNode[] and CanvasConnection[] consumed by Vue Flow.
Data Mapping Flow
Sources: packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.ts71-167
The renderTypeByNodeId computed maps each INodeUi.type to a CanvasNodeRenderType:
CanvasNodeRenderType | Node Type Match | Rendered By |
|---|---|---|
Default | All regular nodes | CanvasNodeDefault.vue |
StickyNote | n8n-nodes-base.stickyNote | CanvasNodeStickyNote.vue |
AddNodes | n8n-nodes-internal.addNodes | CanvasNodeAddNodes.vue |
ChoicePrompt | n8n-nodes-internal.choicePrompt | CanvasNodeChoicePrompt.vue |
Sources: packages/frontend/editor-ui/src/features/workflows/canvas/canvas.types.ts44-49 packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.ts148-167
CanvasNodeDirtiness constants (packages/frontend/editor-ui/src/features/workflows/canvas/canvas.types.ts53-62) mark nodes that need re-execution. They appear as visual indicators (e.g. the node-dirty icon):
| Constant | Meaning |
|---|---|
PARAMETERS_UPDATED | Node's own parameters changed since last run |
INCOMING_CONNECTIONS_UPDATED | Upstream connections changed |
PINNED_DATA_UPDATED | Node's pinned data was modified |
UPSTREAM_DIRTY | At least one upstream node is dirty |
CanvasNodeData (packages/frontend/editor-ui/src/features/workflows/canvas/canvas.types.ts102-150) is the data payload stored in each Vue Flow node. Child components access it via the CanvasNodeKey inject context using useCanvasNode().
Sources: packages/frontend/editor-ui/src/features/workflows/canvas/canvas.types.ts27-150
CanvasNode.vue (packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/nodes/CanvasNode.vue) is the Vue Flow custom node component registered for all node types. It:
NodeProps<CanvasNodeData> from Vue Flow.provide(CanvasNodeKey, ...) so that CanvasNodeToolbar, CanvasNodeRenderer, and all render sub-components can access node data via useCanvasNode().CanvasHandleRenderer (inputs on left, outputs on right), using insertSpacersBetweenEndpoints to distribute handle positions evenly.CanvasNodeRenderer, which switches on CanvasNodeRenderType.CanvasNodeToolbar positioned above the node.The main render component for all non-special nodes (packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/nodes/render-types/CanvasNodeDefault.vue):
NodeIcon.vue), label, and subtitle..selected, .disabled, .success, .error, .pinned, .waiting, .configuring.CanvasNodeStatusIcons for execution state overlays (success checkmark, error badge, spinning indicator, waiting hourglass).CanvasNodeSettingsIcons for configuration and disabled overlays.ExperimentalEmbeddedNodeDetails in-place when isExperimentalNdvActive is true at the current zoom level.useCanvasOperations (packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts) is the single source of truth for all canvas state changes. NodeView destructures approximately 30 functions from it.
useCanvasOperations Function Map
Operations accept an options object; passing { trackHistory: true } records the inverse operation to useHistoryStore for undo/redo support.
Sources: packages/frontend/editor-ui/src/app/views/NodeView.vue215-257
NodeCreation.vue.NodeView.addNodesAndConnections calls useCanvasOperations.addNodesAndConnections(nodes, connections).workflowsStore via addNode(nodeData).createConnection.CanvasNode[] recomputes reactively, triggering Vue Flow to render the new node.Vue Flow fires onNodeDragStop after a drag. Canvas.vue emits update:nodes:position. NodeView.onUpdateNodesPosition calls updateNodesPosition(events, { trackHistory: true }). Positions are stored as XYPosition = [number, number] in INodeUi.position and snapped to GRID_SIZE.
Sources: packages/frontend/editor-ui/src/app/views/NodeView.vue425-435
onConnectStart.onConnect(connection: Connection).Canvas.vue emits create:connection; NodeView.onCreateConnection calls createConnection(connection, { trackHistory: true }).NodeView.onCreateConnectionCancelled opens the Node Creator pre-filtered by NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP.Sources: packages/frontend/editor-ui/src/app/views/NodeView.vue744-785
deleteNode(id, { trackHistory: true }) removes the node and all attached connections.deleteNodes(ids) batches deletions.deleteConnection(connection, { trackHistory: true }) removes one edge.copyNodes(ids) serialises to clipboard JSON.cutNodes(ids) copies then deletes; on isCanvasReadOnly, falls back to copy-only.duplicateNodes(ids, { viewport }) adds offset copies.NodeView.onClipboardPaste: detects a URL (VALID_WORKFLOW_IMPORT_URL_REGEX) and fetches, or parses raw JSON, then calls importWorkflowData(workflowData, 'paste', { viewport }).Sources: packages/frontend/editor-ui/src/app/views/NodeView.vue504-580
NodeCreation.vue is lazy-loaded via defineAsyncComponent. Its visibility is controlled by useNodeCreatorStore. It can be opened from multiple sources:
NodeCreatorOpenSource | Trigger |
|---|---|
plus_endpoint | + handle on a node's output port |
node_connection_drop | Dragging a connection onto empty canvas |
context_menu | Right-click ā "Add Node" |
trigger_placeholder_button | Empty-canvas trigger placeholder |
add_node_button | Toolbar add-node button |
node_shortcut | Keyboard shortcut N |
replace_node_action | Context menu "Replace" action |
When opened via openNodeCreatorForConnectingNode, the source connection is stored in nodeCreatorStore, so the newly added node is automatically connected.
Sources: packages/frontend/editor-ui/src/app/views/NodeView.vue705-777 packages/frontend/editor-ui/src/Interface.ts675-691
useWorkflowsStore is the primary Pinia store for editor state. Key state fields:
| Field | Type | Description |
|---|---|---|
workflow | IWorkflowDb | Currently open workflow (nodes, connections, settings) |
workflowId | string | Current workflow ID |
nodesByName | Record<string, INodeUi> | Nodes indexed by name |
allNodes | INodeUi[] | All nodes as flat array |
workflowExecutionData | execution result | Data from the latest run |
runData | IRunData | Per-node execution data |
pinData | IPinData | Pinned output data keyed by node name |
INodeUi (packages/frontend/editor-ui/src/Interface.ts153-162) extends INode (from n8n-workflow) with canvas-specific fields:
IWorkflowDb (packages/frontend/editor-ui/src/Interface.ts240-269) adds frontend fields like scopes, sharedWithProjects, activeVersionId, checksum, and parentFolder on top of the server-persisted structure.
Sources: packages/frontend/editor-ui/src/Interface.ts153-162 packages/frontend/editor-ui/src/Interface.ts240-269
useRunWorkflow (packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts) provides the functions used by the canvas run buttons:
| Function | Description |
|---|---|
runWorkflow(options) | Starts execution with an IStartRunData payload |
runEntireWorkflow(mode, triggerNodeName) | Runs the full workflow from a specified trigger |
stopCurrentExecution() | Sends a stop request for the active execution |
stopWaitingForWebhook() | Cancels a workflow paused at a webhook wait |
The IStartRunData payload (packages/frontend/editor-ui/src/Interface.ts187-204) controls execution behaviour:
| Field | Purpose |
|---|---|
startNodes | Specific nodes to start from (for partial execution) |
destinationNode | Stop execution at this node |
runData | Pre-existing run data to inject |
dirtyNodeNames | Nodes to treat as dirty for partial execution |
triggerToStartFrom | Trigger node name and optional data override |
chatSessionId | Session ID for chat-triggered workflows |
agentRequest | Agent tool invocation payload |
The canvas hosts three button components that call these functions: CanvasRunWorkflowButton, CanvasStopCurrentExecutionButton, and CanvasStopWaitingForWebhookButton.
Sources: packages/frontend/editor-ui/src/Interface.ts187-204 packages/frontend/editor-ui/src/app/views/NodeView.vue215-216
useWorkflowSaving (packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts) handles saving:
saveCurrentWorkflow() ā serialises the workflow and calls the REST API (POST for new, PATCH for existing).useWorkflowSaveStore with states Idle, Scheduled, InProgress, Failed.NodeView initialises it with an onSaved callback: canvasEventBus.emit('saved:workflow', { isFirstSave }).isAutosaving state in AskAssistantBuild disables the AI Builder chat input while an auto-save is pending, preventing conflicts.Sources: packages/frontend/editor-ui/src/app/views/NodeView.vue173-178 packages/frontend/editor-ui/src/features/ai/assistant/components/Agent/AskAssistantBuild.vue183-196
All canvas mutations that pass { trackHistory: true } are recorded to useHistoryStore as Undoable commands. The historyBus event bus triggers undo/redo from keyboard shortcuts. A BulkCommand groups multiple operations into one undo step.
During AI Builder streaming, all builder-driven canvas changes are wrapped in a single undo group:
builderStore.streaming becomes true ā historyStore.startRecordingUndo()
builderStore.streaming becomes false ā historyStore.stopRecordingUndo()
This ensures every node added or modified in one AI message can be undone atomically.
Sources: packages/frontend/editor-ui/src/features/ai/assistant/components/Agent/AskAssistantBuild.vue405-415 packages/frontend/editor-ui/src/Interface.ts801-807
The AI Builder modifies the canvas programmatically through useWorkflowUpdate (packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.ts).
AI Builder ā Canvas Update Sequence
categorizeNodes (packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.ts63-106) matches incoming nodes to existing ones first by id, then by type::name composite key as a fallback for ID-regeneration cases. This prevents spurious "new node" classifications when the AI SDK regenerates IDs.
Sources: packages/frontend/editor-ui/src/app/composables/useWorkflowUpdate.ts44-106 packages/frontend/editor-ui/src/features/ai/assistant/components/Agent/AskAssistantBuild.vue369-445
WorkflowDiffView.vue (packages/frontend/editor-ui/src/features/workflows/workflowDiff/WorkflowDiffView.vue) renders a side-by-side comparison of two IWorkflowDb snapshots. It is used by:
AIBuilderDiffModal.vue opens it in a modal; ReviewChangesBanner.vue in the chat panel summarises changed nodes with clickable entries that select the node on the canvas.useWorkflowDiff computes nodesDiff (a Map<nodeId, NodeDiffEntry>) and connectionsDiff. useWorkflowDiffUI builds tabs, nodeChanges, and navigation helpers (nextNodeChange, previousNodeChange). Node status values are from the NodeDiffStatus enum in n8n-workflow.
useReviewChanges (packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.ts) bridges the builder store with the diff view: it compares the current workflow state against the last saved version and emits canvasEventBus.emit('nodes:select', ...) when a changed node is clicked.
Sources: packages/frontend/editor-ui/src/features/workflows/workflowDiff/WorkflowDiffView.vue1-102 packages/frontend/editor-ui/src/features/ai/assistant/composables/useReviewChanges.ts1-60 packages/frontend/editor-ui/src/features/ai/assistant/components/Agent/ReviewChangesBanner.vue1-20
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.