skills/n8n-code-javascript/SKILL.md
Use when writing JavaScript in n8n Code nodes, choosing between Code node and built-in nodes, debugging Code node errors (return format, undefined data, silent failures), or implementing pagination/retry/dedup in code. NEVER for expression fields (use n8n-expression-syntax) or Python Code nodes (use n8n-code-python).
npx skillsauth add sharkitect-solutions/sharkitect-claude-toolkit n8n-code-javascriptInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The Code node is the escape hatch, not the default. 80% of workflows need zero Code nodes. When you do need one, failures cluster around three things: wrong return format, wrong data path, and missing error handling. This skill prevents all three.
Use a built-in node when any single node can handle it:
| Task | Use This Instead | Why Not Code |
|------|-----------------|--------------|
| Rename/add fields | Set node | Faster, no maintenance, no return format risk |
| True/false filter | Filter node | Built-in operators handle 95% of cases |
| Single condition branch | IF node | Cleaner workflow graph |
| Multi-value branch | Switch node | Easier to read and modify |
| HTTP call (standalone) | HTTP Request node | Built-in auth, pagination, retry |
| Date formatting only | Expression: {{$now.toFormat(...)}} | No Code node needed for simple format |
Code node is correct when:
The test: If you can describe the operation in one clause ("filter where X > Y", "rename A to B"), use a built-in node. If you need two clauses connected by "and then" or "based on", consider Code.
"Run Once for All Items" (default -- use 95% of the time)
$input.all() or $input.first()."Run Once for Each Item" (rare)
$input.item.Decision rule: If you need to compare/combine across items, use All Items mode. If each item is 100% independent, consider Each Item mode. When unsure, use All Items -- you can always loop inside.
[{json: {...}}]Every Code node must return an array of objects, each with a json property.
// CORRECT - single result
return [{json: {total: 42, status: "done"}}];
// CORRECT - multiple results
return [
{json: {id: 1, name: "Alice"}},
{json: {id: 2, name: "Bob"}}
];
// CORRECT - empty (no output items)
return [];
// CORRECT - transforming input
return $input.all().map(item => ({
json: {...item.json, processed: true}
}));
Wrong formats that SILENTLY FAIL or error:
return {json: {result: "ok"}}; // missing array wrapper
return [{result: "ok"}]; // missing json property
return [{data: {result: "ok"}}]; // "data" is not "json"
return "done"; // not an array of objects
$json.body, not $jsonThe Webhook node wraps incoming payloads under .body. Accessing $json.name returns undefined.
// WRONG - returns undefined
const name = $json.name;
// CORRECT - webhook payload is nested
const name = $json.body.name;
// Full webhook structure:
// $json.headers - HTTP headers
// $json.body - POST/PUT payload (YOUR DATA)
// $json.query - URL query parameters (?key=value)
// $json.params - URL path parameters
{{}} expression syntax in Code nodes{{$json.field}} is expression syntax for Set/IF/HTTP Request nodes. In Code nodes, use plain JavaScript.
// WRONG - treated as literal string, not evaluated
const name = "{{ $json.name }}";
// CORRECT - direct JavaScript access
const name = $json.name;
const name = $input.first().json.name;
Don't memorize API syntax -- know WHEN to reach for each tool. Full syntax in references/api-reference.md.
| You Need To... | Use This | Key Gotcha |
|----------------|----------|------------|
| Read data from previous node | $input.all() / $input.first() | Items have .json property: items[0].json.name not items[0].name |
| Read data from a specific node | $node["Exact Name"].json | Case-sensitive. Quotes required. .json mandatory. |
| Make an HTTP request | $helpers.httpRequest({...}) | Must await. No built-in auth/retry/pagination. |
| Format/compare dates | DateTime (Luxon) | NOT JS Date. Use .toFormat(), .plus(), .minus() |
| Query complex JSON | $jmespath(data, 'path') | First arg is the object, not $input |
| Persist data across runs | $getWorkflowStaticData() | Mutate the object directly; changes auto-save |
| Read env variables | $env.VAR_NAME | Read-only. Set in n8n server config, not workflow |
| Hash/encode data | require('crypto'), Buffer | Only crypto, Buffer, URL, URLSearchParams available |
Cause: Return value isn't [{json: {...}}].
Fix: Check every return path. Common traps:
// TRAP 1: .map() without json wrapper
return items.map(i => i.json); // WRONG
return items.map(i => ({json: i.json})); // CORRECT
// TRAP 2: conditional return misses a branch
if (condition) {
return [{json: {result: "yes"}}];
}
// MISSING: no return for else branch -- returns undefined
// FIX: always return [] for empty case
return [];
// TRAP 3: async function forgot return
const data = await $helpers.httpRequest({...});
// forgot to return! Node outputs nothing
return [{json: data}]; // FIX
Cause: Data path is wrong. The object you're accessing doesn't exist. Diagnosis pattern:
// Step 1: Log what you actually have
const items = $input.all();
return [{json: {debug_first_item: items[0]?.json, debug_count: items.length}}];
// Run once, check output, then write real logic
Common causes:
$json.body.field, not $json.field.json segment: $node["Name"].json.fieldCause: Code ran but returned [] or all items were filtered out.
Diagnosis:
const items = $input.all();
if (items.length === 0) {
return [{json: {error: "NO INPUT ITEMS", hint: "Check previous node output"}}];
}
// ... rest of logic
Cause: Missing await or typo in method name.
Fix: Always await $helpers.httpRequest({...}). The $helpers object is always available -- if it's "not a function", you likely have a typo.
Cause: Using DateTime in an environment where Luxon isn't loaded (rare), or typo.
Fix: DateTime is a global in n8n Code nodes -- no import needed. Check capitalization: DateTime, not datetime or Datetime.
const allResults = [];
let page = 1;
const pageSize = 100;
while (true) {
const response = await $helpers.httpRequest({
method: 'GET',
url: `https://api.example.com/items`,
qs: { page, limit: pageSize },
json: true,
simple: false
});
if (!response || !response.data || response.data.length === 0) break;
allResults.push(...response.data);
if (response.data.length < pageSize) break; // last page
page++;
if (page > 50) break; // safety cap
}
return allResults.map(item => ({json: item}));
async function withRetry(fn, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries) throw err;
// exponential backoff: 1s, 2s, 4s
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
}
}
}
const data = await withRetry(() =>
$helpers.httpRequest({ method: 'GET', url: 'https://api.example.com/data', json: true })
);
return [{json: data}];
const staticData = $getWorkflowStaticData();
const seen = new Set(staticData.processedIds || []);
const items = $input.all();
const newItems = items.filter(item => {
if (seen.has(item.json.id)) return false;
seen.add(item.json.id);
return true;
});
staticData.processedIds = [...seen].slice(-10000); // cap memory
return newItems.length > 0 ? newItems : [{json: {status: "no_new_items"}}];
// When upstream data shape is uncertain, guard access:
const items = $input.all();
return items.map(item => {
const d = item.json || {};
return {
json: {
name: d.body?.name || d.name || 'unknown',
email: d.body?.email || d.email || '',
id: d.body?.id ?? d.id ?? null,
}
};
});
Before writing a Code node, answer these questions:
$input.all() for batch, $input.first() for single, $node["Name"].json for specific node..body.[{json: {...}}]? Check if/else branches, empty cases, error paths.return {json: {...}} silently breaks. MUST be [{json: {...}}]{{}} expression syntax inside Code nodes -- use direct JS: $json.field not "{{$json.field}}"$json.body.field, not $json.fieldjson wrapper in return items -- [{field: value}] silently fails, must be [{json: {field: value}}]$input.item in "All Items" mode -- it is undefined. Use $input.all()$input.all() in "Each Item" mode -- use $input.item insteaditems[0].name -- item data is at items[0].json.name (the .json property is required)moment or new Date() for formatting -- use the built-in DateTime (Luxon) object$helpers.httpRequest() calls without await -- you'll get a Promise object instead of datadevelopment
When the user wants help with paid advertising campaigns on Google Ads, Meta (Facebook/Instagram), LinkedIn, Twitter/X, or other ad platforms. Also use when the user mentions 'PPC,' 'paid media,' 'ad copy,' 'ad creative,' 'ROAS,' 'CPA,' 'ad campaign,' 'retargeting,' or 'audience targeting.' This skill covers campaign strategy, ad creation, audience targeting, and optimization.
testing
--- name: using-sharkitect-methodology description: Use when starting any conversation in a Sharkitect workspace OR before any task involving NEW pricing, positioning, proposal, strategy, plan-execution, or schema-design work — mandates invocation of Sharkitect-specific methodology skills (pricing-strategy, marketing-strategy-pmm, smb-cfo, hq-revenue-ops, executing-plans, brainstorming) under the same anti-rationalization discipline as using-superpowers. Documentation has failed 4 times across H
testing
Use when user says 'end session', 'wrap up', 'stop for the day', 'done for today', 'close out', 'save session', 'wrapping up', or invokes /end-session. Runs the full 9-step end-of-session protocol: resource audit, MEMORY.md update, lessons capture, plan status, pending items, workspace checklist, .tmp/ audit, git commit+push, Supabase brain sync, session brief, summary. Final step schedules a detached self-kill of the current session ONLY (3s delay) so the window closes cleanly. Other claude.exe processes (active workspaces) are NOT touched -- orphan cleanup is handled separately by Claude-Orphan-Cleanup-Hourly with proper age safeguards. Do NOT use for: mid-session quick saves (use session-checkpoint), skill syncing (use sync-skills.py), brain memory queries (use supabase-sync.py pull), document freshness reviews (use document-lifecycle), resource gap detection (use resource-auditor).
testing
Remove signs of AI-generated writing from text. Use when editing or reviewing text to make it sound more natural and human-written. Based on Wikipedia's comprehensive "Signs of AI writing" guide. Detects and fixes patterns including: inflated symbolism, promotional language, superficial -ing analyses, vague attributions, em dash overuse, rule of three, AI vocabulary words, passive voice, negative parallelisms, and filler phrases.