N8n Code Python
Romuald CzlonkowskiCommunityCreated by Romuald Czlonkowski
- Skill
- Tool integration
- Automation and integrations
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides essential guidelines for using the native Python runner in n8n 2.x Code nodes. Unlike the legacy Pyodide implementation, native Python runs in a restricted task runner environment without access to n8n-specific helpers like _input, _json, or _node. Users must adhere to strict sandbox limitations, including blocked imports by default, restricted built-in functions, and specific return shape requirements. This documentation helps developers migrate legacy code, troubleshoot common runtime errors, and optimize performance for Python-based data transformations within n8n workflows.
What it does
- Explains the use of _items and _item variables for data access in different execution modes.
- Details the security sandbox, including blocked imports and restricted built-in functions.
- Provides migration patterns for transitioning from legacy Pyodide code to native Python.
- Defines valid return shapes for both 'Run Once for All Items' and 'Run Once for Each Item' modes.
- Outlines performance considerations and error handling behaviors for node failures.
- Offers a checklist for verifying Python code before deployment.
When to use it
- When a workflow explicitly requires Python logic that cannot be achieved via standard n8n nodes or JavaScript.
- When migrating legacy n8n workflows that previously relied on Pyodide-based Python nodes.
- When troubleshooting NameError or Security violation errors in existing Python Code nodes.
- When you need to perform complex data manipulations that benefit from Python's native syntax.
Advantages
- Enables the use of native Python syntax, including comprehensions, generators, and lambdas.
- Provides clear guidance on avoiding silent data traps during error handling.
- Clarifies the distinction between native Python and JavaScript-based n8n helpers.
- Helps optimize workflow performance by recommending efficient execution modes.
Usage examples
- “How do I filter items in a native Python Code node?”
- Use 'Run Once for All Items' mode and return a list comprehension: return [it for it in _items if it['json'].get('active')].
- “Why am I getting a NameError when using _input in my Python node?”
- The _input helper is not available in native Python. Replace it with _items for 'Run Once for All Items' mode or _item for 'Run Once for Each Item' mode.
- “Can I import the json module in my Python Code node?”
- Imports are blocked by default. You must check if your n8n instance administrator has allowlisted the module in the task-runner configuration.
- “How do I return data from a Python Code node?”
- In 'Run Once for All Items' mode, return a list of dictionaries, e.g., [{'json': {'key': 'value'}}]. In 'Run Once for Each Item' mode, return a single dictionary.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/czlonkowski/n8n-code-python/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip n8n-code-python.zip -d .agents/skills
Documentation by Romuald Czlonkowski
Python Code Node (native)
Since n8n 2.0 the Code node's Python runs as native Python in a task runner (language: "pythonNative"). The old Pyodide "Python (Beta)" is gone, and with it every n8n helper it had:
_input, _json, _node, _now, _today and _jmespath all raise NameError now. Code copied
from old templates, forum posts or older docs usually breaks on this.
JavaScript first — stronger than before
Native Python gives you two variables and plain Python, and by default no imports: no
json, datetime or re. Everything n8n-specific ($('Node'), $jmespath, Luxon,
this.helpers.httpRequest, static data) exists only in JavaScript. Use Python only when the user
explicitly asks for it. Even then, first check whether an expression, Edit Fields, or a native
node (Crypto, Date & Time, HTML, XML) does the job. See n8n-code-javascript and the transform
gatekeeper in n8n-expression-syntax.
Quick start
# Run Once for All Items (default mode)
return [
{"json": {"name": it["json"]["name"], "revenue": it["json"]["revenue"]}}
for it in _items
if it["json"].get("active")
]# Run Once for Each Item
row = _item["json"]
return {"json": {**row, "name_upper": (row.get("name") or "").upper()}}Node config: {"language": "pythonNative", "mode": "runOnceForAllItems" | "runOnceForEachItem", "pythonCode": "..."} on n8n-nodes-base.code typeVersion 2.
The only inputs: _items and _item
| Mode | Variable | Shape |
|---|---|---|
| Run Once for All Items | _items | list of plain dicts {"json": {...}, "pairedItem": {...}} |
| Run Once for Each Item | _item | one plain dict {"json": {...}, "pairedItem": {...}} |
Each variable exists only in its own mode.
_itemsin each-item mode (or_itemin all-items mode) raisesNameError.Dict access only.
it["json"]["name"]orit["json"].get("name").it.json.nameraisesAttributeError: 'dict' object has no attribute 'json'.No other nodes. There is no
_node/$('Node')equivalent. If you need data from another branch, bring it in with Merge first, or read it in JavaScript.Webhook payloads are under
["body"]:_items[0]["json"].get("body", {}).get("email").Binary data isn't covered here. Read and write binary in JavaScript (see n8n-binary-and-data).
pairedItem: returning
_items/_itemkeeps it. When you build new dicts and downstream uses$('Node').item, add"pairedItem": {"item": i}to each returned item. That isn't verified on native Python yet, so test-run before relying on it.Missing keys: prefer
.get(key, default).row["missing"]raisesKeyError.
Migration table for legacy code:
| Legacy (Pyodide) | Native |
|---|---|
_input.all() | _items |
_input.first()["json"] | _items[0]["json"] (guard if _items) |
_input.item / _json | _item / _item["json"] |
_node["X"] | not available: Merge upstream, or use JS |
_now, _today | not available: pass {{ $now.toISO() }} in via Edit Fields, or use JS |
_jmespath(data, q) | not available: $jmespath in an expression, or a comprehension |
item.json.field | item["json"]["field"] |
Imports: blocked by default
Every import (standard library and third-party) is checked against an allowlist before the
code runs. The default allowlist is empty, so even import json rejects the whole node:
Security violations detected
Line 1: Import of standard library module 'json' is disallowed. Allowed stdlib modules: nonen8n Cloud: no imports at all.
Self-hosted: the admin can allowlist modules in the task-runner config (see n8n-self-hosting →
TASK_RUNNERS.md). Some instances therefore allowjson,datetimeandre, and most don't.Default to import-free code. If an import would really help, confirm it first: a one-line test node
import json+return [{"json": {"ok": True}}], or ask the user. Never assume.Without imports: parse JSON strings upstream (
{{ JSON.parse($json.payload) }}in Edit Fields). Do date math with the Date & Time node or Luxon in an expression. ISO-8601 strings still compare and sort correctly as plain strings. Use the Crypto node for hashing.requests,pandasandnumpyare never available unless the admin built a custom runner image. Use the HTTP Request node for HTTP.
Sandbox limits (these fail even without imports)
| You write | What happens | Use instead |
|---|---|---|
eval, exec, compile, open, input, type, getattr, setattr, hasattr, vars, dir, globals, locals, object, memoryview, breakpoint | NameError: name 'type' is not defined (runtime) | isinstance(x, dict); key in d / d.get(key) |
class Foo: ... | __build_class__ not found (runtime) | dicts + functions |
x.__class__, "{0.__class__}".format(x), __import__("json") | Security violations detected (whole node rejected before running) | — |
global counter inside a function | NameError: name 'counter' is not defined, because your code runs inside a wrapper function | nonlocal counter |
Everything else in plain Python works (verified): comprehensions, generators, lambdas, closures,
recursion, try/except, f-strings / .format() / %, sorted/min/max/sum/any/all/
enumerate/zip/round, sets, isinstance, print() (output goes to the browser console).
Return shapes (verified)
Observed on n8n 2.38.5. The auto-wrapping and passthrough behaviours below are undocumented and could change in a later release. Re-check with a test run after upgrading n8n.
Run Once for All Items
| Return | Result |
|---|---|
[{"json": {...}}, ...] | canonical, N items |
[{...}, ...] (plain dicts) | auto-wrapped under json, N items |
{"json": {...}} or a single plain dict | 1 item |
_items (mutated in place) | passthrough with your changes |
None / no return | error Cannot read properties of null (reading 'json') |
Run Once for Each Item
| Return | Result |
|---|---|
{"json": {...}}, a plain dict, or _item | 1 item |
None | the item is dropped (a built-in filter) |
| a list | error A 'json' property isn't a dictionary [item 0] |
Value conversion on output: tuple becomes a list, set becomes the string "{1, 2}",
and a datetime becomes str(dt) ("2026-09-16 10:18:00.025792", not ISO). Convert explicitly
(sorted(s), dt.isoformat()).
Prefer the explicit [{"json": ...}] in all-items mode and {"json": ...} in each-item mode.
The auto-wrapping works, but the explicit shape makes the intent obvious to the next reader.
Errors and onError
A plain raise ValueError("bad row") fails the node with that message. When the node has an
onError continue mode, the three failure kinds behave differently (verified):
| Failure | continueErrorOutput | continueRegularOutput |
|---|---|---|
Runtime exception (raise, KeyError, NameError, denied builtin) | {"error": "<message>"} on the error output (main[1]) ✅ | {"error": "<message>"} on the main output |
Static rejection (Security violations detected: import, dunder) | node marked failed, but the input items, unchanged, go out the success output; main[1] stays empty | input items, unchanged, on the main output |
Bad return shape (list in each-item mode, None in all-items mode) | same: unchanged input on the success output | same |
The last two are silent-data traps: downstream nodes receive unprocessed input as if the code had run, and the execution still shows success. No error branch catches them. Prevent them (no imports unless confirmed, correct return shape) and confirm with a real test run, checking the Code node's status and output (see n8n-error-handling).
Other messages:
Python runner unavailable: Python 3 is missing from this system: the self-hosted instance has no Python task runner (the stock image ships none). It's an infrastructure problem, not a code problem. See n8n-self-hosting →TASK_RUNNERS.md.validate_node/validate_workflowcatch a few Python mistakes (import requests, missingreturn,return None). They do not catch_input/_json, dot access, blocked stdlib imports, dunder access or classes. A test execution is the only reliable check.
Performance
Each Python Code node costs roughly 0.4 s (about 1 s when the runner is cold), noticeably more than a JS Code node or an expression. Process lists in Run Once for All Items mode rather than per item, and don't chain several small Python nodes where one would do.
Checklist
[ ] The user actually asked for Python. Otherwise JS, an expression, or a native node.
[ ] Mode matches the variable:
_items(all items) /_item(each item).[ ] Only dict access. No
_input,_json,_node,_nowor_jmespath.[ ] No
importunless confirmed allowlisted on this instance.[ ] No classes,
type(),getattr/hasattror dunders.nonlocalinstead ofglobal.[ ] Return shape fits the mode. Sets and datetimes converted explicitly.
[ ] Ran a real test execution and inspected the output items (validation alone won't catch the traps above).
Reference
COMMON_PATTERNS.md: 12 import-free patterns, each verified on a live n8n instance (filter, aggregate, group, dedupe, top N, flatten, validate, drop items, text report, safe nested access, running totals, ISO timestamps).
Related skills
n8n-code-javascript: the default for Code nodes, with all n8n helpers.
n8n-expression-syntax:
$jmespath, Luxon and the transform gatekeeper, often a better fit than any Code node.n8n-code-tool: Python in the AI-agent Custom Code Tool (
_query, returns a string).n8n-error-handling: wiring error outputs; the passthrough trap above.
n8n-self-hosting: enabling the Python task runner and allowlisting modules.
Files
- ATTRIBUTION.mdAdded by AutoGineer887 B
- COMMON_PATTERNS.md8.2 KiB
- LICENSELicence file added by AutoGineer1.1 KiB
- README.md3.3 KiB
- SKILL.md10.8 KiB
COMMON_PATTERNS.md
Common Patterns — native Python Code node
Twelve import-free patterns. Each block below was run verbatim in a Code node (language: "pythonNative") on n8n 2.38.5 against the sample input shown, and the noted output was observed. They use only _items / _item, dict access, and builtins that the sandbox allows — no imports, classes, type(), or dunders (see SKILL.md → Sandbox limits).
Before using any of these, re-check the first rule of the skill: is Python actually what the user asked for? Most of these are one expression, an Edit Fields field, or a native node (Filter, Aggregate, Split Out, Remove Duplicates, Sort, Limit) in a JavaScript-first workflow.
Sample input (3 items, e.g. after Split Out):
[
{"name": "Acme", "country": "PL", "revenue": 120000, "active": true, "contact": {"email": "[email protected]", "first-name": "Jan"}, "orders": [{"id": "A1", "total": 500}, {"id": "A2", "total": 1500}]},
{"name": "Foo", "country": "DE", "revenue": 30000, "active": false, "contact": {"email": "[email protected]", "first-name": "Hans"}, "orders": [{"id": "F1", "total": 90}]},
{"name": "Bar", "country": "PL", "revenue": 80000, "active": true, "contact": {"email": null, "first-name": "Ola"}, "orders": []}
]| # | Pattern | Mode |
|---|---|---|
| 1 | Filter and reshape | All Items |
| 2 | Totals and averages | All Items |
| 3 | Group by a field | All Items |
| 4 | Deduplicate by key | All Items |
| 5 | Top N by a field | All Items |
| 6 | One item per nested element | All Items |
| 7 | Validate and flag | Each Item |
| 8 | Keep only some items (each-item) | Each Item |
| 9 | Text report | All Items |
| 10 | Safe nested access | All Items |
| 11 | Running state with nonlocal | All Items |
| 12 | ISO timestamps without datetime | All Items |
1. Filter and reshape
Mode: Run Once for All Items. Keep active customers above a threshold and emit only the fields downstream needs.
return [
{"json": {"name": it["json"]["name"], "revenue": it["json"]["revenue"]}}
for it in _items
if it["json"].get("active") and it["json"].get("revenue", 0) >= 50000
]Output: 2 items: {name, revenue} for Acme and Bar.
2. Totals and averages
Mode: Run Once for All Items. One summary item from all input items. max(..., default=0) and the if count guard keep empty input from raising.
revenues = [it["json"].get("revenue") or 0 for it in _items]
count = len(revenues)
return [{"json": {
"count": count,
"total": sum(revenues),
"average": round(sum(revenues) / count, 2) if count else 0,
"max": max(revenues, default=0),
}}]Output: {count: 3, total: 230000, average: 76666.67, max: 120000}
3. Group by a field
Mode: Run Once for All Items. setdefault builds the buckets; emit one item per group, sorted.
groups = {}
for it in _items:
row = it["json"]
key = row.get("country") or "unknown"
groups.setdefault(key, {"country": key, "customers": [], "revenue": 0})
groups[key]["customers"].append(row["name"])
groups[key]["revenue"] += row.get("revenue") or 0
return [{"json": g} for g in sorted(groups.values(), key=lambda g: g["revenue"], reverse=True)]Output: {country: "PL", customers: ["Acme", "Bar"], revenue: 200000}, then DE.
4. Deduplicate by key
Mode: Run Once for All Items. Keeps the first item per normalized key and returns the original items, so all fields are preserved.
seen = set()
unique = []
for it in _items:
key = (it["json"].get("country") or "").lower()
if key in seen:
continue
seen.add(key)
unique.append(it)
return uniqueOutput: 2 items (first PL, first DE).
5. Top N by a field
Mode: Run Once for All Items. sorted(..., key=..., reverse=True)[:N] then rank with enumerate.
top = sorted(_items, key=lambda it: it["json"].get("revenue") or 0, reverse=True)[:2]
return [{"json": {"rank": i + 1, "name": it["json"]["name"]}} for i, it in enumerate(top)]Output: {rank: 1, name: "Acme"}, {rank: 2, name: "Bar"}
6. One item per nested element
Mode: Run Once for All Items. Fan an array inside each item out into separate items — the Python equivalent of Split Out with parent fields attached.
out = []
for it in _items:
customer = it["json"]
for order in customer.get("orders", []):
out.append({"json": {"customer": customer["name"], "order_id": order["id"], "total": order["total"]}})
return outOutput: 3 items: {customer, order_id, total}
7. Validate and flag
Mode: Run Once for Each Item. Each-item mode: attach valid + problems instead of failing the run; route on valid with an IF node afterwards.
row = _item["json"]
problems = []
if not row.get("name"):
problems.append("name missing")
email = (row.get("contact") or {}).get("email")
if not email or "@" not in email:
problems.append("email missing or invalid")
return {"json": {**row, "valid": not problems, "problems": problems}}Output: Bar gets valid: false, problems: ["email missing or invalid"].
8. Keep only some items (each-item)
Mode: Run Once for Each Item. In each-item mode return None drops the item — a Filter node in code form.
if _item["json"].get("country") != "PL":
return None
return _itemOutput: 2 items (the PL customers).
9. Text report
Mode: Run Once for All Items. f-strings with format specs (:,) and "\n".join for a message body (Slack, email).
lines = [
f"- {it['json']['name']} ({it['json']['country']}): {it['json']['revenue']:,} PLN"
for it in sorted(_items, key=lambda it: it["json"]["name"])
]
return [{"json": {"report": "Customers:\n" + "\n".join(lines), "lines": len(lines)}}]Output: "Customers:\n- Acme (PL): 120,000 PLN\n- Bar (PL): 80,000 PLN\n- Foo (DE): 30,000 PLN"
10. Safe nested access
Mode: Run Once for All Items. A small dig() helper instead of chained [...] lookups that raise on missing keys/indexes. isinstance replaces the denied type().
def dig(data, *path, default=None):
for key in path:
if isinstance(data, dict) and key in data:
data = data[key]
elif isinstance(data, list) and isinstance(key, int) and -len(data) <= key < len(data):
data = data[key]
else:
return default
return data
return [{"json": {
"first_order_total": dig(it["json"], "orders", 0, "total", default=0),
"first_name": dig(it["json"], "contact", "first-name", default=""),
}} for it in _items]Output: Bar (no orders) gets first_order_total: 0.
11. Running state with nonlocal
Mode: Run Once for All Items. Code runs inside a wrapper function, so global fails — use nonlocal for state shared with helper functions.
running = 0
def add(value):
nonlocal running
running += value
return running
return [{"json": {"name": it["json"]["name"], "cumulative": add(it["json"]["revenue"])}} for it in _items]Output: cumulative 120000 → 150000 → 230000
12. ISO timestamps without datetime
Mode: Run Once for All Items. Illustrative, no input needed (the timestamps are inline). With no datetime import, ISO-8601 strings in the same timezone still sort and compare correctly as strings; slice for year/month buckets. Real date math belongs in expressions (Luxon) or JS.
stamps = ["2026-09-16T10:00:00Z", "2026-01-02T08:30:00Z", "2025-12-31T23:59:59Z"]
return [{"json": {
"latest": max(stamps),
"in_2026": [s for s in stamps if s[:4] == "2026"],
"by_month": sorted({s[:7] for s in stamps}),
}}]Output: latest: "2026-09-16T10:00:00Z", by_month: ["2025-12", "2026-01", "2026-09"]
Not covered here — and why
Parsing JSON strings, regex, hashing, real date arithmetic need
json/re/hashlib/datetime, which are blocked unless the instance allowlists them. Do these with a native node or an expression first (Date & Time or Luxon,.parseJson(),.match(), the Crypto node), or in a JavaScript Code node.Reading another node's output — native Python has no
_node. Merge the branches first, or use JavaScript$('Node Name').HTTP calls — HTTP Request node.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Python, Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Romuald Czlonkowski
- Publisher
- Romuald Czlonkowski (Community)
- Source
- czlonkowski/n8n-skills · skills/n8n-code-python · commit e037563
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Romuald Czlonkowski or Romuald Czlonkowski.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download n8n-code-python.zip