n8n:獲 208k 星標的 Fair-Code 工作流程引擎如何取代 Zapier,以及工作流程如何執行
n8n: The 208k-Star Fair-Code Workflow Engine That Replaces Zapier — And What Actually Happens When Your Workflow Runs
n8n 是可自行部署的 Fair-Code 工作流程引擎,執行時以 IRunExecutionData、節點堆疊和等待映射管理工作狀態與節點排程。v0 以 push 形成廣度優先式排程,v1 以 unshift 形成深度優先式排程;多輸入節點會等待所需輸入齊備。佇列模式透過 Redis(BullMQ)把工作交由 worker 處理,使用同一執行引擎。
n8n: The 208k-Star Fair-Code Workflow Engine That Replaces Zapier — And What Actually Happens When Your Workflow Runs
You've probably heard n8n called "open-source Zapier." That's true enough to be useful and wrong enough to be misleading. Zapier is a hosted product you rent. n8n is a Node.js application you run, point at your own database, and own end to end. No per-task metering. No vendor lock-in. A $5–10 VPS handles a surprising amount.
But the marketing pitch isn't why engineers reach for it. The escape hatch is: the moment the visual builder runs out of road, you drop into a Code node and write real JavaScript (or Python). You never hit a wall you can't code your way through.
What most developers never learn is what actually happens between clicking "Execute Workflow" and seeing those green checkmarks light up on the canvas. The orchestrator that schedules nodes, synchronizes multi-input joins, keeps runs cancelable, and emits lifecycle hooks lives in roughly 1,250 lines of TypeScript. Let's open it up.
The Mental Model: A Stack, A Waiting Map, and One Loop
When you hit execute, n8n doesn't spin up a thread per node. It builds a single serializable run object — IRunExecutionData — that carries everything: the in-flight stack of nodes waiting to run, a waiting map for partially satisfied inputs, metadata, and final results. This same object travels from the editor to the server, into the engine, optionally across a Redis queue to a worker process, and back.
// Simplified shape of the live execution object
interface IRunExecutionData {
startData: { destinationNode?, runNodeFilter? };
resultData: {
runData: Record<string, ITaskData[]>; // per-node run history
pinData: Record<string, INodeExecutionData[]>;
lastNodeExecuted: string;
error: ITaskData | null;
};
executionData: {
nodeExecutionStack: IExecuteData[]; // the work list
waitingExecution: Record<string, Record<number, INodeExecutionData[][]>>;
waitingExecutionSource: Record<string, Record<number, ISourceData[][]>>;
contextData: Record<string, any>;
runtimeData: Record<string, any>;
};
}
The engine (WorkflowExecute in packages/core/src/execution-engine/workflow-execute.ts) seeds nodeExecutionStack with the start node, then enters a loop that shifts the front entry, runs the node, records the result, and enqueues downstream nodes. That's it. No hidden thread pool. No magic graph walker. Just a stack and a waiting map.
Execution Order: v0 (Breadth-First) vs v1 (Depth-First)
The single most important setting in a workflow is executionOrder. It changes one line in the scheduler:
const enqueueFn = workflow.settings.executionOrder === 'v1' ? 'unshift' : 'push';
-
v0 (
push): New work goes to the tail. Older work runs first. The walk feels breadth-first — all siblings at depth 1 finish before depth 2 starts. -
v1 (
unshift): New work goes to the front. The current branch keeps running. The walk feels depth-first — you chase one path to the bottom before backtracking.
This doesn't change the graph. It only changes scheduling. Sibling order in v1 still follows canvas position (top-left first), but only among ready siblings. The canvas is not the execution graph — it only shapes ordering where the scheduler has freedom.
Multi-Input Joins: The Waiting Map
A node with two inputs doesn't run when the first branch arrives. The engine parks partial data in waitingExecution[nodeName][runIndex] with per-input placeholders. As each upstream finishes, the engine fills its slot. Only when all required inputs are non-null does it enqueue the node.
// Inside addNodeToBeExecuted (simplified)
if (requiredInputs.every(input => waitingExecution[nodeName][runIndex][input] != null)) {
// All inputs ready — enqueue the node
enqueueFn(executeData);
} else {
// Park partial data, wait for the rest
waitingExecution[nodeName][runIndex][inputIndex] = inputData;
waitingExecutionSource[nodeName][runIndex][inputIndex] = sourceData;
}
After the main stack empties, a sweep pulls forward any waiting node that now has enough data — even if some optional inputs stayed empty. This means a join can complete after one branch dies early, while a node still missing required inputs stays parked.
Empty Output Kills the Branch (Unless You Say Otherwise)
If a node returns no data and doesn't enter waiting state, the branch ends. The scheduler adds no downstream work. Set alwaysOutputData: true on the node and the engine fabricates a single empty item to keep the branch alive — useful when downstream nodes must run regardless.
Legacy v0 keeps one compatibility path: it can still wake a downstream node connected via a second or later main input even when the upstream output is empty.
Cycles: Node State, Not Special Graph Logic
The scheduler doesn't treat cycles specially. It keeps pulling from the front of the same list. A node loops only if it schedules more work for itself. SplitInBatchesV3 demonstrates the pattern:
// Stores loop state in node context
const loopState = this.getContext('node') || { index: 0, items: inputData };
const batch = loopState.items.slice(loopState.index, loopState.index + batchSize);
loopState.index += batchSize;
this.setContext('node', loopState);
if (loopState.index < loopState.items.length) {
return { loop: batch }; // schedules another visit to this node
}
return { done: loopState.items }; // exits the cycle
The loop guard in processRunExecutionData stops accidental repeats when the same node and run index return without progress.
Queue Mode: Same Engine, Different Process
In standalone mode, the main process executes workflows directly. In queue mode (EXECUTIONS_MODE=queue), the main process enqueues a job into Redis (BullMQ) and returns immediately. A worker process dequeues, reconstructs the same IRunExecutionData object, runs the exact same engine loop, and writes results back to PostgreSQL.
┌─────────────┐ Redis Queue ┌─────────────┐
│ Main │ ──────────────────▶ │ Worker │
│ (UI/API) │ enqueue job │ (executes) │
└─────────────┘ └─────────────┘
│ │
│ PostgreSQL │
└──────────────────▶◀────────────────┘
shared state
The main process serves the UI, REST API, and webhooks. Workers are stateless — scale them horizontally via HPA/KEDA based on queue depth. Dedicated webhook-processor pods handle high-throughput webhooks without blocking the main UI. Python/JavaScript code nodes run in isolated taskRunner sidecars (V8 isolates via isolated-vm for JS, separate processes for Python) with RPC over WebSocket.
The Data Model: Items, Runs, and pairedItem
n8n's data model has three layers:
Level 1 — Item: INodeExecutionData — { json, binary?, pairedItem?, metadata?, evaluationData? }. Nodes receive and return arrays of items on each input/output. This is the unit of data flowing through the graph.
Level 2 — Run: Each node execution appends one ITaskData to resultData.runData[nodeName]. The array index = runIndex. ITaskData records input source, output data, timing, status, metadata, error state. source points back to the previous node, output, and run that supplied the input.
Level 3 — Execution Object: The IRunExecutionData bundles startData, resultData, and executionData (the live scheduling state). The same object carries both result and live machine state — that's how n8n hands an execution to a queue worker, pauses it at a Wait node, and resumes it later with history intact.
pairedItem records lineage, not magic. An output item points back to one input item, or one input plus array position, or a list of links. The engine normalizes these as items move through the workflow. When WorkflowDataProxy resolves $node("HTTP Request").json in a downstream node, it walks this ancestry chain. Broken lookups surface as specific errors: missing upstream lineage, no route back to referenced node, multiple matches, pinned data short-circuiting the chain, invalid index, or intermediate node not yet run.
AI Agents: Native, Not Bolted On
Since the 2.0 line, n8n ships a LangChain-based agent runtime. The Agent node maintains conversation history in ChatMemory, calls language models for planning, executes tools based on model decisions, and accumulates results until goal completion. Tool calling uses the same execution engine — tools are just nodes with execute methods. Vector stores, MCP connectivity, and guardrails plug into the same architecture.
Fair-Code, Not Open Source
n8n uses the Sustainable Use License. You can self-host and modify freely for internal business use, including paid consulting building workflows for clients. What you can't do is rebrand and resell it as your own hosted SaaS. Files with .ee. in the path are Enterprise-licensed. For an indie dev or team running their own automations, you're completely fine — but check the current license page rather than trusting any blog post on the fine print.
What This Means for You
- No per-execution pricing: Run 10 or 10 million workflows on your $5 VPS.
- Full observability: Every execution persists to your database. Open any run, see exactly what each node received and emitted, replay it.
- Escape hatch: Code nodes run real JavaScript/Python with npm/pip access. HTTP Request node talks to anything with an endpoint.
- AI-native: Build agents that call tools, use memory, query vector stores — all self-hosted.
- Production architecture: Queue mode + PostgreSQL + Redis + S3 scales horizontally. The same engine runs in-process or distributed.
The Takeaway
n8n isn't "Zapier you host." It's a workflow engine with a visual authoring layer, a serializable execution model that survives process boundaries, a scheduler you can reason about, and an escape hatch to real code. The 208k stars aren't for the UI — they're for the fact that when the visual builder hits a limit, you don't file a feature request. You write the code.
來源:DEV Community · dev.to