Search
2 results for “sterndata”
-
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 (
WorkflowExecuteinpackages/core/src/execution-engine/workflow-execute.ts) seedsnodeExecutionStackwith 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 isexecutionOrder. 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 inwaitingExecution[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. SetalwaysOutputData: trueon 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.SplitInBatchesV3demonstrates 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 cycleThe loop guard in
processRunExecutionDatastops 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 sameIRunExecutionDataobject, runs the exact same engine loop, and writes results back to PostgreSQL.┌─────────────┐ Redis Queue ┌─────────────┐
│ Main │ ──────────────────▶ │ Worker │
│ (UI/API) │ enqueue job │ (executes) │
└─────────────┘ └─────────────┘
│ │
│ PostgreSQL │
└──────────────────▶◀────────────────┘
shared stateThe main process serves the UI, REST API, and webhooks. Workers are stateless — scale them horizontally via HPA/KEDA based on queue depth. Dedicated
webhook-processorpods handle high-throughput webhooks without blocking the main UI. Python/JavaScript code nodes run in isolatedtaskRunnersidecars (V8 isolates viaisolated-vmfor 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
ITaskDatatoresultData.runData[nodeName]. The array index =runIndex.ITaskDatarecords input source, output data, timing, status, metadata, error state.sourcepoints back to the previous node, output, and run that supplied the input.Level 3 — Execution Object: The
IRunExecutionDatabundlesstartData,resultData, andexecutionData(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 aWaitnode, and resumes it later with history intact.pairedItemrecords 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. WhenWorkflowDataProxyresolves$node("HTTP Request").jsonin 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. TheAgentnode maintains conversation history inChatMemory, 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 withexecutemethods. 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.#opensource #automation #workflow #selfhosted #software #coding #development #engineering #inclusive #community
n8n: The 208k-Star Fair-Code Workflow Engine That Replaces Zapier — And What Actually Happens When Your Workflow Runs - v0 (