n8n-core-architecture — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited n8n-core-architecture (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| Layer | Technology | Role |
|---|---|---|
| Execution Engine | Node.js + TypeScript | Processes workflows as item sequences through nodes |
| Item Pipeline | INodeExecutionData | Carries JSON data, binary files, paired items, and errors |
| Node System | INodeType interface | Defines trigger, regular, webhook, polling, and declarative nodes |
| Expression Engine | Data Proxy | Provides $json, $input, $binary, $execution, $workflow, $node |
| Process Model | Main / Worker / Queue | Scales from single-process to distributed queue mode |
| Workflow Settings | IWorkflowSettings | Controls timezone, error handling, execution timeout, execution order |
| Mode | Trigger |
|---|---|
manual | User clicks "Execute Workflow" in editor |
webhook | HTTP webhook received |
trigger | Trigger node activated (schedule, poll) |
retry | Retrying a failed execution |
cli | Command-line execution |
error | Error workflow triggered |
integrated | Sub-workflow called from another workflow |
internal | Internal system execution |
evaluation | Evaluation/test execution |
chat | Chat-based trigger |
| Type | Method | Inputs | Group |
|---|---|---|---|
| Regular | execute() | Has inputs | ['transform'], ['input'], ['output'] |
| Event Trigger | trigger() | No inputs | ['trigger'] |
| Webhook Trigger | webhook() | No inputs | ['trigger'] |
| Polling Trigger | poll() | No inputs | ['trigger'] (with polling: true) |
| Declarative | None (routing config) | Has inputs | Any |
NEVER return a flat INodeExecutionData[] from execute() -- ALWAYS wrap in an outer array: return [returnData]. The outer array represents output indices (for multi-output nodes like IF/Switch).
NEVER mutate input items directly -- ALWAYS create new item objects. Mutating input items causes data corruption in downstream nodes that share the same reference.
NEVER ignore pairedItem in custom nodes that transform items -- ALWAYS set pairedItem: { item: i } to maintain item linking. Without paired items, n8n cannot trace data lineage.
NEVER assume item.binary exists -- ALWAYS check for undefined before accessing binary data. Not all items carry binary data.
NEVER use this.getNodeParameter('param', 0) with a hardcoded index when processing multiple items -- ALWAYS use the loop index i to get per-item parameter values.
NEVER skip continueOnFail() checks in custom nodes -- ALWAYS wrap item processing in try/catch and check this.continueOnFail() to respect user error settings.
n8n processes data as items. Each node receives an array of items, processes them, and outputs an array of items:
Input: INodeExecutionData[] → Node → Output: INodeExecutionData[][]
[item0, item1, item2] [[result0, result1]] (single output)
[[trueItems], [falseItems]] (two outputs)interface INodeExecutionData {
json: IDataObject; // REQUIRED - main data payload
binary?: IBinaryKeyData; // Optional file/binary data
error?: NodeApiError | NodeOperationError; // Error info if item errored
pairedItem?: IPairedItemData | IPairedItemData[] | number; // Item linking
metadata?: {
subExecution: RelatedExecution; // Sub-workflow execution reference
};
}json is the ONLY required field -- it contains the item's data as key-value pairsbinary holds file data keyed by property name (e.g., "data", "attachment")pairedItem links output items back to their source input itemserror carries error details when an item fails processingThe return type INodeExecutionData[][] is a 2D array:
// Single output node (most common):
return [returnData]; // returnData is INodeExecutionData[]
// Two-output node (e.g., IF node):
return [trueItems, falseItems];
// Three-output node (e.g., Switch):
return [output0Items, output1Items, output2Items];interface IBinaryData {
data: string; // Base64 encoded binary data (or reference ID)
mimeType: string; // MIME type (e.g., "image/png", "application/pdf")
fileType?: BinaryFileType; // 'text' | 'json' | 'image' | 'audio' | 'video' | 'pdf' | 'html'
fileName?: string;
fileExtension?: string;
fileSize?: string;
id?: string; // Reference ID for filesystem-stored binary
}Binary data on an item is stored as a keyed dictionary:
interface IBinaryKeyData {
[key: string]: IBinaryData; // e.g., { "data": {...}, "attachment": {...} }
}ALWAYS access binary data through the key name: item.binary?.data or item.binary?.attachment.
| Method | Purpose |
|---|---|
this.helpers.assertBinaryData(itemIndex, propertyName) | Get binary data or throw |
this.helpers.getBinaryDataBuffer(itemIndex, propertyName) | Get binary as Buffer |
this.helpers.detectBinaryEncoding(buffer) | Detect text encoding |
this.helpers.prepareBinaryData(buffer, fileName?, mimeType?) | Create IBinaryData from Buffer |
interface IPairedItemData {
item: number; // Index of the source item in the input
input?: number; // Input index (default 0)
sourceOverwrite?: ISourceData; // Override source node
}ALWAYS set pairedItem when creating output items to maintain data lineage:
for (let i = 0; i < items.length; i++) {
returnData.push({
json: { /* processed data */ },
pairedItem: { item: i }, // Links to input item at index i
});
}For items that map to multiple source items, use an array:
returnData.push({
json: { /* aggregated data */ },
pairedItem: [{ item: 0 }, { item: 1 }, { item: 2 }],
});| Environment Variable | Purpose |
|---|---|
EXECUTIONS_MODE=queue | Enable queue mode |
QUEUE_BULL_REDIS_HOST | Redis host for job queue |
QUEUE_BULL_REDIS_PORT | Redis port for job queue |
QUEUE_WORKER_CONCURRENCY | Concurrent executions per worker |
| Setting | Type | Purpose | |
|---|---|---|---|
timezone | string | Workflow timezone (e.g., "Europe/Amsterdam") | |
errorWorkflow | string | Workflow ID to execute on error | |
executionTimeout | number | Timeout in seconds | |
executionOrder | `'v0' \ | 'v1'` | Node execution order algorithm |
saveDataErrorExecution | string | Save policy for failed executions | |
saveDataSuccessExecution | string | Save policy for successful executions | |
saveManualExecutions | boolean | Whether to save manual test executions | |
saveExecutionProgress | boolean | Save progress during execution | |
callerPolicy | CallerPolicy | Sub-workflow access control | |
callerIds | string | Allowed caller workflow IDs |
ALWAYS set executionOrder: 'v1' for new workflows -- v0 is legacy and produces non-deterministic execution order.
During execution, expressions have access to these variables:
| Variable | Type | Purpose | |
|---|---|---|---|
$json | IDataObject | Current item's JSON data | |
$binary | IBinaryKeyData | Current item's binary data | |
$input | ProxyInput | Current node's input accessor | |
$input.all() | INodeExecutionData[] | All input items | |
$input.first() | INodeExecutionData | First input item | |
$input.last() | INodeExecutionData | Last input item | |
$input.item | INodeExecutionData | Current input item | |
$node["Name"] | -- | Access another node's output | |
$workflow | -- | Workflow metadata | |
$execution.id | string | Current execution ID | |
$execution.mode | `'test' \ | 'production'` | Execution mode |
$execution.resumeUrl | string | URL to resume waiting executions | |
$now | DateTime | Current DateTime (Luxon) | |
$today | DateTime | Today's date (Luxon) | |
$parameter | INodeParameters | Current node's parameters | |
$position | number | Current item index | |
$env | -- | Environment variables |
Store and retrieve custom metadata during execution:
$execution.customData.set("orderId", "12345");
$execution.customData.get("orderId"); // "12345"
$execution.customData.setAll({ key1: "val1", key2: "val2" });
$execution.customData.getAll(); // { key1: "val1", key2: "val2" }Need to START a workflow?
├── YES → Is it time-based?
│ ├── YES → Use trigger() with ITriggerFunctions (schedule trigger)
│ └── NO → Does it receive HTTP requests?
│ ├── YES → Use webhook() with IWebhookFunctions
│ └── NO → Does it poll an external service?
│ ├── YES → Use poll() with IPollFunctions (set polling: true)
│ └── NO → Use trigger() with custom event listener
└── NO → Is it a simple REST API wrapper?
├── YES → Use declarative node (routing config, no execute())
└── NO → Use programmatic node with execute()How many executions per day?
├── < 1000 → Single process (default)
├── 1000-10000 → Queue mode with 1-2 workers
└── > 10000 → Queue mode with multiple workers + Redis cluster~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.