---
name: pikku-n8n-code-translate
description: "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."
metadata:
  version: 1.0.0
---

# n8n Code Node → Pikku Function Translator

## Agent Operating Procedure

Use this skill as an execution checklist, not reference material.

1. Discover before editing. Prefer OpenCode tools such as `pikku-meta` when available; otherwise run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.

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.

## Process

1. **Read the file** the user points you at. Identify:
   - The exported input schema (e.g. `CodeStubCustomCodeInput`)
   - The exported output schema (e.g. `CodeStubCustomCodeOutput`)
   - The verbatim JavaScript inside the JSDoc block
   - The function name and `pikkuSessionlessFunc` shape
2. **Determine the n8n mode** from the original n8n JSON if available (the importer leaves the JSON on disk in `fixtures/` 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.
3. **Apply the rubric below** to translate.
4. **Edit the file** to replace only the function body. Leave the import block, schema definitions, JSDoc, function name, description, and Zod refs untouched unless step 5 forces a change.
5. **If the schemas are wrong** (e.g. the code reads `$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()`.
6. **Add a one-line comment at the top of the body** noting the n8n mode you assumed: `// translated from n8n Code node, mode: runOnceForAllItems`. This is the _only_ comment you may add.
7. **Run the test/typecheck** if available (`yarn tsc` from the package root). Fix any type errors with the smallest viable change.

## Translation rubric

### Envelope unwrapping (all-items mode)

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)`                      |

### Envelope unwrapping (each-item mode)

| 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 |

### Return statement

| 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 |

### n8n built-ins and helpers — do NOT auto-translate

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 TODO
- `require()` / dynamic `import()` — flag and stop

### Async

- If the original uses `await`, the Pikku body is already `async` — keep all `await` calls intact.
- If the original references `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.

### Types

- Cast incoming `items` as `any[]` only if the schema is `z.array(z.unknown())`. If the user has tightened the schema, use the inferred type.
- Never use `as any` on the _return_ value. If the return doesn't match the output schema, the schema is wrong — fix it (step 5).

## Example

### Before (stub the importer emitted)

```ts
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'
    )
  },
})
```

### After (your output)

```ts
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 }] }
  },
})
```

## What you should report back to the user

A short summary, no fluff:

- The mode you inferred and why (one sentence).
- The literal translations you applied from the rubric (e.g. `items[i].json.amount → items[i].amount`).
- Anything you flagged as a TODO and why.
- Any schema tightening you did (with before → after).

Do **not**:

- Add tests
- Refactor surrounding code
- Edit other files unless the user asked
- "Improve" the logic
- Wrap things in `try/catch` unless the original did

If 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.
