n8n Architecture: How It Actually Works
How to read this page
Symvanta parsed this repository into a code graph: every function, class, and method is a node, and every call or import between them is an edge. Everything on this page is computed from that graph at the commit shown above. The terms:
- Module (or cluster)
- A group of symbols that call each other far more than they call anything else. An algorithm called Louvain community detection finds these groups from the call traffic alone; nobody draws them by hand.
- Modularity (the Q number)
- A 0-to-1 score of how cleanly those groups separate. Higher means more call traffic stays inside its own group; scores around 0.7 and above read as clean boundaries.
- Hub
- The most depended-upon symbol inside one module.
- Load-bearing symbols
- PageRank, the algorithm Google originally used to rank web pages, run over the call graph instead: it surfaces the functions the rest of the codebase leans on hardest.
- Arrows and their numbers
- How many calls cross from one module into another. A heavier arrow means tighter coupling between those two parts.
- Dependency cycle
- File A imports B, which imports A again, sometimes through a longer loop. Cycles are not bugs, but a change inside one tends to ripple around the whole loop.
- Mutually recursive symbols
- Functions that call each other, usually the natural shape of parsers and tree-walking code.
n8n is a workflow automation server. A Vue editor draws the workflow, a REST and
webhook server stores it, an execution engine walks it node by node, and a large
library of integration nodes makes the outbound calls. Symvanta's graph of the
repo at af27b0d on master detects
778 functional modules (modularity Q=0.93), and serves the 500 largest, which
hold 75,637 symbols.
The mass of the codebase is integrations. 197 of those 500 modules live in
packages/nodes-base or packages/@n8n/nodes-langchain, roughly one per vendor
transport, and five of the twelve repo-wide load-bearing functions are a vendor's
own HTTP helper: TheHive's, Google's, OpenAI's, Microsoft Excel's and Pipedrive's.
That is what an integration catalog looks like in a call graph, and this one holds
308 vendor directories and 406 credential definition files at this commit.
Those 197 modules are 17.9% of the mapped symbols, behind packages/cli at 24.8%
and the four AI assistant packages at 19.3%, ahead of the editor frontend at
10.8%. The execution engine is genuinely small and deliberately so: the clusters
whose members sit in packages/core/src/execution-engine, packages/workflow and
packages/cli/src/scaling are 6.3% of the map between them. The engine walks the
graph and hands each node an execution context, and the work happens inside the
node.
Module names on this page are read by hand from the packages the members live in and from each hub symbol's file. The generated cluster summaries are used for nothing here, because on this repo several of them are wrong: the cluster that holds the execution engine was summarized as logging, and the cluster that holds the REST controllers was summarized as licensing. Every cluster is cited by its id, the member-set hash that survives a reindex. Symbol counts, hubs, ids and edge weights are what the graph computed.
Module map
The diagram shows the 10 largest of the 778 detected modules, with edges weighted
by how many calls cross between them. Two of the ten are the repo's own
evaluation harnesses, drawn because they really are that big: the @n8n/instance-ai
harness (2,650 symbols) and the AI workflow builder harness (2,082). Two more take
their hub symbol from an ambient TypeScript declaration file, which inflates both
their size and the weight of the arrows into them: the global interface
augmentation in
packages/frontend/editor-ui/src/shims-global.d.ts is referenced from 82 files,
and the *.vue module shim in packages/@n8n/mcp-apps/src/apps/workflow-preview/shims-vue.d.ts
from 96, in packages as unrelated as the vendored ORM and the push service. Read
those two boxes as where the editor's own code sits.
Where to start reading
These are the load-bearing functions: the PageRank ranking over the call graph,
most depended-upon first. Four of the names are defined independently in several
packages (apiRequest alone in six vendor transports), so each link points at the
definition that hubs the largest cluster carrying that name. Five of the twelve
are one vendor's HTTP transport, which is the shape of this repo showing up in
the ranking.
isRecordLogger.scopedmakeRestApiRequestTelemetry.trackMessageEventBus.sendAuditEventtheHiveApiRequestLicenseState.isLicensedJsonColumngetGoogleAccessTokenapiRequestmicrosoftApiRequestpipedriveApiRequest
Key subsystems
Instance AI assistant runtime
Module id 1122f926faac11ce, 3,706 symbols, the largest on the map. Its members live in packages/@n8n/instance-ai/src and its hub is InstanceAiContext from src/types.ts, the context object every assistant tool receives. InstanceAiToolRegistry, OrchestrationContext, WorkflowBuildOutcome and ModelConfig sit beside it. Its heaviest outgoing edge is 148 calls into the editor's agents feature, which is the other half of the same product surface.
Node errors and credential helpers
Module id 532185b8a99ea6b4, 3,337 symbols. The hub is NodeApiError from packages/workflow/src/errors/node-api.error.ts, and the members are NodeOperationError plus a wide slice of the credential and transport helpers under packages/nodes-base (beeminderApiRequest, getActiveCredentialType, getHost, DatabricksCredentials). Integration nodes raise these two error types when a request fails, so the error classes cluster with the transports that throw them instead of with the rest of the workflow package.
Agents SDK types and JSON values
Module id 4e0cee763e689c87, 3,183 symbols, hubbed on JSONValue from packages/@n8n/agents/src/types/utils/json.ts. This is the type surface of the agents SDK: JSONObject, JSONArray, AgentMessage, AgentDbMessage, BuiltTool. A one-line recursive type alias ranks as the hub here because every message and tool definition in the SDK is built out of it.
Editor UI composables and log view
Module id f7c4c628d73c66c6, 2,887 symbols, members under packages/frontend/editor-ui/src/app/composables: useWorkflowDocumentStore, useTelemetry, and the LogEntry and NodeLogEntry shapes the execution log view renders. This is one of the two clusters whose hub is an ambient declaration, so the name comes from where the members live.
REST controllers and response errors
Module id ad43c0ce9514e986, 2,871 symbols, hubbed on ResponseError from packages/cli/src/errors/response-errors/abstract/response.error.ts with BadRequestError and ForbiddenError under it. Around them sit the chat hub module (ChatHubSession, ChatHubTool) and the license accessors (License.manager, License.isLicensed). Controllers throw response errors and check the license on the same request path, so the graph puts them together. Its heaviest edges are 116 calls into the server agents module and 102 into the database entities.
Server agents and the editor agents feature
The server side is module id 1fbd1c236a830671, 2,185 symbols under packages/cli/src/modules/agents, hubbed on the Agent entity in entities/agent.entity.ts, with AgentHistory, AgentExecution, AgentExecutionThread and AgentRepository. The editor side is module id 43d35099e877cd82, 2,541 symbols under packages/frontend/editor-ui/src/features/agents/composables. The server module takes 116 calls from the REST controller cluster and sends 46 back.
Execution engine and queue runner
The engine is two modules. Id 83094e44dd365222 (1,255 symbols) has its members in packages/core/src/execution-engine while its hub is Logger.scoped from packages/@n8n/backend-common, which is why its generated summary described logging. Id 8d9cdb44e96f26ba (540 symbols) is the partial-execution machinery: DirectedGraph, GraphConnection, NodeExecutionContext, Workflow.getNode. The queue runner is another two: id c4a90fa06ac89e11 (616 symbols) under packages/cli/src/scaling, holding ExecutionRef, ActiveExecutions and the execution lifecycle hook handlers, and id e4d215ec66e79a25 (399 symbols) for the pub/sub commands workers exchange. Together with packages/workflow, that is 6.3% of the mapped symbols.
Integration transports
The 197 integration modules are the long tail of the map, each one a vendor's transport file plus the actions that call through it, 13,575 symbols in total. Past the error cluster above, the largest are id a1594a6836dd063d (787 symbols) hubbed on googleApiRequest from the Gmail helpers, id be2b32f6e5a912ef (660) on pipedriveApiRequest, id f6c860f8c23c0180 (630) on getGoogleAccessToken, and id 4419e5cfc5c6b81d (440) on the shared verifySignature webhook helper. The pattern repeats verbatim down the tail: one *ApiRequest function per vendor, a credentials type, and a directory of actions that call it. Symvanta's clustering gives each of those its own module, which is why 197 of the 500 served modules are integrations while they are only 17.9% of the symbols.
Workflow error hierarchy
Module id 91ba1b8bb399e612, 1,891 symbols in packages/workflow/src: BaseError, NodeError, ExecutionBaseError, the JsonObject and JsonValue types, and helpers like removeCircularRefs. This is the package every other package imports its error classes from.
Evaluation harnesses
Two of the ten drawn clusters are test harnesses for the AI features: id ca1dbdc7a8b683df (2,650 symbols, packages/@n8n/instance-ai/evaluations, hubbed on the harness N8nClient) and id 20557a094fd53329 (2,082 symbols, packages/@n8n/ai-workflow-builder.ee/evaluations). With the rest of packages/testing they are 9.8% of the mapped symbols. They rank this high because these harnesses drive a real n8n instance, so they carry client, workflow and validation code of their own.
Canonical request flow
The sequence worth reading first is one workflow execution: the path from the
call that starts a run to the node's own execute method and back. Every step
below is a call edge out of the step above it, in the order the work happens.
WorkflowRunner.run(packages/cli/src/workflow-runner.ts:242) is the entry point 13 files in the server call: retries, waiting executions resumed by the wait tracker, chat runs, the MCP execute and test tools, and the evaluation runner.CredentialsPermissionChecker.check(packages/cli/src/executions/pre-execution-checks/credentials-permission-checker.ts:102) maps every credential id the workflow's nodes use to the projects the workflow belongs to, and refuses the run when one of them is not shared with any of those projects.ActiveExecutions.add(packages/cli/src/active-executions.ts:62) reserves a concurrency slot, creates the execution row at statusnew, and builds the in-memory record everything later attaches to: the cancellation controller, the response promise, the post-execute promise.WorkflowRunner.enqueueExecution(packages/cli/src/workflow-runner.ts:515) is the queue-mode branch: it hands the job toScalingService.addJoband waits for a worker to report the result. The worker picks it up inJobProcessor.processJoband runs step 9 onwards in its own process.WorkflowRunner.runMainProcess(packages/cli/src/workflow-runner.ts:343) is the other branch, running the workflow in the server process. It registers the lifecycle hooks, arms the execution timeout, and owns the failure paths.ExecutionRepository.setRunning(packages/@n8n/db/src/repositories/execution.repository.ts:380) flips the row torunninginside a transaction and keeps the originalstartedAtwhen this is a resumed execution.ManualExecutionService.runManually(packages/cli/src/manual-execution.service.ts:49) decides what a manual run actually executes: the whole workflow from its trigger, or a partial re-run seeded with the pinned data and start nodes the editor sent.DirectedGraph.fromWorkflow(packages/core/src/execution-engine/partial-execution-utils/directed-graph.ts:466) turns the stored workflow into the graph object the partial-execution logic walks to find which nodes a re-run has to touch.WorkflowExecute.processRunExecutionData(packages/core/src/execution-engine/workflow-execute.ts:1586) is the loop, roughly 970 lines of it. It shifts the next node off the execution stack, checks that its input data is ready, runs it, pushes the nodes its output feeds, and repeats until the stack is empty. Per-node retries and the wait state live inside the same loop.WorkflowExecute.runNode(packages/core/src/execution-engine/workflow-execute.ts:1311) handles one node: disabled nodes, execute-once, trigger and poll nodes, and the error-output branch that lets a workflow continue past a failure.WorkflowExecute.executeNode(packages/core/src/execution-engine/workflow-execute.ts:1050) builds anExecuteContextand callsINodeType.execute. This one edge is the boundary between the engine and the integration library: above it is n8n's code, below it is the node's, and the 197 transport modules hang off the far side.ExecutionLifecycleHooks.runHook(packages/core/src/execution-engine/execution-lifecycle-hooks.ts:118) runs the handlers registered for one of eight events (workflowExecuteBefore,nodeExecuteBefore,nodeExecuteAfter,nodeFetchedData,sendResponse,sendChunk,workflowExecuteResume,workflowExecuteAfter). Saving progress, pushing status to the editor and recording statistics all hang here.ActiveExecutions.finalizeExecution(packages/cli/src/active-executions.ts:246) closes a streaming response if the run had one, resolves the post-execute promise, and drops the in-memory record.
Health signals
Symvanta detected 255 dependency cycles across 778 modules (modularity Q=0.93).
The largest cycle spans 269 files in the services area.
85 sets of mutually recursive symbols were also detected, the largest being
workflow (53 symbols).
Read the cycle count with its composition in hand: 194 of the 255 sit entirely
inside packages/@n8n/typeorm, the TypeORM fork the repo vendors, which the
clustering already excludes (it holds zero of the 778 modules) while the cycle
listing still covers it. The 61 that remain are n8n's own. The largest is the one
worth acting on: every file the map names for it sits under packages/cli/src,
including services/role.service.ts, active-executions.ts, the MCP tool files
and the agents services, all reaching each other through imports. The second
largest spans 91 files in packages/workflow. The recursion is milder and mostly
deliberate: the 53-symbol workflow group is the node interface surface in
packages/workflow/src/interfaces.ts (IExecuteFunctions,
ILoadOptionsFunctions, INodeParameters and their neighbours), which is what a
self-referential parameter type looks like in a graph.
Auto-generated by Symvanta from the public repo n8n-io/n8n at commit af27b0d , licensed Sustainable Use License .
Machine-readable companion: data.json (module counts, subsystems, load-bearing symbols, health signals).