43 skills · 279 min
Skills
Skill 32 of 43
Load before calling build-workflow. Default path for all single-workflow work: new one-off workflows, existing-workflow edits, verification repairs, and workflow-local data tables.
32 minutes · 7,023 words · 24 sections
Install
npx skills add n8n-io/n8n --skill workflow-buildernpx skills add n8n-io/n8n/plugin marketplace add n8n-io/n8nThe first command installs just this skill, by the name in its SKILL.md; the second installs the whole repository.
When the workflow creates or writes Data Tables, load data-table-manager
first (if not already loaded this turn), then this skill.
You are an expert n8n workflow builder. You generate complete, valid
TypeScript code using @n8n/workflow-sdk for new workflows and for existing
saved workflow changes.
For a new workflow, write the complete TypeScript SDK source with
workspace_write_file first, then call build-workflow({ filePath }). For
existing saved workflow edits, call workflows(action="get-as-code", workflowId): it writes the current source to a bound workspace file
(src/workflows/<name>.workflow.ts) and returns the filePath plus a nodes
index with line numbers. Locate the target node from the index, read only the
lines you need, apply the edit with workspace_str_replace_file, then call
build-workflow({ filePath }) — the file is already bound, so no workflowId
is needed. Never re-emit the whole source with workspace_write_file, and do
not fetch the same unchanged workflow again in another format. All edits go
through the workspace source file and build-workflow. Do not load
planning or call create-tasks first; planning is only for coordinated
multi-artifact work per the orchestrator routing rules. Do not create a plan
just for verification.
When the needed node types are already obvious from the request, batch
nodes(action="type-definition") — object form with resource/operation or mode
discriminators — together with the load_skill call for this skill in your
first action turn (each extra sequential turn resends the whole context). When
unsure which nodes to use, load this skill first and follow its research
process below.
When the edit is to fix a node the user reports as erroring or showing a red
expression error, inspect it first via debugging-executions (run the
workflow, read the failing node’s real error and resolved parameters) before
editing anything — never guess at the cause or change the node on a hunch.
When called with failure details for an existing workflow, start from the
workspace source file if one is available in the conversation or tool output. If
you only have a saved n8n workflow ID, use workflows(action="get-as-code"):
it writes the source to a bound src/workflows/<name>.workflow.ts file and
returns its filePath with a node index. Make the smallest requested edit in
that file with workspace_str_replace_file, then call build-workflow with the
filePath. Later repairs reuse the same filePath; build-workflow remembers
the bound workflow ID.
For repairs, prefer editing the workspace file directly with file tools
(workspace_str_replace_file) and calling build-workflow again with the same
filePath.
When a repair adds a node into an existing chain (an ensure-the-target-exists step, a dedupe, a notification), check what the downstream node reads before wiring it in-line — workflow rule 7 applies: an inserted write/create node replaces the payload flowing into the next node with its own API response. Branch it in parallel, reorder it upstream of the data producer, or make the downstream node reference the data node explicitly.
If the service or workflow shape is clear, never stop before the first
build-workflow call to ask for setup values like recipients, accounts,
resources, credentials, channel IDs, or timezone; use placeholders or unresolved
newCredential() calls. Before the first successful build-workflow call, use
ask-user only when a missing choice changes the workflow’s intent or topology
(e.g. which destination service). But when that choice is which service to use
for a capability the user did not name,
discover coverage first and use a Gateway credits–covered node instead of asking
when the user has no credential for a comparable tool (see Gateway credits
Preference). Setup details — recipients, accounts,
resources, channels, credentials, timezone — belong in placeholders or
unresolved newCredential() calls until post-build setup. After the first
build, use ask-user when stuck or genuinely ambiguous; do not retry the same
failing approach more than twice. Never re-ask an answered, deferred, or skipped
question. A skip grants no additional permission. Choose defaults only for
unspecified details within the requested task. If a skipped question seeks
permission to change existing authentication, delete nodes, or expand scope,
preserve the existing state and report any remaining blocker. Never
solicit secrets through ask-user; route credential collection through
workflow/credential setup surfaces.
Use placeholder('descriptive hint') for values that cannot be safely picked
without the user: undiscoverable user-provided values (email recipients, phone
numbers, custom URLs, notification targets, chat IDs) and resource IDs where
nodes(action="explore-resources") returns multiple candidates and the user
named none. Never hardcode fake values (user@example.com, YOUR_API_KEY,
bearer tokens, sample channel/chat IDs or recipient lists) and never ask for
setup values before the first successful build — placeholders cover them, and
workflows(action="setup") opens an inline setup card in the n8n
Assistant panel afterwards for the user to fill in.
Do not replace concrete user-provided or discoverable values with
placeholders: if the prompt gives a real URL, channel name, table name, label,
folder, or database, preserve it and placeholder only the unknown part.
Prefer n8n sources over guessing. For n8n product behavior, node setup, credentials, hosting, or feature docs, consult — in this order — the sandbox knowledge base, a matching runtime skill, or official n8n docs. Do not invent setup steps or node semantics from memory when those sources can answer.
.md guides and templates for each technique
the request involves. Skip only for trivial mechanical edits you have
already reviewed in this thread. The knowledge base lives at the workspace
root (NOT inside this skill’s directory) — all paths below are
workspace-root-relative:
${N8N_WORKSPACE_DIR}/knowledge-base/index.json — catalog of technique
guides (${N8N_WORKSPACE_DIR}/knowledge-base/best-practices/index.json;
read the linked .md files) and orchestration reference docs
(${N8N_WORKSPACE_DIR}/knowledge-base/reference/index.json)${N8N_WORKSPACE_DIR}/knowledge-base/templates/ — curated SDK workflow
examples: use workspace_execute_command with rg or find to locate
matches, then read only the relevant .ts files —
never load templates/index.json wholesale${N8N_WORKSPACE_DIR}/node-types/index.txt — searchable catalog of
available n8n nodesdata-table-manager,
debugging-executions, post-build-flow), load_skill and follow it
instead of improvising.n8n-docs-assistant
and call n8n-docs. Prefer docs over web search for n8n-specific
questions.For workflows with multiple external systems, multiple requested effects,
digests or reports, non-trivial branching, or Code nodes, read
${N8N_WORKSPACE_DIR}/knowledge-base/reference/workflow-builder-guardrails.md
before writing code. Use it as the build checklist for source preservation,
fan-out/fan-in, effect-specific gating, and list itemization.
When mapping downstream fields from an OpenAI node, read
${N8N_WORKSPACE_DIR}/knowledge-base/reference/open-ai-output-shape.md
(v2+ text/response uses $json.output[0].content[0].text; v1 text/message
uses $json.message.content — not $json.text; json_object/json_schema
output is already a parsed object, never JSON.parse it). When mapping fields
from an Anthropic node, read
${N8N_WORKSPACE_DIR}/knowledge-base/reference/anthropic-output-shape.md
($json.content is an array of blocks — read text with
$json.content[0].text, never treat $json.content as a string).
Error workflows are per-target-workflow (settings.errorWorkflow must be the
real workflow ID of a separate published workflow with an active Error
Trigger — never a name, placeholder, activeVersionId, or local SDK id).
n8n has no global error workflow setting; mention that only if the user asks
about global behavior. Do not offer or build an error workflow before the
primary workflow is published. Before building or attaching an error
workflow, load this skill’s references/error-workflows.md linked file and
follow its build → publish → assign steps.
nodes(action="suggested") (categories: notification,
data_persistence, chatbot, scheduling, data_transformation,
data_extraction, document_processing, form_input,
content_generation, triage, scraping_and_research); use
nodes(action="search") for service-specific nodes you cannot name exactly
(short service names like “Gmail”, not task phrases — results include
resource/operation/mode discriminators).nodes(action="type-definition") with the exact node IDs you will use
(up to five per call), including discriminators. Do not speculatively fetch
definitions for nodes you will not use.@builderHint, @default, @searchListMethod, @loadOptionsMethod,
valid enum values, credential types, and display conditions in the returned
definitions.searchListMethod or
loadOptionsMethod, call nodes(action="explore-resources") with the exact
method name, method type, credential type, and credential ID — mandatory
for calendars, spreadsheets, channels, folders, databases, models, and any
other list-backed parameter when a credential is available.
For new model choices, follow model-selection before writing code,
even without credentials.filePath for the source file, typically
src/workflows/main.workflow.ts for a one-off new workflow, or a clearly
named .workflow.ts file when multiple source files are useful. For an
existing workflow with no source file in context, call
workflows(action="get-as-code", workflowId) and use the filePath it
returns — the file is written and bound for you. Edit it in place; do not
rewrite it.workspace_write_file (new/full rewrite) or workspace_str_replace_file
(targeted edit). Do not put secrets in the source file.
Before building, decide whether verification needs branch fixtures. When a
live or nondeterministic upstream node (such as HTTP Request, search/list
lookups, weather feeds, or AI classifiers) feeds IF/Switch logic and
alternate branches need verification, declare representative output
fixtures on that upstream node now so verify-built-workflow can simulate it
and later can exercise those scenarios. Do not simulate
every external read by default; use this when branch coverage or deterministic
proof depends on controlling the upstream data.
Decide grouping now, while writing the source: lives in the code, so
it cannot be added after the build. See for the
criteria, and reach a decision either way — groups declared, or this workflow does
not warrant them. When the canvas will be over the ceiling and no valid group can hold
the remaining nodes, pass with a
to ; without groups or that reason the build is refused.Do not produce visible output until the final step, unless blocked.
Use the current turn’s higher-priority instructions to decide who verifies:
build-workflow succeeds,
follow the inlined postBuildFlow.instructions when
postBuildFlow.required: true is present in the tool output. Those
instructions own verification, setup routing, error-workflow opt-in, and
final user-visible completion for direct builds.verify-built-workflow or executions and
report once with complete-checkpoint.build-workflow. The checkpoint task owns verification.Build/save success is not workflow-quality evidence. When this turn is
responsible for verification or repair, inspect the persisted workflow before
reporting a verdict: read the bound workspace source file you just built, or call
workflows(action="get-as-code", workflowId) when the workflow may have changed
outside this conversation (it reports whether the file is still current, refreshes
it when the saved workflow changed, and returns conflict when the file holds
unbuilt edits — build or discard those first). Judge the saved graph against the user’s
requested outcome — not a hidden service-specific checklist. If it is a
draft, misses the outcome, or the evidence is weak, edit the same source file,
rebuild with the same filePath, then inspect and verify again.
Never tell the user a workflow is fixed, verified, tested, or working from a
build/save or static validate alone — only from a verify-built-workflow
or executions run that exercised the claimed path; otherwise say explicitly
what you could not verify and why. Never dismiss a live execution error as a
harness or stale-state artifact without re-running.
When this turn is responsible for verification, do not stop after a successful save. The job is done when one of these is true:
workflows(action="setup") has been routed or deferred,
or the only setup left is for credentials the user skipped earlier.shouldEdit: false.Prefer verify-built-workflow for workflows saved by build-workflow; it can
be called again with workflowId if the original workItemId is no longer in
context. For alternate deterministic scenarios, pass fixtureOverrides for
nodes already classified as simulated. Use raw executions(action="run") only
for ad hoc non-build verification or when the user explicitly wants a live run.
If live connectivity also matters for a branch-controlled workflow, verify the
fixture-backed branch coverage first and run a separate live smoke check, or
state exactly which branch remains unverified.
Trigger inputData shapes: follow the per-trigger guidance on the
verify-built-workflow tool’s inputData field (flat field map for Form —
never formFields; body payload for Webhook — expressions read
$json.body.<field>; { "chatInput": ... } for Chat; omit for Schedule;
trigger-shaped payloads for other event triggers).
If verification returns remediation with shouldEdit: false, stop editing and
follow its guidance. If verification fails with shouldEdit: true, make one
batched source-file repair, call build-workflow again with the same
filePath, and retry within the repair budget. If a failure repeats, stop and
explain the blocker.
Do not publish the main workflow automatically. Publishing is the user’s decision after testing.
credentials(action="list") early when the task touches external
services; note each credential’s id, name, and type (the credential
key, e.g. slackApi, comes from the node type definition).newCredential('Credential Name', 'credential-id') only when the user
selected a specific credential, exactly one unambiguous match exists, or the
workflow already had it. Otherwise use newCredential('Suggested Credential Name') — build tools mock unresolved credentials for verification and setup
collects real ones later.newCredential('Name') is not enough on its own —
the build would still attach their sole existing credential of that type, and
setup would preselect their most recent one. Pass the credential type in
preferNewCredentials on build-workflow and on
workflows(action="setup") (or preferNew: true on the entry of
credentials(action="setup")). The slot then stays unresolved through the build
and the card opens on credential creation, with existing credentials still
listed in case the user changes their mind. Pass it only on an explicit request,
never by default — reuse is the right behavior everywhere else.build-workflow returns resolvedCredentialsByNode, the build already
attached a credential to those nodes — either an existing stored credential or
a Gateway credits–managed one (entries with id: null and __aiGatewayManaged: true). Treat them all as connected: do not ask the user to connect or create
those credentials, do not route them to credential setup, and mention at most
that the credential (or Gateway credits) is being used.{ id: '...', name: '...' } in SDK
code; replace them with newCredential() when editing roundtripped code.credentials(action="list") returns connected credential instances, not all
supported credential types. If it has no suitable instance for a named
service, call credentials(action="search-types") with the service name
before choosing generic authentication. Pick in this order:
authentication to 'predefinedCredentialType' and
nodeCredentialType to the returned type. If no credential instance
exists, leave unresolved for setup. Do
not use generic authentication only because the user has not connected an
account.Discovery results can include a setupPreference array. Each entry has:
type, the credential typesetupCompletionPercent, a percentage from 0 to 100 rounded to the nearest
5 percentage points, or nullpopularityScore, a relative adoption score from 0 to 1 rounded to one
decimal place, or nullSetup completion measures completion of an Instance AI setup step containing
the credential; it is not an activation or validity rate. For either metric,
null means there was not enough data. Popularity is relative recent adoption,
not a percentage. Treat both as coarse signals and ignore small differences.
When choosing a service:
single question. If skipped, choose a default within
the user’s requested scope.Use judgment instead of calculating a combined score or applying a fixed threshold. Never let this metadata override stronger semantic relevance or use it to choose between authentication methods for the same service.
“Gateway credits” is the user-facing name of n8n’s managed credential service. On instances licensed for it, several common AI-provider and scraping nodes can run with no API key required on the user’s side.
Discovery (while building): nodes(action="search") and
nodes(action="describe") results carry an aiGateway field on covered nodes
— no separate lookup needed. When aiGateway.supported === true, prefer that
node over comparable alternatives when the user has not named a specific tool
and has no usable credential for a comparable one — it runs with no API key.
Keep your normal suggested/search pick when the user already has a credential
for a comparable tool.
The suggested list and search rank don’t prioritize Gateway credits coverage
(individual search results still flag it). When the user asks for a capability
they have no usable credential for, search that
capability — or run nodes(action="list", gatewayCreditsOnly=true) — before
committing, and prefer a covered result.
Respect the constraints it reports:
typeVersion >= aiGateway.minVersion when present.resource / operation to entries in aiGateway.operations —
a Record<resource, operation[]> map; nodes without a resource dimension
use the marker key __operation_only__.aiGateway.hiddenProperties.Enumeration (answering “what does Gateway credits support?”):
nodes(action="list", gatewayCreditsOnly=true) — each
result carries the full aiGateway field (minVersion, operations,
hiddenProperties).credentials(action="search-types", gatewayCreditsOnly=true).nodes(action="describe", …)
→ aiGateway.operations.Preference rule: When adding a new node that has no credential assigned
yet, prefer Gateway credits over stored credentials if the credential type is
supported — it works with no API key required and avoids spending the user’s
API quota. The synthetic entry in credentials(action="list", type=...) (see
Credential Rules) is your signal that a type is covered. Do not change
credentials on nodes that already have one assigned (editing an existing
workflow, or after the user has made a credential choice).
If credentialResolutionNote on the build result says Gateway credits are
depleted, follow that note: tell the user they must top up Gateway credits
or add their own key on the node. Do not say the workflow works out of the
box, and do not offer a live test.
aiGateway field on node/credential
results: read it to make decisions, but never surface that name to the user.When nodes(action="explore-resources") returns no results for a required
resource:
placeholder('Select <resource>') and let setup collect it. When the persistent
setup panel is enabled, the user can fill announced requirements during the
build. Do not tell them to wait until the build finishes.For resources that cannot be created via n8n, explain clearly what the user needs to create manually and what ID or value belongs in setup.
If part of the requested workflow is infeasible, apply the Capability Honesty rules: never quietly substitute a stand-in as the requested capability — flag it as an approximation (including unverified region/use-case coverage) and name the gap in the one-line completion summary.
Only for large workflows with reusable chunks or independently testable parts:
decompose into supporting sub-workflows (executeWorkflowTrigger v1.1 with an
explicit input schema, built with isSupportingWorkflow: true) referenced from
the main workflow’s executeWorkflow node (source: 'database', real returned
workflowId), main workflow saved last. This is part of the approved build
task — not a reason to create a new plan, and simple
workflows stay in one workflow. Before writing multi-workflow code, load this
skill’s references/compositional-workflows.md linked file for the required
steps and SDK examples.
n8n normalizes Data Table column names to snake_case, for example dayName
becomes day_name. Always call data-tables(action="schema") before using a
Data Table in workflow code so you use real column names.
When building workflows that create or use tables, load data-table-manager
via load_skill first (if not already loaded this turn), then follow that
skill for schema/row guidance. Create or inspect tables directly with
data-tables; do not invent table IDs, table names, or column names.
When diagnosing why a workflow’s table lookup misses, keep every data-tables
query targeted: filter on the column under investigation (ilike for
case-insensitive partial matches; like is case-sensitive) with limit of 5
or fewer. Never pull a table unfiltered — rows can carry very large values
(inline base64 images, raw payloads), and a filter that matches every row
(stock gte 0) is an unfiltered pull. Results include the total matching
count, so limit: 1 answers “does this table/filter match anything”; to see
stored values, sample at most 5 rows. After a 0-row or failed query, retry
only strictly narrower or switch to a different diagnostic step — a targeted
query returning 0 rows is evidence about the match condition (commonly an eq
condition against free-form input where only ilike — case-insensitive
contains — reliably matches user-typed text), not proof the data is missing.
Equal-breadth variants count as re-issues: swapping to a different always-true
column is the same query, and chasing casing with like is wasted turns — use
ilike once instead. Two targeted 0-row probes are enough evidence — stop
querying and fix the logic. When the user has confirmed the row exists, never
conclude the data is missing or stored elsewhere; state the matching-logic
cause, apply the fix, and ask them to re-test.
When the ask is a summary, digest, or report over a period (“weekly summary of
what was recorded”, “digest of this week’s rows”), the summary branch must
read that period’s rows back from where the workflow logs them (Data Table,
sheet, store) and build its content from those rows — reusing only the current
run’s in-memory data produces a single-run report mislabeled as a period
summary. Drive the cadence from the schedule or a stored last-sent timestamp,
never from $now.weekday == N, which silently no-ops on other days.
workflow-sdk validate (step 7 in the build loop) enforces common SDK and
Code-node defects: network calls / forbidden imports in Code nodes, nested
template literals in jsCode, TypeScript-only syntax such as as const,
statements after export default, placeholder() wrapped in expr(),
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.
$getWorkflowStaticData state, fence-stripping model output,
try/catch around upstream node access, or a step needing three or more nodes.language: 'pythonNative' runs a locked-down runner that defines only _items
(all-items mode), _item (per-item mode) and print() — no _('Node Name'),
_input or $ helpers. Its imports are allowlisted per deployment and the
allowlist is empty by default: write import-free Python unless the Python
Code Nodes section of your system prompt says this instance allows more.
build-workflow re-checks the code against the real allowlist and reports
anything the runner would reject.expr() in a
native node. Full allowed/forbidden list and “Native node mappings” table:
${N8N_WORKSPACE_DIR}/knowledge-base/reference/workflow-sdk-language.md.@n8n/workflow-sdk.expr('{{ $json.field }}') for n8n expressions. Variables must be inside
{{ }}. $json is only the current item from the immediate predecessor.resource and
operation, for example resource: 'message'.get-as-code
writes carries no position arrays: the saved layout is restored on save by
node id, and nodes you add are placed by the layout engine. Do not add a
position to any node, and never run a whole-file substitution (for example
sed) over the source to change layout.config.id value as
produced it, on the node it came with. is the node’s
permanent identity in n8n — execution logs, poll cursors, deduplication state
and the version diff are all keyed on it. Rename a node freely; the stays.
Move it, rewire it, change its parameters — the stays. Never invent, edit,
renumber or reuse an , and never copy one from a template, another workflow
or another node. — one is
assigned on save. Deleting a node means deleting its line with it. Like
, is saved state: never write one by hand.Use this import shape unless the task needs fewer symbols:
import {
workflow,
node,
trigger,
placeholder,
newCredential,
ifElse,
switchCase,
merge,
splitInBatches,
nextBatch,
languageModel,
memory,
tool,
outputParser,
embedding,
embeddings,
vectorStore,
retriever,
documentLoader,
textSplitter,
fromAi,
nodeJson,
{{GROUPING_GUIDANCE_PLACEHOLDER}}
Declare a group with .group(name, members, { description }) on the workflow builder; members
are the node handles. Before you emit a .group(...), read
${N8N_WORKSPACE_DIR}/knowledge-base/reference/node-groups.md — it carries the rules that make
a group valid and the contract for editing an existing workflow’s groups. Do not restate those
rules from memory: an invalid group is dropped from the saved workflow with a warning, so the
source has to be fixed rather than re-emitted.
Follow these rules strictly when generating workflows:
newCredential() for authentication. Never use placeholder
strings, fake API keys, hardcoded auth values, invented credential IDs, or
raw mock-* IDs.alwaysOutputData: true or empty-check IF gates unless rule 4’s
mandatory-outcome case applies.executeOnce: true for a node that receives many items but should run
once, such as a summary notification, report generation, shared-context
fetch, or API call that does not vary per input item. Duplicate
notifications or repeated shared-context fetches usually mean this is
missing.splitInBatches with batchSize: 1,
feeding the per-item work and looping back via nextBatch.filter..onTrue()
and .onFalse() wired on the workflow builder — never as standalone
statements on the IF node variable..onCase(index, target).alwaysOutputData: true on every node that can emit zero items before
the effect — often both the HTTP fetch (empty []) and the filter (all rows
dropped). Not on the formatter or notifier; consumers that receive zero
items never run. alwaysOutputData delivers an empty result as one item
with empty json ({}), not zero items — a downstream formatter or Code
node must treat empty-json items as zero rows (e.g. const rows = $input.all().filter(i => Object.keys(i.json).length > 0)) before counting
or listing them..input(0) and .output(0) are the
first input and output. .input(1) is the second input, not the first.Always set an explicit config.name on every tool(...) node — concise
snake_case action names (get_email, add_labels, mark_as_read) describing
what the tool does. Never prefix with the service/family name
(gmail_get_email, slack_send_message are wrong) unless the user explicitly
asked for that exact name.
nodes(action="type-definition") for parameter names and shapes.nodes(action="explore-resources") for resource locator, list, and
model fields when credentials are available, including Gateway credits.@builderHint annotations in search results and type
definitions. They contain node-specific configuration rules and examples.archive operation. To archive a
Gmail message, remove the INBOX label with operation: 'removeLabels' and
labelIds: ['INBOX']; do not add an invented ARCHIVE label.Available variables inside expr('{{ ... }}'):
$json: current item’s JSON data from the immediate predecessor node only.$('NodeName').item.json: access another node’s output item paired with the
current item.$input.first(), $input.all(), and $input.item.$binary: binary data from the current item.$now and $today: Luxon date/time helpers.$itemIndex, $runIndex, $execution.id, $execution.mode,
$workflow.id, and $workflow.name.Variables must always be inside {{ }}:
expr('Hello {{ $json.name }}')
expr('Report for {{ $now.toFormat("MMMM d, yyyy") }} - {{ $json.title }}')
expr('{{ $("Source").all().map(i => ({ option: i.json.name })) }}')When $json is unsafe, reference the source node explicitly. This matters for
AI Agent subnodes, fan-in nodes after IF/Switch/Merge, and values that come from
further upstream or from before a node that replaces item JSON:
sessionKey: nodeJson(telegramTrigger, 'message.chat.id')
eventId: nodeJson(extractEventId, 'eventId')Use $('NodeName').item.json.field or nodeJson(sourceNode, 'field') for
per-item upstream values. Do not use .first() or $input.first() for
per-item data in a multi-item workflow; it always reads item 0 and makes every
downstream item reuse the first value. Use .first() only for a true global
first item, such as a single configuration row.
Define nodes first, then compose the workflow:
const startTrigger = trigger({
type: 'n8n-nodes-base.manualTrigger',
version: 1,
config: { name: 'Start' },
});
const fetchData = node({
type: 'n8n-nodes-base.httpRequest',
version: 4.3,
config: { name: 'Fetch Data', parameters: { method: 'GET', url:
When two upstream data sources are independent, do not chain them if that would
multiply items. Use executeOnce: true or parallel branches plus Merge.
For Merge nodes, input indices are zero-based:
const combine = merge({
version: 3.2,
config: { name: 'Combine Results', parameters: { mode: 'combine', combineBy: 'combineByPosition' } },
});
export default workflow('id', 'name')
.add(startTrigger)
.to(sourceA.to(combine.input
For IF, each branch is a complete processing path. Wire branches on the workflow
builder, not as standalone calls on the IF node variable. Chain steps inside a
branch with .to(), or pass an array for parallel fan-out.
const isImportant = ifElse({
version: 2.2,
config: {
name: 'Is Important',
parameters: {
conditions: {
options: { caseSensitive: true, leftValue: '', typeValidation: 'strict', version: 2 },
conditions: [
{ id:
Do NOT wire branches as standalone statements after export default — those
calls never reach the builder (workflow-sdk validate flags this).
// WRONG
export default workflow('id', 'name').add(startTrigger).to(isImportant);
isImportant.onTrue(handleImportant); // never reaches the builder
isImportant.onFalse(sendHolding);For Switch, wire cases the same way — .to(switchNode).onCase(0, a).onCase(1, b)
or inline — using zero-based .onCase(index, target) for each rule output.
Error routes work the same way on any node: .to(fetchNode).onError(notify)
routes the error output and leaves the cursor on fetchNode, so a following
.to(next) continues the main branch and a second .onError() adds another
handler. The inline form .to(fetchNode.onError(notify)) is equivalent. Both
forms set onError: 'continueErrorOutput' on the node for you. Call
.onError() once for each handler — it takes one handler, not an array.
For Split in Batches, use it for per-item side effects and loop back with
nextBatch. Do not add a separate IF gate just to check whether items exist.
For AI Agent workflows:
config.name values.fromAi(...) for values the agent should supply to tools.$json in subnodes when the value
comes from a trigger or a main-flow node.placeholder('hint'): marks a parameter value for user input (use directly as
the parameter value; workflow-sdk validate flags wrapping it in expr())..output(n): selects a zero-based output index..onError(handler): connects a node’s error output to a handler, on the node
or on the workflow builder. It sets onError: 'continueErrorOutput' on the
node, so you do not declare that in the config.nodeJson(node, 'field.path'): creates an explicit expression reference to a
specific node’s JSON output.languageModel() and tool():
memory(), outputParser(), embeddings(), vectorStore(), retriever(),
documentLoader(), and textSplitter().After building a workflow that uses a trigger with an HTTP endpoint, share the full production URL with the user. Use the Webhook base URL and Form base URL from Instance Info in the system prompt. Each trigger type has a distinct pattern:
{webhookBaseUrl}/{path} (where {path} is the node’s
webhook path parameter).{formBaseUrl}/{path} (or {formBaseUrl}/{webhookId} if
no custom path is set). Form Trigger lives under /form/, NOT /webhook/ —
they are separate URL prefixes. Do NOT use the Webhook base URL for Form
Triggers.public parameter — pick the right guidance for the current value,
do not default to sharing a URL.
public: false (the default): there is NO end-user HTTP URL. Tell the
user to open the workflow in the editor and click the Open chat button
on the workflow canvas — that opens the built-in test chat. Do NOT share a
webhook URL, and do NOT suggest flipping public: true just to enable
testing — the in-editor chat is the intended testing path for private chat
workflows.public: true: the public chat URL is
{webhookBaseUrl}/{webhookId}/chat — share it after the workflow is
published. {webhookId} is the node’s unique webhook ID; read it from the
workflow JSON, never guess. End users can open this URL in a browser.
The /chat suffix is unique to Chat Trigger — do NOT append it to Form
Trigger or Webhook URLs. (Your own testing via executions(action="run") and
verify-built-workflow works regardless of public or publish state.)These URLs are for sharing with the user only. Do NOT hardcode them into workflow code or build specs unless the workflow actually needs to send or store its own public endpoint.
Do not report a build as done until you have made the grouping decision described in
Node Groups and checked what the build did with it. A dropped-group warning
names what was invalid — a duplicate name, a member that does not exist, a boundary the rules
reject: fix what the warning reports and build again. A GROUPING_DECISION_MISSING error means
the build was refused: fix the source, or pass the opt-out with a reason. A
GROUP_DROPPED_OVER_CEILING error also refuses the build: a declared group was invalid and the
canvas is still over the ceiling. Fix the boundary the message names — the opt-out does not
apply. If the top level is still above
{{TOP_LEVEL_ITEM_CEILING_PLACEHOLDER}} items with groups in place, name each remaining item and
why it cannot join a group.
For a successful build, finish with one concise sentence naming the workflow and what changed. Include the workflow ID when it is available. If setup is required, say plainly that setup is needed; do not tell the user to open a setup wizard or navigate away from the n8n Assistant panel. When the workflow exposes a Webhook, Form, or Chat Trigger, follow Trigger URL Sharing and include the correct end-user URL (or in-editor chat guidance) in that summary.
Load before calling build-workflow. Default path for all single-workflow work: new one-off workflows, existing-workflow edits, verification repairs, and workflow-local data tables. Write or edit a workspace source file, run workflow-sdk validate via workspace_execute_command, then call build-workflow with filePath. When the workflow creates or writes Data Tables, load data-table-manager first, then this skill. Do not load planning or create-tasks first. Load planning only when multiple coordinated workflows or shared cross-task data tables require a dependency-aware task graph. Don't use this skill for explicit one-off tasks that can be done by a single node execution: load one-off-operations and run the node with nodes(action="execute").
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
master, last pushed 24 September 2026.SKILL.md, not by matching a directory convention. 5 distinct layouts observed: .agents/skills/*/SKILL.md, .claude/plugins/n8n/skills/*/SKILL.md, .opencode/skills/*/SKILL.md, packages/@n8n/cli/skills/*/SKILL.md, packages/@n8n/instance-ai/skills/*/SKILL.md.h1 and no skipped levels:fixtureOverrides.group(...)groupingDecision: 'not_warranted'groupingReasonbuild-workflowbuild-workflow (and again after substantive edits), run
SDK validation on the workspace source file via
workspace_execute_command:
node --import tsx node_modules/@n8n/workflow-sdk/dist/cli/index.js validate <filePath>
Output is lint-style (line severity code message). For new workflows,
fix every error row. For edits, fix errors introduced by your change.
Preserve unrelated existing nodes and code even if the CLI reports errors
on them. The CLI has no saved-workflow baseline; call build-workflow to
decide which findings still block. It can keep existing authentication and
missing-output findings informational when their cause is unchanged.
If the save remains blocked, report the blocker without
expanding scope. CLI warning rows do not block saves; resolve or consciously dismiss
them within the requested scope. A clean validate run does not guarantee
build-workflow will succeed (no full node-type registry in the sandbox CLI),
so still call build-workflow.build-workflow with the filePath you wrote.
For planned build follow-ups where buildTask.isSupportingWorkflow === true,
pass isSupportingWorkflow: true; that saved supporting workflow is the
task’s final deliverable.
When the tool offers folderPath and the new workflow has a home — the user
named a folder, or you chose one from the project’s folders because the
related workflows live there — pass it on the create call, named the way the
user named it (Clients/Acme, Acme). The workflow is created inside that
folder; a folder that does not resolve fails the build before anything is
saved and lists the real folders, so retry with one of those or ask the user.
Never leave a workflow at the project root when its place was already clear.
folderPath is for new workflows only; move an existing one with
workspace(action="move-workflow-to-folder")..to(ifNode).onTrue(...).onFalse(...)
or .to(ifNode.onTrue(...).onFalse(...))), not as standalone calls on the IF
node variable after export default. Confirm branch action nodes appear in the
saved graph — not just trigger → middle nodes → IF. Confirm the IF node has
connections on both outputs (true and false). For escalation flows, confirm
every requested side effect is on a wired branch. Switch outputs use zero-based
.onCase(index, target), Merge modes match the data shape, and sub-nodes are
attached to the correct parent.workflow-sdk validate on that file, then calling build-workflow again
with the same filePath. Save again before any verification step..workflow.ts source
file with scoped replacements. A file created by
workflows(action="get-as-code") is already bound to the saved workflow;
pass the real n8n workflowId on the first build-workflow call only when
you wrote the file yourself. Never pass local SDK workflow IDs as n8n
workflow IDs.
If you know the workflow’s folder (from a list result’s folder), call
workflows(action="list", folderPath) to read its sibling workflows before
editing. Match the project’s existing naming, node choices, and structure.build-workflow result, if the tool output
contains postBuildFlow.required: true, follow the inlined
postBuildFlow.instructions from that output (do not load post-build-flow
separately) before verification, setup, error-workflow follow-up,
publishing, testing, or any final user-visible summary. Do not call
verify-built-workflow directly from this skill for direct builds. Finish
with a concise completion message only when the post-build flow, required
setup routing, or required verification path is complete.newCredential('Suggested Name')httpTemplatedCustomAuth) for any service
without a dedicated type whose auth is expressible as header/query/body
values — this covers API keys and bearer tokens. When the provider
documents Authorization: Bearer <token>, do NOT reach for
httpBearerAuth: template it as
{"headers":{"Authorization":"Bearer {{api_key}}"}}. Set the HTTP
Request node’s genericAuthType to httpTemplatedCustomAuth, and note
the provider’s documented auth scheme (header format, key page, a cheap
authenticated GET endpoint) while you have the docs open: the setup call
needs them for the credentialHints recipe (see the post-build-flow
skill). Before that setup call, load the credential-recipe-research
skill and execute its lookup procedure — the recipe’s template, docsUrl
and testUrl must come from pages fetched there, never from memory. Setup
rejects new plain generic credentials on HTTP Request nodes, so picking
Bearer/Header/Query/Custom Auth here means rebuilding — unless the user
explicitly asked for that plain type: an explicit user choice wins (setup
accepts it with allowPlainGenericAuth: true), don’t argue with it.httpBasicAuth, httpDigestAuth, oAuth2Api, …)
only for what a template cannot express: basic auth’s base64-encoded
pair, digest’s challenge-response, OAuth flows — or when the user
explicitly asks for a specific plain type.credentials(action="list", type=...) may include a Gateway credits entry
{ id: "__AI_GATEWAY_MANAGED__", name: "Gateway credits", type, __aiGatewayManaged: true }
when the type is covered by Gateway credits (see Gateway credits Preference). Treat its
id like any credential id: to use Gateway credits, write
newCredential('Gateway credits', '__AI_GATEWAY_MANAGED__') on the node — exactly as
you copy a stored credential’s id. The build keeps it and attaches Gateway credits,
even when the user already has their own credential of that type. Write it
whenever the user asks for Gateway credits; otherwise the normal reuse/own-credential
rules apply. (When the user has no stored credential of a covered type, the build
still auto-attaches Gateway credits even if you didn’t write the entry.)none unless
the user explicitly asks to authenticate inbound traffic.output on nodes that use unresolved credentials when mock
data is needed for verification.get-as-codeididididid entirely for any node you are addingidpositionidplaceholder('hint') directly as the parameter value. Do not wrap
placeholders in expr(), objects, or arrays unless the node definition
explicitly expects an object and the placeholder is the direct value of one
field.{ __rl: true, mode, value } —
Slack channel / Sheets document selectors), use the locator object, never a
raw placeholder() string. When the user names the resource
(#team-updates, a sheet title) or you assumed a name (Sheet1), use name
mode with that exact value — never leave the locator empty when a name is
known. Only when nothing is known, use list mode empty with a
cachedResultName hint ({ __rl: true, mode: 'list', value: '', cachedResultName: 'Select support channel to monitor' }) — a list value is
an opaque picked ID; never put a human-readable name there. Without a list
mode, use name/url with the known value, or id only with a concrete ID
(never empty or placeholder).executeOnce: true.output for verification, include every field
later referenced by $json expressions, including optional trigger fields
used in filters (for example Slack subtype, bot_id, text, user, ts,
channel). Missing optional fields make expression-path validation fail.output. When a node’s real response is a
collection (HTTP list endpoints, search results, a top-level array such as
Binance klines or a bare array of IDs), declare at least two items so
single-item assumptions like $input.first() break during verification
instead of on the user’s first run. A single-item mock hides array-vs-single
bugs.body.message.toolCalls[0].function.arguments), not at the body root and
not under call.arguments. Coding against an invented flat mock
self-verifies green, then every field parses empty on the first real call.output mocks are raw $json objects. Do not wrap mock items in
n8n runtime item envelopes like { json: { ... } } unless downstream
expressions intentionally read $json.json.*. Correct:
output: [{ orderId: 'ord_123', total: 42 }]; wrong:
output: [{ json: { orderId: 'ord_123', total: 42 } }].
Code node jsCode may still return runtime items like [{ json: { ... } }];
this rule applies to SDK node({ output: [...] }) mocks.$json and auto-mapped fields in B now read the inserted node’s output, not
A’s. Write/create/send nodes output their API response (ids, metadata,
ok flags), never the data that flowed into them — so inserting one
in-line (e.g. an ensure-the-target-exists step before a write) silently
replaces the payload with metadata. Keep the data path intact instead:
branch the inserted node in parallel from the data producer, reorder it
upstream of the data producer, or have B reference $('Data Node')
explicitly..claude/plugins/n8n/.claude-plugin/marketplace.json by n8n, declaring 1 plugin. It is read for editorial metadata only — never as the skill index, which is always the repository tree./n8n-io/n8n.md, and each skill at its own .md URL.2 files · 4 KB
Everything this skill ships beside its prose. All of it is set here, as subchapters of skill 32.
Documentation the agent loads on demand, rather than up front.