packages/cli/skills/pikku-n8n-code-translate/SKILL.md
Use when translating an n8n Code node body into a real Pikku function body. Triggered when the user opens or points at a stub generated by @pikku/n8n-import (look for `STUB — generated from n8n Code node` in the file's JSDoc), or when the user says 'translate this n8n code', 'port this n8n code node', 'finish the codeStub__... function', etc. The stub file is a `pikkuSessionlessFunc` with a Zod input/output, a JSDoc preserving the original n8n JavaScript verbatim, and a `throw new Error('… — implement me')` body.
npx skillsauth add pikkujs/pikku pikku-n8n-code-translateInstall 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.
Use this skill as an execution checklist, not reference material.
pikku-meta when available; otherwise run the relevant pikku meta ... --json command and inspect only the focused output you need..pikku, node_modules, vendored packages, or broad build artifacts.pikku-verify or pikku all when functions, wirings, schemas, or generated clients may have changed.You are translating an n8n Code node body into a Pikku pikkuSessionlessFunc body. The original JavaScript is preserved verbatim in the JSDoc above the function. Your job: replace the throw new Error(...) body with a faithful TypeScript reimplementation, keep the function signature and the JSDoc intact, and only widen the Zod input/output if the original code's data shape demands it.
This is a narrow, mechanical translation. Do not "improve" the logic, refactor for style, add error handling, or invent fields. The goal is behavioral parity, not better code.
CodeStubCustomCodeInput)CodeStubCustomCodeOutput)pikkuSessionlessFunc shapefixtures/ or you can ask the user). The two modes:
runOnceForAllItems (default) — code runs once with items: Array<{ json, binary, pairedItem }> in scope, returns an array of envelopes.runOnceForEachItem — code runs once per input item, with $json / $input.item.json in scope, returns a single envelope.
If you can't determine the mode, infer from the code: bare items.X → all-items; bare $json.X or $input.item.X → each-item.$json.userId: string but the input schema is items: z.array(z.unknown())), tighten the schemas with the smallest change that lets the code compile. Prefer z.unknown() over z.any(). Never widen output to z.any().// translated from n8n Code node, mode: runOnceForAllItems. This is the only comment you may add.yarn tsc from the package root). Fix any type errors with the smallest viable change.The n8n items is an array of { json, binary, pairedItem } envelopes. In Pikku, the input is typed — data.items is the payload array. Translate by treating items[i] as the payload directly.
| n8n | Pikku |
| -------------------------- | ------------------------------------------------- |
| items | (data.items ?? []) as any[] (or typed if known) |
| items[i].json.X | items[i].X |
| items[i].json | items[i] |
| items[i].binary | NOT supported — leave a TODO and explain |
| items.length | items.length |
| items.map(i => i.json.X) | items.map((i: any) => i.X) |
| n8n | Pikku |
| -------------------------------- | ------------------------------------------------- |
| $json.X / $input.item.json.X | data.X (assuming input is the item itself) |
| $input.item.json | data |
| $input.all() | not available per-item — change to all-items mode |
| n8n | Pikku |
| ---------------------------------------- | ----------------------------------------------------------- |
| return [{ json: X }] | return { items: [X] } |
| return items.map(i => ({ json: ... })) | return { items: items.map(...) } |
| return [{ json: X }, { json: Y }] | return { items: [X, Y] } |
| return { json: X } (each-item) | return X |
| return [...] (already plain) | wrap in { items: [...] } only if output schema expects it |
If the code references any of the following, stop, leave the body as a stub, and ask the user how to handle it (or annotate with a // TODO: line and explain in your reply):
this.helpers.* (binary buffers, HTTP requests, prepareBinaryData, etc.)$node['Some Node'].json (cross-node references — these need to be resolved via Pikku's ref() upstream wiring, not in the function body)$workflow, $execution, $item(), $items('Other Node')$now, $today, $env.X — translate to new Date(), new Date(), and services.variables.get('X') respectively, but only if the function signature lets you reach services (it does — first param). NOTE: process.env is forbidden per Pikku house rules; always use services.variables.get().getBinaryDataBuffer / getStaticData — Pikku has no equivalent, leave a TODOrequire() / dynamic import() — flag and stopawait, the Pikku body is already async — keep all await calls intact.this.helpers.httpRequest(...), do not translate — the user should use a separate httpRequest rpc node in the workflow, not embed the call. Leave a // TODO: and explain.items as any[] only if the schema is z.array(z.unknown()). If the user has tightened the schema, use the inferred type.as any on the return value. If the return doesn't match the output schema, the schema is wrong — fix it (step 5).import { z } from 'zod'
import { pikkuSessionlessFunc } from '#pikku'
export const CodeStubCustomCodeInput = z.object({
items: z.array(z.unknown()),
})
export const CodeStubCustomCodeOutput = z.object({
items: z.array(z.unknown()),
})
/**
* STUB — generated from n8n Code node "Custom Code".
*
* Original n8n JavaScript (preserved verbatim for reference; rewrite for Pikku semantics):
*
* const total = items.reduce((acc, i) => acc + i.json.amount, 0);
* return [{ json: { total } }];
*
* TODO: re-implement in TypeScript. ...
*/
export const codeStubCustomCode = pikkuSessionlessFunc({
description: 'Stub: ported from n8n Code node "Custom Code"',
input: CodeStubCustomCodeInput,
output: CodeStubCustomCodeOutput,
func: async (_services, _data) => {
throw new Error(
'Stub: ported from n8n Code node "Custom Code" — implement me'
)
},
})
export const codeStubCustomCode = pikkuSessionlessFunc({
description: 'Ported from n8n Code node "Custom Code"',
input: CodeStubCustomCodeInput,
output: CodeStubCustomCodeOutput,
func: async (_services, data) => {
// translated from n8n Code node, mode: runOnceForAllItems
const items = (data.items ?? []) as any[]
const total = items.reduce((acc, i) => acc + i.amount, 0)
return { items: [{ total }] }
},
})
A short summary, no fluff:
items[i].json.amount → items[i].amount).Do not:
try/catch unless the original didIf the original is empty, contains only comments, or is so dependent on n8n internals (binary, cross-node refs, helpers) that no honest translation is possible, leave the stub in place and tell the user which n8n features are blocking it.
documentation
Deprecated — use pikku-middleware instead. Tag middleware (addTagMiddleware) is now documented as a section within the pikku-middleware skill, alongside global HTTP middleware, execution order, and the service-to-service bearer auth pattern.
testing
Use when adding authorization checks to Pikku functions or routes — pikkuPermission, pikkuAuth, per-function permissions, pattern-based permissions, or understanding OR/AND permission logic. TRIGGER when: user wants to restrict who can call a function, check resource ownership, add role-based access, or understand where permission checks belong. DO NOT TRIGGER when: user asks about middleware or request interception (use pikku-middleware), authentication strategies (use pikku-security), or session management.
testing
Use when adding any middleware to a Pikku app — global HTTP middleware, tag-scoped middleware (including service-to-service bearer auth), per-route middleware, session-setting middleware, or understanding middleware execution order and priority. TRIGGER when: user wants middleware on some or all routes, machine-to-machine auth, tag-scoped cross-cutting concerns, global interceptors, or middleware priority/order questions. DO NOT TRIGGER when: user asks about permissions/authorization checks (use pikku-permissions), auth strategies like authBearer/authCookie (use pikku-security), or deployment.
documentation
Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `pikku create`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.