Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Write and debug JavaScript or Python for the AI-callable n8n Custom Code Tool, including schemas, sandbox limits, and return formats.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-16 | ✗→✓ | ▲ Improved | 199% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 168% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 208% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 176% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 191% | 0% |
Use this skill specifically for code executed by the AI-agent-callable n8n Custom Code Tool. Use the separate JavaScript or Python Code-node skills for ordinary workflow Code nodes.
Do not hardcode secrets or accept arbitrary executable code from untrusted input. Constrain inputs with a schema, validate outputs, allowlist any network destinations, and ask before testing a tool whose code can write data or invoke an external service.
Expert guidance for writing code inside @n8n/n8n-nodes-langchain.toolCode — the tool an AI Agent can invoke, not the regular workflow Code node.
The Custom Code Tool looks like a Code node in the editor — same JavaScript editor, similar layout — but it is a completely different node from a different package with a different runtime contract.
| | Code node | Custom Code Tool | |---|---|---| | Node type | n8n-nodes-base.code | @n8n/n8n-nodes-langchain.toolCode | | Package | n8n-nodes-base | @n8n/n8n-nodes-langchain | | Invoked by | Previous node (workflow flow) | AI Agent (LangChain) | | Input | $input.all() — item stream | query — string or object from LLM | | Return | [{json: {...}}] (items array) | A string | | $fromAI() | N/A | Not available (see Errors) | | HTTP helper | this.helpers.httpRequest (auth helpers blocked) | Not exposed to the tool sandbox | | State | Per-run execution data | No getContext, no $getWorkflowStaticData |
If you treat it like a Code node, it fails. The rest of this skill covers the Code Tool's actual contract.
javascript// `query` is whatever the AI sent (a string by default) return `You asked: ${query}`;
python# `_query` is whatever the AI sent (a string by default) return f"You asked: {_query}"
"The response property should be a string, but it is an object".query (JS), _query (Python). You cannot rename it.$fromAI() inside the Code Tool sandbox — it throws "No execution data available".[{json: {...}}] return format — that's for Code nodes. Throws "Wrong output type returned".The Code Tool has two input shapes, controlled by specifyInputSchema:
specifyInputSchema: false)The AI passes a single string as query. If you need multiple fields, the AI has to stuff them into that one string and you parse them out. In practice, LLMs will happily pass a JSON string if your description tells them to.
javascript// Parse a JSON string the AI sent let params; try { params = typeof query === 'string' ? JSON.parse(query) : query; } catch (e) { throw new Error('Expected a JSON object. Parser said: ' + e.message); } const price = Number(params.price); const months = Number(params.months); // ... return JSON.stringify({ monthly_payment: /* ... */ });
Pros: simplest to set up, one field to describe. Cons: no schema validation — if the LLM forgets a field, the tool throws at runtime.
Best for: quick prototypes, tools with one natural input (a question, a URL, a text blob).
specifyInputSchema: true)The tool becomes a LangChain DynamicStructuredTool. The LLM sees a typed argument schema and passes a validated object as query. You access fields directly.
javascript// query is now an object matching your schema const price = query.price; const months = query.months; const residual_percent = query.residual_percent; const monthly = computeAnnuity(price, months, residual_percent); return JSON.stringify({ monthly_payment: monthly });
Schema is defined via either:
schemaType: "fromJson" + jsonSchemaExample (n8n v≥1.3) — paste an example JSON, n8n infers the schemaschemaType: "manual" + inputSchema — write a full JSON Schema yourselfPros: LLM gets type hints, invalid calls rejected before your code runs, cleaner code. Cons: a little more setup; requires n8n version with schema support.
Best for: production tools with multiple typed parameters (calculators, API wrappers, anything with numeric fields the LLM tends to stringify).
See: references/INPUT_SCHEMA.md for complete schema setup.
The return value must be a string. The LLM reads it as the tool's observation.
javascript// ✅ String return "42"; // ✅ Number (auto-converted to string by n8n) return 42; // ✅ JSON-encoded structured result (recommended for rich output) return JSON.stringify({ result: 42, currency: "SEK" }); // ❌ Raw object → "The response property should be a string, but it is an object" return { result: 42 }; // ❌ Workflow item format → "Wrong output type returned" return [{ json: { result: 42 } }]; // ❌ Array → "The response property should be a string, but it is an object" return [1, 2, 3];
When your tool has more than a trivial scalar output, return a JSON string:
javascriptreturn JSON.stringify({ monthly_payment_sek: 5405, loan_amount: 351920, total_cost_of_credit: 63295 });
The LLM parses JSON reliably and can pick the fields it needs to present to the user.
Errors don't just stop the workflow — they go back to the LLM, which usually corrects its call and retries. Use that:
javascript// Option A: throw — n8n surfaces the message to the agent if (!isFinite(price)) throw new Error('price must be a number, e.g. 439900'); // Option B: return an error string — agent reads it like any tool result if (!isFinite(price)) return JSON.stringify({ error: 'price must be a number, e.g. 439900' });
Either way, write error messages for the LLM: state what was wrong and what a valid call looks like. A bare throw new Error('invalid input') wastes the retry; an instructive message usually fixes the next call.
These fields are NOT documentation — they are the tool contract the LLM sees. Treat them as prompt engineering.
[A-Za-z0-9_]+ (v1.1+). No spaces, no hyphens, no emoji.calculate_car_loan, get_weather, search_orders.Code Tool (the default) is useless — the agent won't know when to call it.Unstructured example (JSON-in-string pattern):
Deterministiskt beräknar månadskostnad för billån. Anropa med EN JSON-sträng:
{"price":439900,"down_payment":87980,"interest_rate":6.95,"months":36,"residual_percent":50}
Fält: price (SEK), down_payment (SEK), interest_rate (% per år), months, residual_percent (0-99).Structured example (schema-defined):
Deterministically computes the monthly car-loan payment given price, down payment,
annual interest rate, term, and residual percent. Use whenever the user asks for
monthly cost, total credit cost, or loan breakdown."There was an error: 'Cannot assign to read only property \"name\" of object: Error: No execution data available'"Cause: you called $fromAI() inside the Code Tool sandbox.
Fix: $fromAI() is a helper for other tool-enabled nodes (HTTP Request Tool, SendGrid Tool, toolWorkflow, etc.) — it's not exposed inside toolCode. Read the AI's input from query directly (or use specifyInputSchema for structured fields).
"Wrong output type returned"Cause: you returned a workflow-style array like [{ json: { ... } }]. That's the Code node contract, not the Code Tool contract.
Fix: return a string. For structured data, return JSON.stringify(output).
"The response property should be a string, but it is an object"Cause: you returned a plain object or array.
Fix: JSON.stringify() the result, or coerce to a string.
Cause: tool name is generic (Code Tool, My Tool) or description doesn't clearly state when to use it.
Fix: rename to a verb-y name (calculate_car_loan), and rewrite the description to explicitly state the trigger conditions (e.g. "Use this whenever the user asks about monthly cost").
queryCause: unstructured tool with a vague description. The LLM guesses at the format.
Fix: either (a) include a concrete JSON example in the description, or (b) switch to specifyInputSchema: true so the LLM gets a typed schema.
See: references/ERROR_PATTERNS.md for full catalog with reproductions.
The Code Tool sandbox is narrower than the Code node sandbox. Don't assume helpers carry over:
| Helper | Code node | Code Tool | |---|---|---| | $input.all(), $input.first(), $input.item | ✅ | ❌ | | $node["NodeName"] | ✅ | ❌ | | $json, $binary | ✅ | ❌ | | $fromAI() | ❌ | ❌ (despite sitting next to an AI agent) | | this.helpers.httpRequest() | ✅ | ❌ | | DateTime (Luxon) | ✅ | ✅ (standard in JS sandbox) | | $jmespath() | ✅ | ❌ | | this.getContext(...) | ✅ | ❌ | | $getWorkflowStaticData(...) | ✅ | ❌ |
Implication: the Code Tool is for pure computation. If you need an HTTP call, an API lookup, or cross-invocation state, use a different tool node:
toolWorkflow (Call Sub-workflow Tool) for multi-step logic with access to the full Code node sandboxUse Code Tool when:
Use toolWorkflow (Call Sub-workflow Tool) when:
$fromAI() typingthis.helpers, credentials, or other nodesUse HTTP Request Tool when:
$fromAI() bindings in URL/query/bodyRule of thumb: if you find yourself wanting $fromAI(), you probably want toolWorkflow instead of toolCode.
A production calculator tool (unstructured, JSON-in-string pattern):
json{ "parameters": { "name": "calculate_car_loan", "description": "Computes monthly car-loan payment using an annuity formula with residual/balloon. Call with a single JSON string. Example: {\"price\":439900,\"down_payment\":87980,\"interest_rate\":6.95,\"months\":36,\"residual_percent\":50,\"setup_fee\":695,\"monthly_admin_fee\":59}. Required: price, down_payment, interest_rate, months, residual_percent. Optional: setup_fee, monthly_admin_fee (default 0).", "language": "javaScript", "jsCode": "let params;\ntry {\n params = typeof query === 'string' ? JSON.parse(query) : query;\n} catch (e) {\n throw new Error('Invalid JSON: ' + e.message);\n}\n\nconst price = Number(params.price);\nconst down_payment = Number(params.down_payment);\nconst interest_rate = Number(params.interest_rate);\nconst months = Number(params.months);\nconst residual_percent= Number(params.residual_percent);\nconst setup_fee = Number(params.setup_fee ?? 0) || 0;\nconst monthly_admin_fee = Number(params.monthly_admin_fee ?? 0) || 0;\n\nif (!isFinite(price) || price <= 0) throw new Error('price must be > 0');\nif (down_payment < 0 || down_payment >= price) throw new Error('down_payment must be in [0, price)');\n\nconst principal = price - down_payment;\nconst residual = price * (residual_percent / 100);\nconst r = interest_rate / 100 / 12;\nconst growth = Math.pow(1 + r, months);\nconst base = r === 0\n ? (principal - residual) / months\n : (principal - residual / growth) * r / (1 - 1 / growth);\nconst monthly_payment = base + monthly_admin_fee;\n\nreturn JSON.stringify({\n monthly_payment_sek: Math.round(monthly_payment),\n loan_amount: Math.round(principal),\n residual_value_sek: Math.round(residual),\n total_cost_of_credit: Math.round(monthly_payment * months + residual + setup_fee - principal)\n});" }, "type": "@n8n/n8n-nodes-langchain.toolCode", "typeVersion": 1.3, "name": "calculate_car_loan" }
Wire it into an AI Agent via the ai_tool connection type.
n8n-code-javascript: the Code node skill. Most JavaScript patterns (arrays, map/filter, DateTime) transfer — but I/O contract is different. Don't copy data-access code.
n8n-node-configuration: specifyInputSchema is a classic displayOptions-driven conditional field. Use get_node({detail: "standard"}) on @n8n/n8n-nodes-langchain.toolCode to see schema-related properties.
n8n-workflow-patterns: Code Tool sits inside the "AI Agent with tools" pattern. An agent typically has several tools; Code Tool is the "local compute" option.
n8n-validation-expert: the three Code Tool errors listed above have clear signatures — if validation surfaces "Wrong output type returned", you know to switch from array-of-items to a string.
Before deploying a Code Tool:
@n8n/n8n-nodes-langchain.toolCode (not nodes-base.code)calculate_car_loan)query (JS) or _query (Python)$fromAI() in the code body$input / $json / $helpers — those aren't in the sandboxJSON.stringify() for structured output)ai_tool connectionRemember: the Code Tool is a LangChain tool wearing a Code-node UI. Contract is: string in, string out. Everything else follows from that.
Other measured skills in the registry, with their headline benchmark lift.