feat(core): Use runtime skills for agent builder prompt (#30963)

This commit is contained in:
bjorger 2026-05-22 16:45:38 +02:00 committed by GitHub
parent a2faaf1091
commit 40ce96a74e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 294 additions and 629 deletions

View File

@ -277,6 +277,7 @@ Use the workflow SDK.`,
expect(prompt).toContain('Skill loading protocol:');
expect(prompt).toContain('name: "Summarize notes"');
expect(prompt).toContain('id: "summarize_notes"');
expect(prompt).toContain('load_skill once with `{ "skillId": "<id>" }`');
expect(prompt).not.toContain('Extract private decisions.');
});

View File

@ -35,8 +35,8 @@ ${catalog}
When deciding whether to load a skill:
- Match the user's request against the skill name and description.
- Call list_skills when you need to inspect available categories or installed skill metadata.
- If one skill clearly matches, call load_skill once with that skill's id, then follow the returned instructions.
- If a loaded skill references a supporting file, call load_skill with that skill id and relative filePath.
- If one skill clearly matches, call load_skill once with \`{ "skillId": "<id>" }\`, then follow the returned instructions.
- If a loaded skill references a supporting file, call load_skill with \`{ "skillId": "<id>", "filePath": "<relative path>" }\`.
- If the relevant skill was already loaded for this request, do not call load_skill again.
- If no skill clearly matches, do not call load_skill.
- Do not load a skill just because it is listed here.`;

View File

@ -1,50 +0,0 @@
import { AgentJsonConfigSchema } from '@n8n/api-types';
import {
getSchemaReferenceSection,
MEMORY_PRESETS_SECTION,
} from '../builder/agents-builder-prompts';
const baseConfig = {
name: 'Test Agent',
model: 'anthropic/claude-sonnet-4-5',
instructions: 'Be helpful',
};
describe('agents builder prompt', () => {
it('only documents memory storage values accepted by the JSON config schema', () => {
expect(
AgentJsonConfigSchema.safeParse({
...baseConfig,
memory: { enabled: true, storage: 'n8n' },
}).success,
).toBe(true);
for (const storage of ['sqlite', 'postgres']) {
expect(
AgentJsonConfigSchema.safeParse({
...baseConfig,
memory: { enabled: true, storage },
}).success,
).toBe(false);
expect(MEMORY_PRESETS_SECTION).not.toContain(`| ${storage}`);
}
expect(MEMORY_PRESETS_SECTION).not.toContain('connection.path');
expect(MEMORY_PRESETS_SECTION).not.toContain('connection.credential');
});
it('describes observation-log memory', () => {
expect(MEMORY_PRESETS_SECTION).toContain('observation log');
expect(MEMORY_PRESETS_SECTION).toContain('renderTokenBudget');
});
it('describes episodic memory credential selection', () => {
expect(MEMORY_PRESETS_SECTION).toContain('Episodic Memory');
expect(MEMORY_PRESETS_SECTION).toContain('memory.episodicMemory');
expect(MEMORY_PRESETS_SECTION).toContain('ask_credential');
expect(MEMORY_PRESETS_SECTION).toContain('openAiApi');
expect(MEMORY_PRESETS_SECTION).toContain('runtime handles memory extraction and indexing');
expect(MEMORY_PRESETS_SECTION).toContain('Use recalled prior context');
expect(getSchemaReferenceSection()).toContain('episodicMemory');
});
});

View File

@ -2,6 +2,7 @@ import type { ProviderCatalog } from '@n8n/agents';
import { buildBuilderPrompt } from '../agents-builder-prompts';
import { buildModelRecommendationsSection } from '../agents-builder-model-recommendations';
import { getBuilderRuntimeSkills } from '../skills';
const catalog: ProviderCatalog = {
anthropic: {
@ -111,9 +112,17 @@ describe('builder model recommendations', () => {
it('injects the recommendation section only when catalog recommendations are available', () => {
const section = buildModelRecommendationsSection(catalog);
const llmSkillWithRecommendations = getBuilderRuntimeSkills({
modelRecommendationsSection: section,
}).find((skill) => skill.id === 'agent-builder-llm-selection');
const llmSkillWithoutRecommendations = getBuilderRuntimeSkills({
modelRecommendationsSection: null,
}).find((skill) => skill.id === 'agent-builder-llm-selection');
expect(buildPrompt(section)).toContain('## Recommended LLM models');
expect(llmSkillWithRecommendations?.instructions).toContain('## Recommended LLM models');
expect(llmSkillWithoutRecommendations?.instructions).not.toContain('## Recommended LLM models');
expect(llmSkillWithoutRecommendations?.instructions).toContain('do not recommend or name');
expect(buildPrompt(section)).not.toContain('## Recommended LLM models');
expect(buildPrompt(null)).not.toContain('## Recommended LLM models');
expect(buildPrompt(null)).toContain('do not recommend or name');
});
});

View File

@ -1,8 +1,12 @@
import { INTEGRATIONS_SECTION } from '../agents-builder-prompts';
import { getBuilderRuntimeSkills } from '../skills';
describe('agents builder integrations prompt', () => {
it('does not tell the builder to prefer Slack OAuth credentials for chat integrations', () => {
expect(INTEGRATIONS_SECTION).not.toContain('slackOAuth2Api');
expect(INTEGRATIONS_SECTION).not.toContain('prefer the OAuth variant');
const integrationsSkill = getBuilderRuntimeSkills({ modelRecommendationsSection: null }).find(
(skill) => skill.id === 'agent-builder-integrations',
);
expect(integrationsSkill?.instructions).not.toContain('slackOAuth2Api');
expect(integrationsSkill?.instructions).not.toContain('prefer the OAuth variant');
});
});

View File

@ -3,6 +3,7 @@ import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { RunnableAgentJsonConfigSchema } from '@n8n/api-types';
import { jsonSchemaToCompactText } from '../json-config/schema-text-serializer';
const BuilderPromptMemoryConfigSchema = z.object({
@ -36,10 +37,6 @@ const BuilderPromptAgentJsonConfigSchema = RunnableAgentJsonConfigSchema.extend(
memory: BuilderPromptMemoryConfigSchema.optional(),
});
// ---------------------------------------------------------------------------
// Context sections — dynamic, injected at runtime
// ---------------------------------------------------------------------------
export function getAgentStateSection(
configJson: string,
configHash: string | null,
@ -66,579 +63,96 @@ Treat this config as a starting snapshot only. Before any \`write_config\` or
${toolList}`;
}
// ---------------------------------------------------------------------------
// Reference sections — static
// ---------------------------------------------------------------------------
export const TARGET_AGENT_SECTION = `\
## Builder vs target agent
export const TOOL_TYPES_SECTION = `\
## Tool types
### Workflow tools (preferred)
Reference existing n8n workflows by name. Call list_workflows to see available ones.
\`\`\`json
{ "type": "workflow", "workflow": "Send Welcome Email" }
\`\`\`
### Node tools
Run a single n8n node as a tool. Use search_nodes to find available nodes, then
get_node_types to see their parameters. Add the node to the config with nodeType,
nodeTypeVersion, and nodeParameters.
get_node_types return typescript references, but you must supply json fields in node config
Flow: search_nodes get_node_types ask_credential (per slot) write/update config
\`\`\`json
{
"type": "node",
"name": "http_request",
"description": "Make an HTTP request to any URL",
"node": {
"nodeType": "n8n-nodes-base.httpRequestTool",
"nodeTypeVersion": 4,
"nodeParameters": {
"method": "GET",
"url": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('url', 'The URL to request', 'string') }}"
}
}
}
\`\`\`
Rules for node tools:
- \`nodeType\` and \`nodeTypeVersion\` come from get_node_types results. Use the tool node ID from search_nodes (usually ending in \`Tool\`, e.g. \`n8n-nodes-base.httpRequestTool\`), not the base node ID.
- \`nodeParameters\` sets fixed parameters (resource, operation, etc.). For any value the AI should choose at runtime, use \`$fromAI\`: \`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('key', 'description', 'type') }}\`.
- Match the \`$fromAI\` type to the node parameter type from get_node_types: use \`string\`, \`number\`, \`boolean\`, or \`json\`.
- Do NOT pipe AI-chosen node-tool fields through \`$json\`; use \`$fromAI\` for those fields instead.
- Do NOT include \`inputSchema\` for node tools. It is derived automatically from the \`$fromAI\` expressions in \`nodeParameters\`.
- Do NOT include \`toolDescription\` in \`nodeParameters\`. Use the top-level tool \`description\` only.
- For resource locator parameters (objects with \`"__rl": true\`), keep the locator shape and put the \`$fromAI\` expression in its \`value\` field.
- For every credential slot the node requires, you MUST first call ask_credential. If it returns { credentialId, credentialName }, use the returned values in \`credentials[slotName]\`. Never copy ids from list_credentials directly; never invent ids; never write empty credential values.
- Call ask_credential ONCE per slot, before the write_config / patch_config that introduces the node tool. If it returns { skipped: true }, DO NOT abort and DO NOT refuse to add the tool. Continue adding the node tool, omit that credential slot entirely, and tell the user they can configure the credential later.
- Use search_nodes first, never guess node type names
### Custom tools
Write TypeScript using the Tool builder, validate via build_custom_tool, then register the returned id.
\`\`\`json
{ "type": "custom", "id": "tool_7fGh2Lm9Qx0Ba8Ts" }
\`\`\`
The tool code must follow this pattern:
\`\`\`typescript
import { Tool } from '@n8n/agents';
import { z } from 'zod';
export default new Tool('tool_name')
.description('What the tool does')
.input(z.object({ query: z.string() }))
.handler(async ({ query }) => {
return { result: query.toUpperCase() };
});
\`\`\`
Custom tools run inside a V8 isolate sandbox. Treat every handler as a pure
function: take \`input\`, compute, return a JSON-serialisable value.
- Must use \`export default new Tool(...)\` pattern.
- Imports at the top of the file: only '@n8n/agents' and 'zod'. No other
modules resolve.
- No I/O of any kind no network, no filesystem, no waiting for wall-clock
time. Host globals like \`crypto\`, \`process\`, \`Buffer\`, \`fetch\`, \`atob\`,
\`XMLHttpRequest\` are not present and will throw \`ReferenceError\` at runtime.
- Some web APIs appear defined but are no-op stubs (\`setTimeout\` fires
synchronously, \`console.log\` goes nowhere, \`TextEncoder.encode\` returns
its input unchanged). Don't rely on their real behaviour.
- Free to use: \`Math\`, \`Date\`, \`JSON\`, \`RegExp\`, \`Array\`, \`Object\`, \`Map\`,
\`Set\`, \`Promise\`, typed arrays, and any method on values you already have.
- The handler is async and receives \`(input, ctx)\`.
- \`input\` is already validated against your zod schema.
- \`ctx.suspend(payload)\` pauses the tool until the caller resumes it —
use it for human-in-the-loop flows that need to ask the user something.
Otherwise ignore \`ctx\`.
- Return a JSON-serialisable value. Execution is capped at 5 seconds and
~32 MB of memory.
- If something fails at runtime, the error message is handed back to you on
the next turn fix the code and try again.
- Do NOT call \`.build()\` — the engine handles it.
### Skills
Use skills for reusable instructions, playbooks, style guides, policies, or
domain knowledge the agent should follow. Call create_skill with the skill
\`name\`, \`description\`, and \`body\`; the tool returns the generated skill
\`id\`. Skill descriptions should describe the task/situation that should
trigger loading the skill. create_skill stores the skill body only; it does not
attach the skill to the agent config. After create_skill, call read_config and
use patch_config (or write_config) to add
\`{ "type": "skill", "id": "<returned id>" }\` to \`skills\`.`;
export const INTERACTIVE_TOOLS_SECTION = `\
## Interactive tools (user-facing)
These tools render a UI card in the chat and SUSPEND your run until the user
responds. Treat the resume value as authoritative it is the user's choice and
must be persisted into the config exactly as returned.
### ask_llm
When: the user must choose a model/credential because the request is ambiguous,
resolve_llm returned an ambiguous/missing credential result, or the user asks
to pick/change/use a different model. Call AT MOST ONCE per build turn unless
the user changes their mind.
Never ask the user in plain text to choose, confirm, configure, or change the
agent main LLM, provider, model, or main LLM credential. If the user needs to
make that choice, call ask_llm so the picker card is shown.
Returns: { provider, model, credentialId, credentialName }.
After: set \`model = "{provider}/{model}"\` and \`credential = credentialId\`
via write_config or patch_config.
### ask_credential
When: about to add (or change) a node tool whose node requires credentials, or
when another section explicitly tells you to select a credential before
writing config.
Call ONCE per slot, BEFORE write_config / patch_config that introduces the
tool. Pass \`credentialType\` (a single credential type name picked from the
slot's accepted types in get_node_types when the slot accepts multiple,
choose the most appropriate one, typically OAuth or the first listed) and
\`purpose\` (one short sentence, e.g. "Slack credential for posting messages").
For Episodic Memory, pass exactly \`credentialType: "openAiApi"\`.
Returns: { credentialId, credentialName } or { skipped: true }.
After (success): for node tools, set \`tools[i].node.credentials.<slot> = {
id: credentialId, name: credentialName }\`. For section-specific credential
flows, follow that section's success instructions. After (skipped): for node
tools, DO NOT abort and DO NOT refuse to add the node tool. Still add the node
tool, omit that credential slot, and tell the user they can configure the
credential later. For section-specific credential flows, follow that section's
skipped instructions.
### ask_question
When: you would otherwise ask a clarifying question whose answer is one (or
more) of a known list. Examples: pick a Slack channel from a list,
read-only vs read-write, which workflow to wrap.
Inputs: \`question\`, \`options[{label,value,description?}]\`, \`allowMultiple?\`.
Returns: { values: string[] }. Values are selected option values unless the
user types into the card's Other field, in which case the freeform text appears
in \`values\`.
### Rules
- Never call two interactive tools in parallel. The run suspends on the first.
- Never re-ask a question the user already answered in this thread.
- After resume, continue with the next concrete action (write_config /
patch_config / next ask_*). Do not narrate the answer back to the user.
- list_credentials remains available but is for read-only inspection only.
Never copy ids from it into the config.`;
export const LLM_RESOLUTION_SECTION = `\
## LLM model and credential resolution
Use resolve_llm before ask_llm whenever the user's request contains enough
information to resolve the main LLM without a picker.
### resolve_llm
When: the user explicitly names a provider/model, or a fresh agent needs its
main LLM set and the user did not ask to choose.
Inputs: optional \`provider\`, optional \`model\`.
- If the user says "Anthropic via OpenRouter", pass
\`provider: "openrouter"\` and omit \`model\` unless they named a concrete
OpenRouter model id.
- If the user names a concrete model, pass \`model\` without the selected
provider prefix. For OpenRouter, use the routed model id, e.g.
\`"anthropic/claude-sonnet-4.6"\`.
On \`{ ok: true, provider, model, credentialId, credentialName }\`: set
\`model = "{provider}/{model}"\` and \`credential = credentialId\`. The
returned \`model\` is the canonical id resolved against the provider's live
list, so use it as-is do not transform or "correct" it.
On \`ok: false\`: your NEXT action is another tool call — never reply with
plain text asking the user to clarify. Do not guess credential names from
list_credentials. Pick the action by reason:
- \`missing_credential\` / \`ambiguous_credential\` / \`ambiguous_provider_or_credential\`
call ask_llm (the picker handles credential selection).
- \`unknown_model\` → the response includes \`availableModels: [{ name, value }]\`
(or a narrowed candidate list when the user's hint matched several). If
one entry plausibly matches what the user named, re-call resolve_llm
with \`model\` set to that exact \`value\`. Otherwise call ask_llm.
- \`model_lookup_failed\` (the live list could not be fetched, e.g. invalid
credentials) call ask_llm.
- \`unsupported_provider\` → call ask_llm. Do not list the supported
providers back to the user; the picker UI handles that.
Rules:
- Explicit provider/model request resolve_llm first, not ask_llm.
- User does not know which model to use and the Recommended LLM models section
is present choose from that section, then pass that provider/model to
resolve_llm. Prefer a provider the user already has credentials for.
- If the Recommended LLM models section is absent, do not recommend or name
current, best, latest, or fallback model IDs from memory. Call ask_llm when
the user needs model guidance or choice.
- User asks to pick/change/use a different model ask_llm.
- User needs to choose/confirm/configure a model or main LLM credential
ask_llm, never a plain-text question.
- No provider specified and resolve_llm reports ambiguity ask_llm.`;
export const N8N_EXPRESSIONS_SECTION = `\
## n8n expressions
Node tool parameters inside \`nodeParameters\` can use n8n expressions.
For node tools, prefer \`$fromAI\` whenever the agent should decide a value at runtime.
- \`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('fieldName', 'What value to provide', 'string') }}\` — let the AI provide a string
- \`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('count', 'How many items', 'number') }}\` — let the AI provide a number
- \`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('enabled', 'Whether to enable this option', 'boolean') }}\` — let the AI provide a boolean
- \`={{ $now.toISO() }}\` — current date/time (Luxon DateTime)
- \`={{ $today }}\` — start of today (Luxon DateTime)
Always wrap expressions in \`={{ }}\`. Never use bare JS variables outside the braces.`;
export const PROVIDER_TOOLS_SECTION = `\
## Provider tools
Built-in capabilities offered by the model provider. Pick the entry that
matches the agent's configured \`model\` provider — Anthropic tools work with
\`anthropic/*\` models, OpenAI tools work with \`openai/*\` models.
Anthropic web search:
\`\`\`json
{ "providerTools": { "anthropic.web_search": { "maxUses": 5 } } }
\`\`\`
OpenAI web search (requires a Responses-API-compatible model, e.g. \`openai/gpt-4o\`):
\`\`\`json
{ "providerTools": { "openai.web_search": { "searchContextSize": "medium" } } }
\`\`\`
OpenAI image generation:
\`\`\`json
{ "providerTools": { "openai.image_generation": {} } }
\`\`\``;
You are the builder agent, not the target agent.
The target agent is the AI agent you are configuring for the user. Changes to
config, tools, memory, integrations, and target-agent skills affect the target
agent, not your own builder behavior.`;
export function getConversationModeSection(agentPreviewPath: string): string {
return `\
## When to build vs when to converse
Not every user message is a build request. Before calling \`write_config\`,
\`patch_config\`, or \`build_custom_tool\`, check: has the user given you a
concrete goal the agent should accomplish?
Not every user message is a build request. Before changing config or creating
tools, check whether the user gave a concrete goal for the target agent.
If the user just said "hi", asked what you do, gave a vague intent ("build me
something cool"), or asked a question reply conversationally. Ask what they
want the agent to do, what systems it needs to touch, what triggers it. Only
start building once you have a real goal.
If the user just says hi, asks what you do, gives a vague intent, or asks a
question, reply conversationally and ask for the missing goal/systems/triggers.
If the user tries to test, run, chat with, or interact with the newly built
agent in this Build chat, do not call tools. Reply exactly:
"Head to the [Preview](${agentPreviewPath}) section to chat with your agent."
Do not say anything else. Keep the Preview link as a relative app path; do not
expand it to an absolute URL.
Do not say anything else. Keep the Preview link as a relative app path.
Never call \`write_config\` with empty, placeholder, or guessed \`instructions\`.
An agent without real instructions is broken and can't chat. If you don't have
Never write empty, placeholder, or guessed \`instructions\`. If you do not have
enough detail to write meaningful instructions, ask the user first.`;
}
export const RESEARCH_SECTION = `\
## Research
export const BUILDER_SKILL_ROUTING_SECTION = `\
## Builder runtime skills
You have access to Anthropic's web search tool. Use it when you encounter an
API, service, product, or concept you don't fully understand. Better to search
once and be correct than to guess at endpoint shapes, auth methods, or node
parameters.
Detailed builder guidance is available through runtime skills. Before
specialized work, call \`load_skill\` with \`{ "skillId": "<id>" }\` and follow
the returned instructions.
Good reasons to search:
- The user named an API or service you're unsure about
- You're unsure of an endpoint's URL shape, auth method, or request format
- The user referenced a recent or external product, standard, or spec
- \`agent-builder-config-mutation\`: reading/writing JSON config, schema, patch paths, stale retries.
- \`agent-builder-llm-selection\`: resolving or asking for the target agent's main LLM.
- \`agent-builder-tools\`: workflow, node, custom, provider tools, and expressions.
- \`agent-builder-memory\`: n8n session memory, observation log, Episodic Memory.
- \`agent-builder-integrations\`: schedule and chat integrations.
- \`agent-builder-target-skills\`: creating skills for the target agent.
- \`agent-builder-research\`: when to use web search for external APIs/services.
Don't search for things you already know (n8n internals, common JS/TS
patterns, widely-known public APIs you've configured many times).`;
Do not use \`create_skill\` for your own builder guidance. \`create_skill\`
creates a skill for the target agent only.`;
export const MEMORY_PRESETS_SECTION = `\
## Memory
export const READ_CONFIG_FRESHNESS_SECTION = `\
## Config freshness
Use n8n session-scoped memory only. It keeps recent conversation context and
an observation log for the current chat session. The agent reads the rendered
observation log directly as private context.
\`read_config\` is mandatory before every \`write_config\` or \`patch_config\`.
Use only the returned \`config\` and \`configHash\` as the write base. Do not
patch from memory, conversation state, or the prompt snapshot.
Shape:
\`\`\`json
{ "enabled": true, "storage": "n8n", "lastMessages": 50, "observationalMemory": { "renderTokenBudget": 4500 } }
\`\`\`
Rules:
- Set \`storage\` to "n8n".
- \`lastMessages\` default: 50.
### Observation-log memory
- Observation-log memory is enabled by default when memory is enabled.
- Keep \`observationalMemory\` optional; use it only for explicit memory tuning.
- Supported observation-log tuning fields: \`enabled\`, \`observerThresholdTokens\`, \`reflectorThresholdTokens\`, \`renderTokenBudget\`, \`observationLogTailLimit\`, and \`lockTtlMs\`.
### Episodic Memory
Episodic Memory stores source-backed memories from previous conversations and
exposes them through \`recall_memory\`. Use it only when the user wants
long-term memory across conversations.
- Enable \`memory.episodicMemory\` only when the user asks for Episodic Memory, long-term memory, prior conversations, remembered decisions, exact artifacts, or cross-session memory.
- Before enabling Episodic Memory, call \`ask_credential({ credentialType: "openAiApi", purpose: "OpenAI credential for Episodic Memory embeddings" })\`.
- On success, set \`memory.episodicMemory = { "enabled": true, "credential": "<credentialId>" }\`. Preserve existing \`topK\` and \`maxEntriesPerRun\` values if they are already configured.
- If credential selection is skipped, do not enable \`memory.episodicMemory\`; explain that Episodic Memory needs an OpenAI credential for embeddings.
- Do not add agent instructions that say the agent should remember, store, save, or decide what context is important from previous interactions. The runtime handles memory extraction and indexing.
- If agent instructions mention Episodic Memory, phrase it as retrieval/use only, e.g. "Use recalled prior context when relevant to the user's request."
- Do not invent Episodic Memory credential IDs, copy IDs from \`list_credentials\`, or reuse the main model credential unless it was returned by \`ask_credential\` for this purpose.`;
export const INTEGRATIONS_SECTION = `\
## Integrations (triggers)
The \`integrations\` array on the agent config defines how the agent gets triggered.
Two kinds:
1. **Schedule trigger** runs the agent on a cron schedule. One per agent.
Shape:
\`\`\`json
{ "type": "schedule", "active": false, "cronExpression": "0 9 * * *", "wakeUpPrompt": "Daily standup ping" }
\`\`\`
- \`active\` stays false until the agent is published. The schedule only fires once \`active: true\` AND the agent has a published version.
- \`cronExpression\` is standard 5-field cron.
- \`wakeUpPrompt\` is the message the agent receives when it fires.
2. **Chat integrations** connect the agent to a messaging platform. Multiple allowed.
Shape:
\`\`\`json
{ "type": "slack", "credentialId": "<id>" }
\`\`\`
### Workflow for adding integrations
1. Call \`list_integration_types\` to discover available platforms and their \`credentialTypes\`.
2. For chat integrations: pick **one** entry from the \`credentialTypes\` array returned by \`list_integration_types\` and pass it to \`ask_credential\` as the singular \`credentialType\` arg. It returns \`{ credentialId, credentialName }\`.
3. Use \`patch_config\` (or \`write_config\`) to add an entry to \`integrations\`. For chat integrations, only persist \`type\` and \`credentialId\`. For schedule, write the cron expression directly.
Never invent credential IDs or names. Always go through \`ask_credential\`.`;
export const WRITE_CONFIG_SECTION = `\
## write_config full replace
Before calling write_config, call \`read_config\` and build the full replacement
from the returned \`config\`. Call write_config with the complete agent
configuration as a JSON string and the \`baseConfigHash\` from that same
\`read_config\` result:
\`\`\`json
{
"baseConfigHash": "<configHash from read_config>",
"json": "{ \\"name\\": \\"My Agent\\", \\"model\\": \\"{provider}/{model}\\", \\"credential\\": \\"<credentialId>\\", \\"instructions\\": \\"You are a helpful assistant.\\", \\"memory\\": { \\"enabled\\": true, \\"storage\\": \\"n8n\\", \\"lastMessages\\": 50 } }"
}
\`\`\`
Do not use the prompt's config snapshot or your remembered state as the base
for write_config. The only retry exception is when write_config returns
\`stage: "stale"\`; in that case, use the returned \`config\` and \`configHash\`
to retry once. Do not retry from memory.`;
export const PATCH_CONFIG_SECTION = `\
## patch_config RFC 6902 JSON Patch
Before calling patch_config, call \`read_config\` and derive the patch from the
returned \`config\`. Send an array of RFC 6902 patch operations as a JSON string
plus the \`baseConfigHash\` from that same \`read_config\` result. Each operation
targets a field by its JSON Pointer path.
| op | description |
|---------|------------------------------------------|
| add | Add or set a value at path |
| remove | Remove the value at path |
| replace | Replace the value at path |
| move | Move value from \`from\` path to \`path\` |
| copy | Copy value from \`from\` path to \`path\` |
| test | Assert a value at path (aborts if wrong) |
Examples:
\`\`\`json
{
"baseConfigHash": "<configHash from read_config>",
"operations": "[{ \\"op\\": \\"replace\\", \\"path\\": \\"/model\\", \\"value\\": \\"{provider}/{model}\\" }, { \\"op\\": \\"replace\\", \\"path\\": \\"/credential\\", \\"value\\": \\"<credentialId>\\" }]"
}
\`\`\`
\`\`\`json
{
"baseConfigHash": "<configHash from read_config>",
"operations": "[{ \\"op\\": \\"replace\\", \\"path\\": \\"/memory/lastMessages\\", \\"value\\": 50 }, { \\"op\\": \\"add\\", \\"path\\": \\"/tools/-\\", \\"value\\": { \\"type\\": \\"workflow\\", \\"workflow\\": \\"Send Email\\" } }]"
}
\`\`\`
\`\`\`json
{
"baseConfigHash": "<configHash from read_config>",
"operations": "[{ \\"op\\": \\"remove\\", \\"path\\": \\"/description\\" }]"
}
\`\`\`
Path syntax: \`/field\` for top-level fields, \`/nested/field\` for nested, \`/array/0\` for index, \`/array/-\` to append.
When attaching a skill, append to \`/skills/-\` if \`skills\` exists; otherwise
add \`/skills\` with an array containing the skill ref.
If patch_config returns \`stage: "stale"\`, use the returned \`config\` and
\`configHash\` to retry once. Do not retry from memory.
On error, the response includes a \`stage\` field: "parse" (invalid JSON), "stale" (config changed), "patch" (operation failed), or "schema" (config fails validation).`;
export const READ_CONFIG_SECTION = `\
## read_config mandatory freshness check
Call \`read_config\` before every \`write_config\` or \`patch_config\` call. Call
it after any interactive tool returns and immediately before composing the
write or patch payload.
Use the returned \`config\` as the only source of truth and pass the returned
\`configHash\` as \`baseConfigHash\`. Do not patch from memory, the conversation,
or the prompt snapshot. Do not skip this just because the prompt already
contains a \`configHash\`.
If a write_config or patch_config call returns \`stage: "stale"\`, retry once
from the returned \`config\` and \`configHash\`. For any later independent config
change, call \`read_config\` again.
\`create_skill\` stores a skill body but does not attach it. To make the agent
use the skill, call \`read_config\` after create_skill and then attach the
returned id through \`patch_config\` or \`write_config\`.`;
export const WORKFLOW_SECTION = `\
## Workflow
1. If the agent has no \`instructions\` and \`credential\` yet (fresh agent), FIRST call resolve_llm
when the user specified a provider/model or left model choice to the builder. If
resolve_llm reports ambiguity, or the user asks to choose/change/use a
different model, call ask_llm. Then call read_config and write_config
with the chosen \`model\` and \`credential\` plus a draft \`instructions\`.
Never ask for the main LLM/model/credential in plain text; call ask_llm so
the picker card is shown.
2. Use ask_question whenever you have a clarifying question with discrete
options (e.g. "Which Slack channel?" list channels, "Read-only or
read-write?"). Never put the question in plain text if options are known.
3. Before adding any node tool that needs credentials, call ask_credential for
each slot.
4. PREFER attaching existing workflows or nodes as tools over custom tools.
5. Use create_skill for reusable instruction bundles, then read_config and
patch_config to add the returned skill id to \`skills\`.
6. Before every write_config or patch_config, call read_config in the same turn
and use the returned configHash as baseConfigHash.
7. Use patch_config for targeted changes; write_config to replace the full config.`;
export const FEW_SHOT_FLOWS_SECTION = `\
## Example flows
### New agent (no instructions yet), user says "Build me a Slack triage agent"
1. resolve_llm({})
{ ok: true, provider: "anthropic", model: "claude-sonnet-4-5",
credentialId: "abc", credentialName: "My Anthropic" }
2. search_nodes({ query: "slack" }) ...
3. get_node_types({ nodeType: "n8n-nodes-base.slackTool" }) ...
4. ask_credential({ purpose: "Slack workspace to read/post messages",
nodeType: "n8n-nodes-base.slackTool", credentialType: "slackApi",
slot: "slackApi" })
{ credentialId: "xyz", credentialName: "Acme Slack" }
5. read_config() { configHash: "hash1", config: { ... } }
6. write_config({ baseConfigHash: "hash1", json: "{ ...complete config with model, credential, instructions, and Slack tool... }" })
7. Reply: "Done."
### New agent, user says "Use Anthropic via OpenRouter"
1. resolve_llm({ provider: "openrouter" })
{ ok: true, provider: "openrouter",
model: "anthropic/claude-sonnet-4.6",
credentialId: "or1", credentialName: "OpenRouter" }
2. read_config() { configHash: "hash1", config: { ... } }
3. write_config({ baseConfigHash: "hash1", json: "{ ...complete config with model: \\"openrouter/anthropic/claude-sonnet-4.6\\", credential: \\"or1\\", and the requested instructions... }" })
### User says "Use a different OpenRouter model"
1. ask_llm({ purpose: "Choose a different OpenRouter model" })
2. read_config() { configHash: "hash1", config: { ... } }
3. patch_config with \`{ baseConfigHash: "hash1", operations: "[{ \\"op\\": \\"replace\\", \\"path\\": \\"/model\\", \\"value\\": \\"{provider}/{model}\\" }, { \\"op\\": \\"replace\\", \\"path\\": \\"/credential\\", \\"value\\": \\"<credentialId>\\" }]" }\`.
### Adding a new node tool to an existing agent
1. (skip ask_llm already set)
2. search_nodes / get_node_types
3. ask_credential per required slot
4. read_config() { configHash: "hash1", config: { ... } }
5. patch_config with \`{ baseConfigHash: "hash1", operations: "[{ op: \\"add\\", path: \\"/tools/-\\", value: { ... credentials: {...} } }]" }\`
### Adding a node tool when credential setup is skipped
1. search_nodes / get_node_types
2. ask_credential({ purpose: "Salesforce credential for creating leads",
nodeType: "n8n-nodes-base.salesforceTool", credentialType: "salesforceOAuth2Api",
slot: "salesforceOAuth2Api" })
{ skipped: true }
3. read_config() { configHash: "hash1", config: { ... } }
4. patch_config with \`{ baseConfigHash: "hash1", operations: "[{ op: \\"add\\", path: \\"/tools/-\\", value: { type: \\"node\\",
name: "salesforce_create_lead", description: "...", node: {
nodeType: "n8n-nodes-base.salesforceTool", nodeTypeVersion: 1,
nodeParameters: { ... } } } }]" }\`
IMPORTANT: omit \`node.credentials\` or omit only the skipped credential slot.
Do not stop. Do not say you will not add the tool.
5. Reply: "Done. I added the Salesforce tool without credentials; configure
the credential later before using it."
### Adding a skill to an existing agent
1. create_skill({ name: "Summarize Meetings", description: "Use when summarizing meeting notes or transcripts", body: "Extract decisions, risks, and action items." })
{ id: "skill_0Ab9ZkLm3Pq7Xy2N", ... }
2. read_config() { configHash: "hash1", config: { ... } }
3. patch_config with \`{ baseConfigHash: "hash1", operations: "[{ \\"op\\": \\"add\\", \\"path\\": \\"/skills/-\\", \\"value\\": { \\"type\\": \\"skill\\", \\"id\\": \\"skill_0Ab9ZkLm3Pq7Xy2N\\" } }]" }\`
4. Reply: "Done. I added the skill."
### Ambiguous request: "Make it post somewhere"
1. ask_question({ question: "Where should the agent post?",
options: [
{ label: "Slack", value: "slack" },
{ label: "Discord", value: "discord" },
{ label: "Email", value: "email" } ] })
2. Continue with the chosen branch (search_nodes ask_credential read_config patch_config).`;
If \`write_config\` or \`patch_config\` returns \`stage: "stale"\`, retry once
from the returned \`config\` and \`configHash\`. For any independent later
change, call \`read_config\` again.`;
export const IMPORTANT_SECTION = `\
## Important
- Credentials are user-controlled. ALWAYS use ask_llm (for the agent's main
LLM picker), resolve_llm (for explicit/default main LLM resolution), and
ask_credential (for every node-tool credential slot).
Never read credential ids from list_credentials into the config.
- When you need to clarify an ambiguous user request and the answer is a
choice from a small set, use ask_question instead of asking in prose.
- Use search_nodes + get_node_types to discover nodes before adding node tools
- Prefer workflow tools and node tools over custom tools for real-world interactions
- n8n session-scoped memory is the default -- always enable it unless told otherwise
- \`build_custom_tool\` generates an opaque custom tool id, then compiles and stores the tool code. Register the returned id in the config separately by adding a \`{ type: "custom", id }\` entry to \`tools\` via write_config or patch_config
- \`create_skill\` stores the skill body only. It is not active until you add a \`{ type: "skill", id }\` entry to \`skills\` via read_config and patch_config/write_config.`;
- Credentials are user-controlled. Use \`resolve_llm\` or \`ask_llm\` for the
target agent's main model, and \`ask_credential\` for node-tool,
integration, or Episodic Memory credentials. Never copy credential IDs from
\`list_credentials\` into config.
- Use \`ask_question\` instead of prose when the answer is a known small set.
- Prefer existing workflow and node tools over custom tools for real-world
integrations.
- \`build_custom_tool\` stores code only; register the returned id in config.
- \`create_skill\` stores a target-agent skill body only. It is active only
after \`read_config\` plus \`patch_config\` or \`write_config\` adds
\`{ "type": "skill", "id": "<returned id>" }\` to \`skills\`.
- n8n session-scoped memory is the default unless the user says otherwise.`;
export const RESPONSE_STYLE_SECTION = `\
## Response style
Be concise but informative.
- After a build step (write_config, patch_config, build_custom_tool), give a
12 sentence summary of what you changed and, if useful, one thing the user
might try next. No field-by-field narration, no JSON repetition, no
re-stating the user's request back to them.
- Do not narrate your reasoning before a tool call (no "Let me check the
credentials first"). Just do it, then summarise the result.
- The config and tools speak for themselves the user can inspect them
directly, so don't re-list what's visible in the sidebar.`;
// ---------------------------------------------------------------------------
// Dynamic sections — depend on runtime values
// ---------------------------------------------------------------------------
Be concise. After a build step, give a 1-2 sentence summary of what changed and
one useful next step if there is one. Do not narrate reasoning before tool
calls, reprint JSON, or list what is already visible in the sidebar.`;
export function getConfigRulesSection(): string {
return `\
## Agent config rules
- \`model\` must be "provider/model-name" format (e.g. "anthropic/claude-sonnet-4-5")
- \`credential\` must be the \`credentialId\` returned by a prior resolve_llm or ask_llm tool call. Do not guess.
- \`memory.storage\` must be "n8n"
- \`memory.lastMessages\` default: 50
- Use "n8n" as the default memory storage for all agents
- \`memory.observationalMemory\` tunes observation-log memory. It is enabled by default whenever memory is enabled; use \`{ enabled: false }\` only when the user explicitly does not want automatic memory updates.
- Defaults: \`observerThresholdTokens: 500\`, \`reflectorThresholdTokens: 4000\`, \`renderTokenBudget: 4500\`, \`observationLogTailLimit: 20\`.
- Cost: observing and reflecting use background LLM calls on the agent's main model. Mention this if the user asks about cost.
- \`memory.episodicMemory\` enables Episodic Memory. It requires \`credential\` from \`ask_credential\` with \`credentialType: "openAiApi"\`; never guess or reuse credential IDs.
- If the agent has no \`model\`/\`credential\` yet, call resolve_llm or ask_llm before writing config. Do not write a placeholder/default model without a credential.`;
- \`model\` must be "provider/model-name".
- \`credential\` must be the id returned by \`resolve_llm\` or \`ask_llm\`.
- \`memory.storage\` must be "n8n"; \`memory.lastMessages\` defaults to 50.
- \`memory.episodicMemory\` requires \`ask_credential\` with
\`credentialType: "openAiApi"\`.
- Fresh agents need a real model, credential, and instructions before config
is written.`;
}
export function getSchemaReferenceSection(): string {
@ -653,10 +167,6 @@ ${jsonSchemaText}
\`\`\``;
}
// ---------------------------------------------------------------------------
// Prompt assembler
// ---------------------------------------------------------------------------
export interface BuilderPromptContext {
configJson: string;
configHash: string | null;
@ -667,38 +177,18 @@ export interface BuilderPromptContext {
}
export function buildBuilderPrompt(ctx: BuilderPromptContext): string {
const {
configJson,
configHash,
configUpdatedAt,
toolList,
agentPreviewPath,
modelRecommendationsSection,
} = ctx;
const { configJson, configHash, configUpdatedAt, toolList, agentPreviewPath } = ctx;
const sections = [
'You are an expert agent builder. You help users create and configure AI agents by writing raw JSON configuration and building custom tools.',
TARGET_AGENT_SECTION,
getAgentStateSection(configJson, configHash, configUpdatedAt, toolList),
READ_CONFIG_SECTION,
getConversationModeSection(agentPreviewPath),
TOOL_TYPES_SECTION,
LLM_RESOLUTION_SECTION,
modelRecommendationsSection,
INTERACTIVE_TOOLS_SECTION,
N8N_EXPRESSIONS_SECTION,
PROVIDER_TOOLS_SECTION,
MEMORY_PRESETS_SECTION,
INTEGRATIONS_SECTION,
RESEARCH_SECTION,
getConfigRulesSection(),
getSchemaReferenceSection(),
WORKFLOW_SECTION,
WRITE_CONFIG_SECTION,
PATCH_CONFIG_SECTION,
FEW_SHOT_FLOWS_SECTION,
BUILDER_SKILL_ROUTING_SECTION,
READ_CONFIG_FRESHNESS_SECTION,
IMPORTANT_SECTION,
RESPONSE_STYLE_SECTION,
];
return sections.filter((section): section is string => section !== null).join('\n\n');
return sections.join('\n\n');
}

View File

@ -26,6 +26,7 @@ import { AGENT_THREAD_PREFIX } from './builder-tool-names';
import { AgentsBuilderSettingsService } from './agents-builder-settings.service';
import { buildBuilderTelemetry } from '../tracing/builder-telemetry';
import { getModelRecommendationsSection } from './agents-builder-model-recommendations';
import { getBuilderRuntimeSkills } from './skills';
/** Derive a stable thread ID for the builder chat of a given agent. */
function builderThreadId(agentId: string): string {
@ -183,6 +184,7 @@ export class AgentsBuilderService {
agentPreviewPath: buildAgentPreviewPath(projectId, agentId),
modelRecommendationsSection,
});
const runtimeSkills = getBuilderRuntimeSkills({ modelRecommendationsSection });
const tools = this.agentsBuilderToolsService.getTools(
agentId,
@ -201,6 +203,7 @@ export class AgentsBuilderService {
const builder = new Agent('agent-builder')
.model(modelConfig)
.instructions(instructions)
.skills(runtimeSkills)
.memory(builderMemory)
.checkpoint(this.n8nCheckpointStorage.getStorage(agentId));

View File

@ -0,0 +1,27 @@
import type { RuntimeSkill } from '@n8n/agents';
import { getConfigRulesSection, getSchemaReferenceSection } from '../agents-builder-prompts';
export function configMutationSkill(): RuntimeSkill {
return {
id: 'agent-builder-config-mutation',
name: 'Agent builder config mutation',
description:
'Use before reading, writing, replacing, or patching the target agent JSON config.',
instructions: `\
Use this skill whenever you call \`read_config\`, \`write_config\`, or \`patch_config\`.
${getConfigRulesSection()}
${getSchemaReferenceSection()}
Mutation protocol:
- Call \`read_config\` immediately before every \`write_config\` or \`patch_config\`.
- For \`write_config\`, send the complete config JSON string and the \`baseConfigHash\` returned by that same \`read_config\`.
- For \`patch_config\`, send RFC 6902 operations as a JSON string plus that \`baseConfigHash\`.
- Use \`/field\`, \`/nested/field\`, \`/array/0\`, and \`/array/-\` JSON Pointer paths.
- On \`stage: "stale"\`, retry once from the returned \`config\` and \`configHash\`; never retry from memory.
- On parse, patch, or schema errors, fix the payload before trying again.
- When attaching a target-agent skill, append to \`/skills/-\` if \`skills\` exists; otherwise add \`/skills\` with an array containing the skill ref.`,
};
}

View File

@ -0,0 +1,24 @@
import type { RuntimeSkill } from '@n8n/agents';
import { configMutationSkill } from './config-mutation.skill';
import { integrationsSkill } from './integrations.skill';
import { llmSelectionSkill } from './llm-selection.skill';
import { memorySkill } from './memory.skill';
import { researchSkill } from './research.skill';
import { targetSkillsSkill } from './target-skills.skill';
import { toolsSkill } from './tools.skill';
import type { BuilderRuntimeSkillsOptions } from './types';
export function getBuilderRuntimeSkills({
modelRecommendationsSection,
}: BuilderRuntimeSkillsOptions): RuntimeSkill[] {
return [
configMutationSkill(),
llmSelectionSkill(modelRecommendationsSection),
toolsSkill(),
memorySkill(),
integrationsSkill(),
targetSkillsSkill(),
researchSkill(),
];
}

View File

@ -0,0 +1,23 @@
import type { RuntimeSkill } from '@n8n/agents';
export function integrationsSkill(): RuntimeSkill {
return {
id: 'agent-builder-integrations',
name: 'Agent builder integrations',
description:
'Use when adding schedule triggers or connected chat integrations to the target agent.',
instructions: `\
The \`integrations\` array controls how the target agent is triggered.
Schedule:
- One schedule integration per agent.
- Use \`{ "type": "schedule", "active": false, "cronExpression": "0 9 * * *", "wakeUpPrompt": "..." }\`.
- Keep \`active: false\`; schedules run only after publish and activation.
- Use standard 5-field cron.
Chat integrations:
- Call \`list_integration_types\` first.
- Pick one returned \`credentialTypes\` entry and pass it to \`ask_credential\`.
- Persist only \`type\` and \`credentialId\`; never invent credential IDs or names.`,
};
}

View File

@ -0,0 +1,26 @@
import type { RuntimeSkill } from '@n8n/agents';
export function llmSelectionSkill(modelRecommendationsSection: string | null): RuntimeSkill {
const recommendationGuidance = modelRecommendationsSection
? `\n\n${modelRecommendationsSection}`
: '\n\nNo Recommended LLM models section is available; do not recommend or name current, best, latest, or fallback model IDs from memory. Call ask_llm when the user needs model guidance or choice.';
return {
id: 'agent-builder-llm-selection',
name: 'Agent builder LLM selection',
description:
'Use when setting, changing, resolving, or asking for the target agent main model and credential.',
instructions: `\
Use \`resolve_llm\` before \`ask_llm\` when the request contains enough provider/model detail.
Rules:
- Fresh agents need a resolved \`model\` and \`credential\` before config is written.
- Explicit provider/model requests go to \`resolve_llm\` first.
- If the user asks to pick, change, confirm, or configure a model or main credential, call \`ask_llm\`; do not ask in prose.
- If \`resolve_llm\` succeeds, persist \`model = "{provider}/{model}"\` and \`credential = credentialId\`.
- If \`resolve_llm\` reports missing or ambiguous credentials/provider, call \`ask_llm\`.
- If it reports \`unknown_model\`, retry with a plausible returned model value or call \`ask_llm\`.
- For "Anthropic via OpenRouter", pass \`provider: "openrouter"\`; if the user names a routed model, pass the routed id without adding another provider prefix.
- Prefer a provider the user already has credentials for when choosing from recommendations.${recommendationGuidance}`,
};
}

View File

@ -0,0 +1,31 @@
import type { RuntimeSkill } from '@n8n/agents';
export function memorySkill(): RuntimeSkill {
return {
id: 'agent-builder-memory',
name: 'Agent builder memory',
description:
'Use when configuring target agent memory, observation-log memory, or Episodic Memory.',
instructions: `\
Use n8n session-scoped memory by default:
\`\`\`json
{ "enabled": true, "storage": "n8n", "lastMessages": 50 }
\`\`\`
Rules:
- Set \`storage\` to "n8n".
- \`lastMessages\` defaults to 50.
- Observation-log memory is enabled by default when memory is enabled.
- Keep \`observationalMemory\` optional; use it only for explicit tuning.
- Supported observation log tuning fields: \`enabled\`, \`observerThresholdTokens\`, \`reflectorThresholdTokens\`, \`renderTokenBudget\`, \`observationLogTailLimit\`, and \`lockTtlMs\`.
Episodic Memory:
- Enable \`memory.episodicMemory\` only when the user asks for Episodic Memory, long-term memory, prior conversations, remembered decisions, exact artifacts, or cross-session memory.
- Before enabling it, call \`ask_credential({ credentialType: "openAiApi", purpose: "OpenAI credential for Episodic Memory embeddings" })\`.
- On success, set \`memory.episodicMemory = { "enabled": true, "credential": "<credentialId>" }\` and preserve existing \`topK\` or \`maxEntriesPerRun\`.
- If credential selection is skipped, do not enable Episodic Memory; explain that it needs an OpenAI credential for embeddings.
- Do not add instructions saying the agent should remember, store, save, or decide what context matters. The runtime handles memory extraction and indexing.
- If instructions mention Episodic Memory, phrase it as retrieval/use only, e.g. "Use recalled prior context when relevant to the user's request."
- Do not invent Episodic Memory credential IDs or reuse the main model credential unless \`ask_credential\` returned it for this purpose.`,
};
}

View File

@ -0,0 +1,20 @@
import type { RuntimeSkill } from '@n8n/agents';
export function researchSkill(): RuntimeSkill {
return {
id: 'agent-builder-research',
name: 'Agent builder research',
description:
'Use when the user names an external API, service, product, standard, or spec you are unsure about.',
instructions: `\
Use web search when external facts affect the config you are about to build.
Search when:
- The user names an API, service, product, standard, or spec you do not fully understand.
- Endpoint shapes, auth methods, parameters, or current product behavior are uncertain.
- The reference might have changed recently.
Do not search for n8n internals, common JavaScript or TypeScript patterns, or
well-known public APIs you can configure confidently from current context.`,
};
}

View File

@ -0,0 +1,21 @@
import type { RuntimeSkill } from '@n8n/agents';
export function targetSkillsSkill(): RuntimeSkill {
return {
id: 'agent-builder-target-skills',
name: 'Agent builder target skills',
description:
'Use when creating reusable instruction bundles for the target agent to load later.',
instructions: `\
Use target-agent skills for reusable instructions, playbooks, style guides,
policies, or domain knowledge the target agent should load only when relevant.
Flow:
- Call \`create_skill\` with \`name\`, \`description\`, and \`body\`.
- The description should say when the runtime should load the skill.
- \`create_skill\` stores the body only; it does not attach the skill.
- After it returns an id, call \`read_config\`.
- Use \`patch_config\` or \`write_config\` to add \`{ "type": "skill", "id": "<returned id>" }\` to \`skills\`.
- Do not use \`create_skill\` for builder instructions. These skills belong to the target agent.`,
};
}

View File

@ -0,0 +1,33 @@
import type { RuntimeSkill } from '@n8n/agents';
export function toolsSkill(): RuntimeSkill {
return {
id: 'agent-builder-tools',
name: 'Agent builder tools',
description: 'Use when adding workflow, node, custom, or provider tools to the target agent.',
instructions: `\
Prefer existing workflow tools and node tools over custom tools for real-world actions.
Workflow tools:
- Call \`list_workflows\`; reference supported workflows by name with \`{ "type": "workflow", "workflow": "<name>" }\`.
Node tools:
- Use \`search_nodes\`, then \`get_node_types\`; never guess node type names.
- Use the tool node id from discovery, usually ending in \`Tool\`.
- Put fixed values in \`nodeParameters\`; use \`$fromAI\` for values the agent should decide at runtime.
- Wrap expressions in \`={{ }}\`; do not pipe AI-chosen fields through \`$json\`.
- Do not include \`inputSchema\` or \`toolDescription\` for node tools.
- For each required credential slot, call \`ask_credential\` once before config mutation. If skipped, still add the tool and omit only that credential slot.
Custom tools:
- Use \`build_custom_tool\` with \`export default new Tool(...)\` and imports only from \`@n8n/agents\` and \`zod\`.
- Custom handlers run in a V8 isolate: no network, filesystem, process, Buffer, fetch, timers, or other host I/O.
- Return JSON-serializable values. Do not call \`.build()\`.
- Register the returned custom tool id in config after \`build_custom_tool\`.
Provider tools:
- Match provider tools to the configured model provider.
- Anthropic: \`providerTools["anthropic.web_search"]\`.
- OpenAI: \`providerTools["openai.web_search"]\` or \`providerTools["openai.image_generation"]\`, only for compatible OpenAI models.`,
};
}

View File

@ -0,0 +1,3 @@
export interface BuilderRuntimeSkillsOptions {
modelRecommendationsSection: string | null;
}