mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
fix(core): Prefer native nodes over Code nodes in n8n Assistant workflow builds (#38598)
This commit is contained in:
parent
c3f9dedef6
commit
4e1fcf6eac
|
|
@ -6,8 +6,10 @@ nodes. They are a runtime checklist, not extra user-facing output.
|
|||
|
||||
Code-node runtime limits (no network, forbidden imports, nested template
|
||||
literals) and unsolicited stickies are enforced by `workflow-sdk validate` —
|
||||
fix those findings before `build-workflow`. Prefer built-in nodes for simple
|
||||
split, map, filter, merge, and aggregate work.
|
||||
fix those findings before `build-workflow`. Use built-in nodes (Edit Fields
|
||||
(Set), Filter, IF / Switch, Sort, Remove Duplicates, Aggregate, Split Out, Merge)
|
||||
for shaping, filtering, routing, sorting, de-duplicating and aggregating work; a
|
||||
Code node is only for the reserved cases the workflow-builder skill lists.
|
||||
|
||||
## Preserve Source Data
|
||||
|
||||
|
|
|
|||
|
|
@ -591,8 +591,12 @@ unsolicited `sticky()`, forbidden builder constructs (e.g. `.map()`), and
|
|||
repeated `.onTrue()` / `.onFalse()` overwrites on the same IF variable. Fix
|
||||
every reported error and warning before calling `build-workflow`.
|
||||
|
||||
- Avoid code node where possible, use n8n nodes that help do the same thing.
|
||||
If it makes it simpler, go ahead and use code node.
|
||||
- Native node first: shape, compute, default or format fields with
|
||||
**Edit Fields (Set)** and expressions (full JavaScript); **Filter**, **IF** /
|
||||
**Switch**, **Sort**, **Remove Duplicates**, **Aggregate**, **Split Out**,
|
||||
**Limit** and **Merge** cover the rest. A Code node is only for multi-pass
|
||||
algorithms, `$getWorkflowStaticData` state, fence-stripping model output,
|
||||
try/catch around upstream node access, or a step needing three or more nodes.
|
||||
- Write Code nodes in JavaScript unless the user explicitly asks for Python.
|
||||
`language: 'pythonNative'` runs a locked-down runner that defines only `_items`
|
||||
(all-items mode), `_item` (per-item mode) and `print()` — no `_('Node Name')`,
|
||||
|
|
@ -603,8 +607,8 @@ every reported error and warning before calling `build-workflow`.
|
|||
anything the runner would reject.
|
||||
- SDK builder code is a restricted subset of TypeScript that builds a static
|
||||
graph; it is not a Code node and does not run. Build strings with template
|
||||
literals; do runtime joining, aggregation, or transforms in a Code node or
|
||||
`expr()`. Full allowed/forbidden list:
|
||||
literals; do runtime joining, aggregation, or transforms with `expr()` in a
|
||||
native node. Full allowed/forbidden list and "Native node mappings" table:
|
||||
`${N8N_WORKSPACE_DIR}/knowledge-base/reference/workflow-sdk-language.md`.
|
||||
- Use `@n8n/workflow-sdk`.
|
||||
- Do not specify node positions. They are auto-calculated by the layout engine.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { NATIVE_NODE_PREFERENCE } from '@n8n/workflow-sdk/prompts/node-selection';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
|
||||
import type { SandboxWorkspace } from '../../workspace/sandbox-fs';
|
||||
|
|
@ -106,9 +107,13 @@ describe('buildKnowledgeBaseWorkspaceBundle', () => {
|
|||
),
|
||||
).toContain('# Workflow Builder Guardrails');
|
||||
|
||||
expect(
|
||||
bundle.files.get(`${ROOT}/${SANDBOX_KNOWLEDGE_BASE_DIR}/reference/workflow-sdk-language.md`),
|
||||
).toContain('# Workflow SDK language reference');
|
||||
const sdkLanguageReference = bundle.files.get(
|
||||
`${ROOT}/${SANDBOX_KNOWLEDGE_BASE_DIR}/reference/workflow-sdk-language.md`,
|
||||
);
|
||||
expect(sdkLanguageReference).toContain('# Workflow SDK language reference');
|
||||
// The Code-node-to-native-node mapping table rides along in the same file.
|
||||
expect(sdkLanguageReference).toContain('## Native node mappings');
|
||||
expect(sdkLanguageReference).toContain(NATIVE_NODE_PREFERENCE);
|
||||
|
||||
// Rules only: the when-to-group guidance already lives in the skill's
|
||||
// always-loaded "## Node Groups" section, so it is not repeated here.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
WorkflowTechnique,
|
||||
type WorkflowTechniqueType as BestPracticesGuideId,
|
||||
} from '@n8n/workflow-sdk/prompts/best-practices';
|
||||
import { NATIVE_NODE_PREFERENCE } from '@n8n/workflow-sdk/prompts/node-selection';
|
||||
import {
|
||||
NODE_GROUPS_REFERENCE,
|
||||
SDK_LANGUAGE_REFERENCE,
|
||||
|
|
@ -165,9 +166,11 @@ const KNOWLEDGE_BASE_REFERENCE_ENTRIES: Array<
|
|||
{
|
||||
id: 'workflow-sdk-language',
|
||||
description:
|
||||
'Allowed/forbidden constructs in workflow SDK builder code: methods, globals, language subset, node groups',
|
||||
'Allowed/forbidden constructs in workflow SDK builder code: methods, globals, language subset, node groups, native node mappings that replace Code nodes',
|
||||
fileName: 'workflow-sdk-language.md',
|
||||
content: SDK_LANGUAGE_REFERENCE,
|
||||
// The mapping table ships with the SDK reference so the builder reads
|
||||
// "which native node replaces this Code node" in the same file.
|
||||
content: `${SDK_LANGUAGE_REFERENCE}\n## Native node mappings\n\n${NATIVE_NODE_PREFERENCE}\n`,
|
||||
},
|
||||
{
|
||||
id: 'node-groups',
|
||||
|
|
|
|||
|
|
@ -63,6 +63,24 @@ describe('Instance AI runtime skills', () => {
|
|||
expect(skill).toContain('knowledge-base/reference/workflow-sdk-language.md');
|
||||
});
|
||||
|
||||
// Code nodes were 19% of all nodes in production builds versus 10% in public
|
||||
// templates, and half of a sample were plain field shaping. The old rule ended
|
||||
// with an escape hatch ("if it makes it simpler, go ahead and use code node").
|
||||
it('prefers native nodes over Code nodes with no escape hatch', () => {
|
||||
const skill = readFileSync(
|
||||
join(INSTANCE_AI_SKILLS_DIR, 'workflow-builder', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(skill).not.toMatch(/if it makes it simpler, go ahead and use code node/i);
|
||||
expect(skill).toContain('Native node first');
|
||||
expect(skill).toContain('Edit Fields (Set)');
|
||||
expect(skill).toContain('Native node mappings');
|
||||
// The Python bullet that follows the rule stays as it was.
|
||||
expect(skill).toContain(
|
||||
'Write Code nodes in JavaScript unless the user explicitly asks for Python.',
|
||||
);
|
||||
});
|
||||
|
||||
// The builder agent has been observed recalling a pre-ADO-5627 `.group()` signature
|
||||
// with no description argument, so the call is spelled out in the skill itself
|
||||
// rather than only linked — a wrong prior is not corrected by a pointer.
|
||||
|
|
|
|||
|
|
@ -72,9 +72,38 @@ describe('SDK_LANGUAGE_REFERENCE rendering', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('steers runtime logic to a Code node or expression', () => {
|
||||
expect(SDK_LANGUAGE_REFERENCE).toContain('Code node');
|
||||
expect(SDK_LANGUAGE_REFERENCE).toContain("expr('{{ ... }}')");
|
||||
it('steers runtime logic to a native node first and reserves the Code node', () => {
|
||||
const section = SDK_LANGUAGE_REFERENCE.slice(
|
||||
SDK_LANGUAGE_REFERENCE.indexOf('## Where to put runtime logic'),
|
||||
);
|
||||
for (const node of [
|
||||
'Edit Fields (Set)',
|
||||
'Filter',
|
||||
'IF',
|
||||
'Switch',
|
||||
'Sort',
|
||||
'Remove Duplicates',
|
||||
'Aggregate',
|
||||
'Summarize',
|
||||
'Split Out',
|
||||
'Limit',
|
||||
'Merge',
|
||||
]) {
|
||||
expect(section).toContain(`**${node}**`);
|
||||
}
|
||||
expect(section.indexOf('Edit Fields (Set)')).toBeLessThan(section.indexOf('Code node'));
|
||||
expect(section).toContain("expr('{{ ... }}')");
|
||||
expect(section).toContain("{{ [$json.title, $json.category].filter(Boolean).join(' ') }}");
|
||||
expect(section).toContain('Use a **Code node** only for');
|
||||
expect(section).toContain('$getWorkflowStaticData');
|
||||
expect(section).toContain('three or more native nodes');
|
||||
});
|
||||
|
||||
it('scopes the method restriction to builder code, not expressions', () => {
|
||||
expect(SDK_LANGUAGE_REFERENCE).toContain('are NOT available in builder code');
|
||||
expect(SDK_LANGUAGE_REFERENCE).toContain(
|
||||
'n8n expressions (`{{ ... }}`) run full JavaScript at runtime',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ function renderInlineConstraintLines(): string {
|
|||
const SAFE_METHODS_SENTENCE =
|
||||
`The only non-builder methods available are ${SAFE_JSON_METHOD_NAMES.map((n) => `\`JSON.${n}\``).join(', ')} ` +
|
||||
`and the string methods ${SAFE_STRING_METHOD_NAMES.map((n) => `\`.${n}()\``).join(', ')}. ` +
|
||||
'Native array/string methods such as `.join()`, `.map()`, `.filter()`, `.reduce()`, and `.split()` are NOT available.';
|
||||
'Native array/string methods such as `.join()`, `.map()`, `.filter()`, `.reduce()`, and `.split()` are NOT available in builder code. ' +
|
||||
'This restriction applies to builder code only: n8n expressions (`{{ ... }}`) run full JavaScript at runtime, so all of these methods work inside an expression.';
|
||||
|
||||
/**
|
||||
* Node-groups documentation, shared by Instance AI and the MCP `get_sdk_reference` tool.
|
||||
|
|
@ -188,13 +189,26 @@ ${renderBlockedGlobalsLines()}
|
|||
|
||||
## Where to put runtime logic
|
||||
|
||||
Builder code only describes the graph. For anything that needs to run at
|
||||
runtime (joining/aggregating values, transforming items, parsing, date math,
|
||||
regex), do it in one of these:
|
||||
Builder code only describes the graph. Anything that runs at runtime (shaping
|
||||
items, filtering, routing, sorting, de-duplicating, aggregating, splitting,
|
||||
parsing, date math, regex) belongs in a node. Pick the native node first:
|
||||
|
||||
- Build strings with **template literals** or explicit lines.
|
||||
- Use an **n8n expression** via \`expr('{{ ... }}')\` for per-item values.
|
||||
- Use a **Code node** for multi-step aggregation or transformation.
|
||||
- Per-item shaping (rename, combine, compute, default, format fields): **Edit Fields (Set)** with expressions.
|
||||
- Filtering: **Filter**. Routing: **IF** / **Switch**. Sorting: **Sort**.
|
||||
- De-duplication: **Remove Duplicates**. Aggregation: **Aggregate** / **Summarize**.
|
||||
- Splitting an array into items: **Split Out**. Capping items: **Limit**. Joining branches: **Merge**.
|
||||
|
||||
n8n expressions run full JavaScript at runtime, so \`.join()\`, \`.filter()\`,
|
||||
\`.map()\`, \`.split()\`, ternaries and \`||\` defaults all work inside a Set field
|
||||
value, for example \`{{ [$json.title, $json.category].filter(Boolean).join(' ') }}\`.
|
||||
The builder-code method restriction above does not apply to expressions. In
|
||||
builder code, pass an expression with \`expr('{{ ... }}')\` and build static
|
||||
strings with **template literals**.
|
||||
|
||||
Use a **Code node** only for: multi-pass algorithms (loops over loops,
|
||||
recursion), \`$getWorkflowStaticData\` counters or state kept across runs,
|
||||
parsing model output that needs fence stripping, try/catch around upstream
|
||||
node access, or a step that would otherwise need three or more native nodes.
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user