All skills
czlonkowski avatar

/n8n-code-python

@d6465af

Write Python in n8n Code nodes (native Python, `language` pythonNative, n8n 2.x). Use when the user explicitly wants Python in a Code node, when migrating old Pyodide/"Python (Beta)" code that used _input/_json/_node/_now, or when a Python Code node fails with NameError, "Security violations detected", "Import of standard library module … is disallowed", "__build_class__ not found", "A 'json' property isn't a dictionary", or "Python runner unavailable". Covers the only two variables (_items/_item), dict-only access, imports blocked by default, the sandbox's denied builtins, accepted return shapes, and how errors interact with onError. JavaScript is the default for Code nodes — native Python has no n8n helpers and, by default, no imports. EXCEPTION — for Python in the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode), use the n8n-code-tool skill instead (input is _query, return must be a string).

Use this Skill: https://skilld.dev/gh/czlonkowski/n8n-skills/n8n-code-python

This session only. Nothing lands on disk.

COMMON_PATTERNS.md

≈2.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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": "a@acme.pl", "first-name": "Jan"},  "orders": [{"id": "A1", "total": 500}, {"id": "A2", "total": 1500}]},
  {"name": "Foo",  "country": "DE", "revenue": 30000,  "active": false, "contact": {"email": "f@foo.de", "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 unique

Output: 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 out

Output: 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 _item

Output: 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 in an expression (JSON.parse, .match(), Luxon), the Crypto node, or 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.

Source: SKILL.md on GitHub

1 warning14d5 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    This skill provides safe guidance and code patterns for writing Python within n8n's native task-runner environment. It correctly identifies and describes the platform's sandbox limitations, including disabled imports and restricted built-in functions, ensuring developers work within established security boundaries.

  • Socket14d

    No alerts

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer7mo

    6/6 files flagged

  • ZeroLeaks5mo

    2 findings · Score: 80/100

Signed by skilld at d6465af. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 weeks ago
  • Python
  • n8n
  • code-node
  • workflow
  • data-transformation
  • standard-library
  • beta

README badge

README badge for czlonkowski/n8n-skills/n8n-code-python

Writes Python code in n8n Code nodes using the _input/_json/_node helper syntax, with access to standard library functions only (json, datetime, re, base64, hashlib, urllib.parse, math, random, statistics). Use this skill when a user explicitly requests Python for n8n workflows; JavaScript is recommended for most cases since it has fuller n8n helper support and no external library restrictions.

Generated from the current SKILL.md.

When should I use Python instead of JavaScript in n8n?
Only when you need specific Python standard library functions (regex, hashlib, statistics) or are significantly more comfortable with Python syntax. JavaScript is recommended for 95% of use cases because it has access to all n8n helper functions and better documentation.
What external libraries can I import in n8n Python Code nodes?
No external libraries are available by default — only the Python standard library (json, datetime, re, base64, hashlib, urllib.parse, math, random, statistics). Self-hosted instances may have additional packages depending on their Python runner configuration.
What is the correct return format for a Python Code node?
Always return a list of dictionaries with a 'json' key: `[{"json": {...}}]`. Returning a plain dictionary, list without the json wrapper, or other formats will cause the workflow to fail.
Why does my webhook data return undefined when I access it directly?
Webhook data is nested under the 'body' property. Access it via `_json.get('body', {})` rather than directly from `_json`, or use `.get()` for safe access to nested fields.
What's the difference between Python (Beta) and Python (Native) modes?
Python (Beta) uses `_input`, `_json`, `_node` helper syntax and built-in helpers like `_now`, making it better for n8n integration. Python (Native) uses only `_items` and `_item` variables with no helpers — use Python (Beta) for most cases.

Generated from the current SKILL.md. These answers refresh after source changes.