feat(ai-builder): Allow AIA agent builder to publish and unpublish first-class agents (#34368)

This commit is contained in:
Anne Aguirre
2026-07-17 08:08:34 +01:00
committed by GitHub
parent 600826c8a7
commit 84ee0632a3
13 changed files with 298 additions and 25 deletions
@@ -36,11 +36,18 @@ export const BUILDER_NOT_CONFIGURED_CODE = 'BUILDER_NOT_CONFIGURED' as const;
export const BUILDER_CHECKPOINT_UNAVAILABLE_CODE = 'BUILDER_CHECKPOINT_UNAVAILABLE' as const;
/**
* The only two agent-builder tools that mutate the agent config. Mirrors
* `BUILDER_TOOLS.WRITE_CONFIG` / `PATCH_CONFIG` in
* Agent-builder tools whose success should set `configUpdated` on `build-agent`
* (refresh the agent artifact preview) and emit "Builder modified agent"
* telemetry. Includes config writers and publish lifecycle tools. Values must
* match `BUILDER_TOOLS` in
* `packages/cli/src/modules/agents/builder/builder-tool-names.ts`.
*/
export const CONFIG_MUTATION_TOOL_NAMES = ['write_config', 'patch_config'] as const;
export const CONFIG_MUTATION_TOOL_NAMES = [
'write_config',
'patch_config',
'publish_agent',
'unpublish_agent',
] as const;
// ---------------------------------------------------------------------------
// ask_questions
+5 -2
View File
@@ -685,8 +685,11 @@ Delegates agent building to the agents-module builder chat
turn per call. Registered in `createOrchestrationTools` only when the host
provides `builderDelegate` (agents module active). The builder's own prompt
and tools drive the build, including its interactive tools (`ask_questions`,
`ask_credential`, `ask_embedding_credential`, `configure_channel`) — the
sub-agent session no longer excludes them. Builder session state is keyed to
`ask_credential`, `ask_embedding_credential`, `configure_channel`) and
lifecycle tools (`publish_agent`, `unpublish_agent`) on the bound target agent —
the sub-agent session no longer excludes them. Forward publish/unpublish/
activate/make-live intents to `build-agent`; never tell the user to open the
agent editor and click Publish. Builder session state is keyed to
instance-AI-scoped threads (`ia-builder:<threadId>:<agentId>`) and never
appears in the agents-module builder UI.
@@ -243,13 +243,16 @@ describe('getSystemPrompt', () => {
);
});
it('routes agent listing and agent switching guidance when agents are enabled', async () => {
it('routes agent listing, switching, and publish guidance when agents are enabled', async () => {
const prompt = await getSystemPromptWithEnabledModules('agents,instance-ai');
expect(prompt).toContain('Each distinct agent the user asks for is its own build target');
expect(prompt).toContain('calls without either continue the most recent target');
expect(prompt).toContain('call `agents(action="list")` directly');
expect(prompt).toContain('find its id via `agents(action="list")` and pass it as `agentId`');
expect(prompt).toContain('forward that intent to `build-agent`');
expect(prompt).toContain('never tell the user to open the agent editor and click Publish');
expect(prompt).toContain('publishes, and unpublishes n8n **Agent** artifacts');
});
it('omits the build-agent fence and intent gate when the agents module is disabled', async () => {
@@ -45,7 +45,7 @@ const INTENT_HINT = isAgentFeatureEnabled()
: '';
const AGENT_BUILD_ROUTE = isAgentFeatureEnabled()
? '\n- **Agent build or edit** (agent-anchored per the intent gate: chat or session interaction, cross-session memory, proactive or long-running operation, learning from feedback) → call `build-agent` right away and let the builder gather requirements — do not run your own requirement-gathering round first. The builder cannot see this conversation: its only knowledge is what you pass in `message`, so include ALL requirements, constraints, and answers already gathered in the first `message` (plus `name` for a new agent or `agentId` for an existing one, and `workflowContext` for workflows built this session). Each distinct agent the user asks for is its own build target — pass `name` or `agentId` again to create or switch agents (prefer the `agentId` returned by earlier build-agent results when switching back; a name used earlier in this conversation also switches back to that agent rather than duplicating it); calls without either continue the most recent target. When the user message includes an agent-preview transcript / `<agent-preview-context>`, treat it as an edit of that agent: call `build-agent` with the given `agentId` and put the concrete behavioral findings (failures, bad tool use, wrong answers) into `message` — do not ask the user to re-describe what already appears in the transcript. While a build is in progress, forward each user follow-up to `build-agent` near-verbatim and relay its `builderReply` back. When the builder needs user input it asks directly through interactive cards in this chat — do not re-ask those questions yourself; the tool call resumes with the users answer and returns the builders reply. Actions the agent should invoke as reusable tools are built as workflows via `workflow-builder` first, then attached via `workflowContext`. For questions about existing agents ("what agents do I have?") call `agents(action="list")` directly — no intent gate; and to edit an agent NOT built in this conversation, find its id via `agents(action="list")` and pass it as `agentId`.'
? '\n- **Agent build or edit** (agent-anchored per the intent gate: chat or session interaction, cross-session memory, proactive or long-running operation, learning from feedback) → call `build-agent` right away and let the builder gather requirements — do not run your own requirement-gathering round first. The builder cannot see this conversation: its only knowledge is what you pass in `message`, so include ALL requirements, constraints, and answers already gathered in the first `message` (plus `name` for a new agent or `agentId` for an existing one, and `workflowContext` for workflows built this session). Each distinct agent the user asks for is its own build target — pass `name` or `agentId` again to create or switch agents (prefer the `agentId` returned by earlier build-agent results when switching back; a name used earlier in this conversation also switches back to that agent rather than duplicating it); calls without either continue the most recent target. When the user message includes an agent-preview transcript / `<agent-preview-context>`, treat it as an edit of that agent: call `build-agent` with the given `agentId` and put the concrete behavioral findings (failures, bad tool use, wrong answers) into `message` — do not ask the user to re-describe what already appears in the transcript. While a build is in progress, forward each user follow-up to `build-agent` near-verbatim and relay its `builderReply` back. When the builder needs user input it asks directly through interactive cards in this chat — do not re-ask those questions yourself; the tool call resumes with the users answer and returns the builders reply. When the user asks to publish, unpublish, activate, or make an agent live/usable, forward that intent to `build-agent` (the builder calls `publish_agent` / `unpublish_agent`) — never tell the user to open the agent editor and click Publish. Actions the agent should invoke as reusable tools are built as workflows via `workflow-builder` first, then attached via `workflowContext`. For questions about existing agents ("what agents do I have?") call `agents(action="list")` directly — no intent gate; and to edit an agent NOT built in this conversation, find its id via `agents(action="list")` and pass it as `agentId`.'
: '';
const WORKFLOW_ROUTE_GATE_REF = isAgentFeatureEnabled()
@@ -53,7 +53,7 @@ const WORKFLOW_ROUTE_GATE_REF = isAgentFeatureEnabled()
: '';
const AGENT_BUILDER_FENCE = isAgentFeatureEnabled()
? 'The `build-agent` tool builds and edits n8n **Agent** artifacts only (instructions, model, tools, skills, tasks, integrations, sub-agents) by delegating to the agents-module builder. It is only for that purpose. When you have classified the request as workflow-anchored (via the intent gate above), stay on the `workflow-builder` path and do not call `build-agent` at all — not to inspect nodes, not to list workflows, and not to compile custom tools. If a workflow build seems to need a utility tool the workspace does not provide, ask the user or use a placeholder; do not route around that by delegating to `build-agent`.'
? 'The `build-agent` tool builds, edits, publishes, and unpublishes n8n **Agent** artifacts (instructions, model, tools, skills, tasks, integrations, sub-agents, and publish lifecycle) by delegating to the agents-module builder. It is only for that purpose. When you have classified the request as workflow-anchored (via the intent gate above), stay on the `workflow-builder` path and do not call `build-agent` at all — not to inspect nodes, not to list workflows, and not to compile custom tools. If a workflow build seems to need a utility tool the workspace does not provide, ask the user or use a placeholder; do not route around that by delegating to `build-agent`.'
: '';
function getInstanceInfoSection(webhookBaseUrl: string, formBaseUrl: string): string {
@@ -178,9 +178,9 @@ function toTurnStream(result: StreamResult): BuilderTurnStream {
}
/**
* Build the agent-builder sub-agent: `write_config` (a plain config-mutation
* tool — its name drives `configUpdated` via `CONFIG_MUTATION_TOOL_NAMES`)
* and `ask_questions` (an interruptible tool using the real shared contract
* Build the agent-builder sub-agent: `write_config` (a mutation tool whose
* name drives `configUpdated` via `CONFIG_MUTATION_TOOL_NAMES`) and
* `ask_questions` (an interruptible tool using the real shared contract
* from `@n8n/api-types`, mirroring the cli's own `ask_questions` tool). On
* resume, `onResume` records the exact `ctx.resumeData` the SDK handed back
* after validating it against `questionsResumeSchema` — the central
@@ -500,7 +500,7 @@ describe('build-agent tool', () => {
});
describe('configUpdated', () => {
it.each(['write_config', 'patch_config'])(
it.each(['write_config', 'patch_config', 'publish_agent', 'unpublish_agent'])(
'is true when the work summary has a succeeded %s call',
async (toolName) => {
const { context, delegate } = makeContext();
@@ -647,7 +647,10 @@ export function createBuildAgentTool(context: OrchestrationContext) {
'without either keep editing the current agent. To build ANOTHER agent in the same ' +
'conversation, pass its `name` or `agentId` — a name matching an agent already built ' +
'in this conversation switches back to it; an unmatched name creates a new agent and ' +
'switches the active target. When the builder needs user input (a choice, a ' +
'switches the active target. The builder can also publish or unpublish the target ' +
'agent when the user asks to publish, activate, make it live/usable, or unpublish — ' +
'forward that intent in `message`; never tell the user to open the agent editor and ' +
'click Publish. When the builder needs user input (a choice, a ' +
'credential, or a chat channel), it surfaces automatically as an interactive card in ' +
'this chat — do not relay those questions yourself; this tool call resumes with the ' +
'users answer and returns the builders reply. Returns the builders reply, the ' +
@@ -24,6 +24,7 @@ import type { DynamicNodeParametersService } from '@/services/dynamic-node-param
import type { AgentConfigService } from '../agent-config.service';
import type { AgentCustomToolsService } from '../agent-custom-tools.service';
import type { AgentIntegrationPersistenceService } from '../agent-integration-persistence.service';
import type { AgentPublishService } from '../agent-publish.service';
import type { AgentSkillsService } from '../agent-skills.service';
import type { AgentTaskService } from '../agent-task.service';
import type { AgentsToolsService } from '../agents-tools.service';
@@ -38,6 +39,7 @@ import { BUILDER_TOOLS } from '../builder/builder-tool-names';
import type { Agent } from '../entities/agent.entity';
import type { AgentSecureRuntime } from '../runtime/agent-secure-runtime';
import type { AiService } from '@/services/ai.service';
import * as checkAccess from '@/permissions.ee/check-access';
const ctx = {
resumeData: undefined,
@@ -73,6 +75,7 @@ function makeService() {
const credentialTypes = mock<CredentialTypes>();
const mcpRegistryService = mock<McpRegistryService>();
const agentTaskService = mock<AgentTaskService>();
const agentPublishService = mock<AgentPublishService>();
const aiService = mock<AiService>();
aiService.isProxyEnabled.mockReturnValue(false);
const dynamicNodeParametersService = mock<DynamicNodeParametersService>();
@@ -101,6 +104,7 @@ function makeService() {
mock(),
credentialTypes,
agentTaskService,
agentPublishService,
aiService,
outboundHttp,
dynamicNodeParametersService,
@@ -115,6 +119,7 @@ function makeService() {
secureRuntime,
attachableWorkflowsService,
agentTaskService,
agentPublishService,
nodeTypes,
outboundHttp,
};
@@ -219,6 +224,10 @@ describe('AgentsBuilderToolsService', () => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('JSON config tools', () => {
function getJsonTool(service: AgentsBuilderToolsService, name: string) {
return service
@@ -235,6 +244,16 @@ describe('AgentsBuilderToolsService', () => {
expect(toolNames).toContain(BUILDER_TOOLS.SEARCH_MCP_SERVERS);
});
it('registers publish and unpublish tools in the builder toolset', () => {
const { service } = makeService();
const toolNames = service
.getTools(agentId, projectId, credentialProvider, user)
.json.map((tool) => tool.name);
expect(toolNames).toContain(BUILDER_TOOLS.PUBLISH_AGENT);
expect(toolNames).toContain(BUILDER_TOOLS.UNPUBLISH_AGENT);
});
it('builds verify_mcp_server with OutboundHttp SSRF protection enabled', () => {
const { service, outboundHttp } = makeService();
@@ -1391,4 +1410,139 @@ describe('AgentsBuilderToolsService', () => {
expect(result).toEqual({ ok: false, errors: [{ message: 'Agent "agent-1" not found' }] });
});
});
describe('publish_agent / unpublish_agent tools', () => {
function getPublishTool(service: AgentsBuilderToolsService) {
return service
.getTools(agentId, projectId, credentialProvider, user)
.json.find((tool) => tool.name === BUILDER_TOOLS.PUBLISH_AGENT)!;
}
function getUnpublishTool(service: AgentsBuilderToolsService) {
return service
.getTools(agentId, projectId, credentialProvider, user)
.json.find((tool) => tool.name === BUILDER_TOOLS.UNPUBLISH_AGENT)!;
}
it('publishes the bound agent draft when the user has agent:publish', async () => {
const { service, agentPublishService } = makeService();
vi.spyOn(checkAccess, 'userHasScopes').mockResolvedValue(true);
agentPublishService.publishAgent.mockResolvedValue({
activeVersionId: 'v-active',
versionId: 'v-active',
} as Agent);
const result = await getPublishTool(service).handler!({}, ctx);
expect(checkAccess.userHasScopes).toHaveBeenCalledWith(user, ['agent:publish'], false, {
projectId,
});
expect(agentPublishService.publishAgent).toHaveBeenCalledWith(
agentId,
projectId,
user,
undefined,
);
expect(result).toEqual({
ok: true,
agentId,
activeVersionId: 'v-active',
versionId: 'v-active',
});
});
it('forwards an optional versionId to publishAgent', async () => {
const { service, agentPublishService } = makeService();
vi.spyOn(checkAccess, 'userHasScopes').mockResolvedValue(true);
agentPublishService.publishAgent.mockResolvedValue({
activeVersionId: 'v-history',
versionId: 'v-draft',
} as Agent);
const result = await getPublishTool(service).handler!({ versionId: 'v-history' }, ctx);
expect(agentPublishService.publishAgent).toHaveBeenCalledWith(
agentId,
projectId,
user,
'v-history',
);
expect(result).toEqual({
ok: true,
agentId,
activeVersionId: 'v-history',
versionId: 'v-draft',
});
});
it('denies publish when the user lacks agent:publish', async () => {
const { service, agentPublishService } = makeService();
vi.spyOn(checkAccess, 'userHasScopes').mockResolvedValue(false);
const result = await getPublishTool(service).handler!({}, ctx);
expect(result).toEqual({
ok: false,
errors: [{ message: 'You do not have permission to publish agents in this project.' }],
});
expect(agentPublishService.publishAgent).not.toHaveBeenCalled();
});
it('surfaces publish service errors to the model', async () => {
const { service, agentPublishService } = makeService();
vi.spyOn(checkAccess, 'userHasScopes').mockResolvedValue(true);
agentPublishService.publishAgent.mockRejectedValue(
new Error('Cannot publish agent with missing custom tools: my_tool'),
);
const result = await getPublishTool(service).handler!({}, ctx);
expect(result).toEqual({
ok: false,
errors: [{ message: 'Cannot publish agent with missing custom tools: my_tool' }],
});
});
it('unpublishes the bound agent when the user has agent:unpublish', async () => {
const { service, agentPublishService } = makeService();
vi.spyOn(checkAccess, 'userHasScopes').mockResolvedValue(true);
agentPublishService.unpublishAgent.mockResolvedValue({
activeVersionId: null,
} as Agent);
const result = await getUnpublishTool(service).handler!({}, ctx);
expect(checkAccess.userHasScopes).toHaveBeenCalledWith(user, ['agent:unpublish'], false, {
projectId,
});
expect(agentPublishService.unpublishAgent).toHaveBeenCalledWith(agentId, projectId);
expect(result).toEqual({ ok: true, agentId, activeVersionId: null });
});
it('denies unpublish when the user lacks agent:unpublish', async () => {
const { service, agentPublishService } = makeService();
vi.spyOn(checkAccess, 'userHasScopes').mockResolvedValue(false);
const result = await getUnpublishTool(service).handler!({}, ctx);
expect(result).toEqual({
ok: false,
errors: [{ message: 'You do not have permission to unpublish agents in this project.' }],
});
expect(agentPublishService.unpublishAgent).not.toHaveBeenCalled();
});
it('surfaces unpublish service errors to the model', async () => {
const { service, agentPublishService } = makeService();
vi.spyOn(checkAccess, 'userHasScopes').mockResolvedValue(true);
agentPublishService.unpublishAgent.mockRejectedValue(new Error('Agent "agent-1" not found'));
const result = await getUnpublishTool(service).handler!({}, ctx);
expect(result).toEqual({
ok: false,
errors: [{ message: 'Agent "agent-1" not found' }],
});
});
});
});
@@ -94,7 +94,6 @@ a \`baseConfigHash\` for a later write. If \`write_config\` or
using the \`config\` and \`configHash\` it returns. Call \`read_config\`
again immediately before every later mutation and before any later
inspection of the config.`;
export const RESPONSE_STYLE_SECTION = `\
## Response Style
@@ -113,7 +112,11 @@ export const WORKFLOW_SECTION = `\
6. Follow Config Freshness immediately before every config mutation.
7. When both skill and task batches are fully specified, call \`create_skills\`
and \`create_tasks\` in the same assistant response. Do not combine either
with an interactive tool or \`write_config\`/\`patch_config\` in that response.`;
with an interactive tool or \`write_config\`/\`patch_config\` in that response.
8. When the user asks to publish, activate, or make the agent live/usable, call
\`publish_agent\`. Never tell them to click Publish in the editor. Do not
auto-publish without that intent. Use \`unpublish_agent\` when they ask to
unpublish.`;
export const FEW_SHOT_FLOWS_SECTION = `\
## Example flows
@@ -163,7 +166,12 @@ export const FEW_SHOT_FLOWS_SECTION = `\
### Ambiguous request: "Make it post somewhere"
1. \`ask_questions(...)\` with the known destination choices.
2. Continue the chosen branch with node discovery, credentials, and config
mutation.`;
mutation.
### Publish after build: "Publish it" / "Make it live"
1. Finish any pending config mutations.
2. \`publish_agent()\`.
3. Confirm the agent is live; do not send the user to the editor Publish button.`;
export interface BuilderPromptContext {
agentPreviewPath: string;
@@ -33,6 +33,7 @@ import { CredentialTypes } from '@/credential-types';
import { McpRegistryService } from '@/modules/mcp-registry/registry/mcp-registry.service';
import { NodeTypes } from '@/node-types';
import { OauthService } from '@/oauth/oauth.service';
import { userHasScopes } from '@/permissions.ee/check-access';
import { AiService } from '@/services/ai.service';
import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service';
import { createAiMcpFetch } from '@/utils/ai-proxy-fetch';
@@ -40,6 +41,7 @@ import { createAiMcpFetch } from '@/utils/ai-proxy-fetch';
import { AgentConfigService } from '../agent-config.service';
import { AgentCustomToolsService } from '../agent-custom-tools.service';
import { AgentIntegrationPersistenceService } from '../agent-integration-persistence.service';
import { AgentPublishService } from '../agent-publish.service';
import { AgentSkillsService } from '../agent-skills.service';
import { AgentTaskService } from '../agent-task.service';
import { AgentsToolsService } from '../agents-tools.service';
@@ -180,6 +182,7 @@ export class AgentsBuilderToolsService {
private readonly oauthService: OauthService,
private readonly credentialTypes: CredentialTypes,
private readonly agentTaskService: AgentTaskService,
private readonly agentPublishService: AgentPublishService,
private readonly aiService: AiService,
private readonly outboundHttp: OutboundHttp,
private readonly dynamicNodeParametersService: DynamicNodeParametersService,
@@ -465,6 +468,82 @@ export class AgentsBuilderToolsService {
})
.build();
const publishAgentTool = new Tool(BUILDER_TOOLS.PUBLISH_AGENT)
.description(
'Publish this target agent so it becomes live: integrations sync and scheduled tasks start running. ' +
'Idempotent when the draft is already the active published version. Pass optional `versionId` to ' +
'activate an existing history row instead of publishing the current draft. Call only when the user ' +
'asks to publish, activate, or make the agent live/usable — never tell them to click Publish in the editor. ' +
'Returns { ok: true, agentId, activeVersionId, versionId } or { ok: false, errors }.',
)
.input(
z.object({
versionId: z
.string()
.min(1)
.optional()
.describe(
'Optional history version ID to activate. Omit to publish the current draft.',
),
}),
)
.handler(async ({ versionId }: { versionId?: string }) => {
if (!(await userHasScopes(user, ['agent:publish'], false, { projectId }))) {
return {
ok: false,
errors: [{ message: 'You do not have permission to publish agents in this project.' }],
};
}
try {
const agent = await this.agentPublishService.publishAgent(
agentId,
projectId,
user,
versionId,
);
return {
ok: true,
agentId,
activeVersionId: agent.activeVersionId,
versionId: agent.versionId,
};
} catch (e) {
return {
ok: false,
errors: [{ message: e instanceof Error ? e.message : String(e) }],
};
}
})
.build();
const unpublishAgentTool = new Tool(BUILDER_TOOLS.UNPUBLISH_AGENT)
.description(
'Unpublish this target agent: clears the live version while preserving the draft, disconnects chat ' +
'integrations, and stops scheduled tasks. Call when the user asks to unpublish or take the agent offline. ' +
'Returns { ok: true, agentId, activeVersionId: null } or { ok: false, errors }.',
)
.input(z.object({}))
.handler(async () => {
if (!(await userHasScopes(user, ['agent:unpublish'], false, { projectId }))) {
return {
ok: false,
errors: [
{ message: 'You do not have permission to unpublish agents in this project.' },
],
};
}
try {
await this.agentPublishService.unpublishAgent(agentId, projectId);
return { ok: true, agentId, activeVersionId: null };
} catch (e) {
return {
ok: false,
errors: [{ message: e instanceof Error ? e.message : String(e) }],
};
}
})
.build();
const modelLookup: ModelLookup = {
list: async (credentialId, credentialType, provider) =>
await this.builderModelLiveLookupService.list(
@@ -482,6 +561,8 @@ export class AgentsBuilderToolsService {
patchConfigTool,
listIntegrationTypesTool,
listSubAgentsTool,
publishAgentTool,
unpublishAgentTool,
buildResolveLlmTool({ credentialProvider, modelLookup }),
buildAskCredentialTool({
credentialProvider,
@@ -618,7 +699,7 @@ export class AgentsBuilderToolsService {
'objective field carries its own structured template. The whole batch is all-or-nothing: an ' +
'invalid cron or objective rejects every task in the call. This adds a `{ type: "task", id, ' +
'enabled }` ref per task to the agent config (config.tasks) and each task starts running once ' +
'the agent is (re)published. Returns { ok: true, tasks: [{ id, name, enabled }, ...] } (same ' +
'the agent is (re)published via `publish_agent`. Returns { ok: true, tasks: [{ id, name, enabled }, ...] } (same ' +
'order as input, objectives and crons are not echoed back) or { ok: false, errors }.',
)
.systemInstruction(
@@ -661,7 +742,7 @@ export class AgentsBuilderToolsService {
try {
// Adds a `{ type:'task', id, enabled }` ref per task to the agent config
// and creates every body in one transaction. Enabled by default; each
// task starts running once the agent is (re)published.
// task starts running once the agent is (re)published via publish_agent.
const created = await this.agentTaskService.createTasks(
agentId,
projectId,
@@ -9,8 +9,9 @@
*/
export const BUILDER_TOOLS = {
READ_CONFIG: 'read_config',
// WRITE_CONFIG / PATCH_CONFIG values must match `CONFIG_MUTATION_TOOL_NAMES`
// in `@n8n/api-types` (agents/agent-interaction.schema.ts).
// WRITE_CONFIG / PATCH_CONFIG / PUBLISH_AGENT / UNPUBLISH_AGENT values must
// match `CONFIG_MUTATION_TOOL_NAMES` in `@n8n/api-types`
// (agents/agent-interaction.schema.ts).
WRITE_CONFIG: 'write_config',
PATCH_CONFIG: 'patch_config',
BUILD_CUSTOM_TOOL: 'build_custom_tool',
@@ -19,6 +20,8 @@ export const BUILDER_TOOLS = {
GET_RESOURCE_LOCATOR_OPTIONS: 'get_resource_locator_options',
LIST_INTEGRATION_TYPES: 'list_integration_types',
LIST_SUB_AGENTS: 'list_sub_agents',
PUBLISH_AGENT: 'publish_agent',
UNPUBLISH_AGENT: 'unpublish_agent',
RESOLVE_LLM: 'resolve_llm',
SEARCH_MCP_SERVERS: 'search_mcp_servers',
VERIFY_MCP_SERVER: 'verify_mcp_server',
@@ -8,7 +8,13 @@ export function targetTasksSkill(): RuntimeSkill {
name: 'Agent Builder Target Tasks',
description:
'Use when the user wants the target agent to run something on a recurring schedule (a "task"): a daily/weekly/hourly objective the agent carries out on its own with create_tasks. Not for one-off requests, chat/event triggers, or config/tool/skill/model edits.',
recommendedTools: ['create_tasks', 'ask_questions', 'read_config', 'patch_config'],
recommendedTools: [
'create_tasks',
'ask_questions',
'read_config',
'patch_config',
'publish_agent',
],
allowedTools: [
'create_tasks',
'ask_questions',
@@ -19,6 +25,7 @@ export function targetTasksSkill(): RuntimeSkill {
'search_nodes',
'get_node_types',
'ask_credential',
'publish_agent',
],
instructions: `\
## Purpose
@@ -89,10 +96,12 @@ template and pin down the cadence for every task. Never create a placeholder or
- \`create_tasks\` adds a \`{ type: "task", id, enabled }\` ref per task to
\`config.tasks\` and creates each task body. Tasks are enabled by default and
only start running once the agent is (re)published; tell the user this when
relevant.
only start running once the agent is (re)published via \`publish_agent\`; tell
the user this when relevant, and call \`publish_agent\` when they ask to publish
or make the agent live.
- To disable or remove a task, edit \`config.tasks\` with \`patch_config\` (set
\`enabled: false\`, or drop the ref). Changes take effect on the next publish.
\`enabled: false\`, or drop the ref). Changes take effect on the next
\`publish_agent\`.
- \`create_tasks\` does NOT add tools — if a task needs a tool the agent lacks,
add it to the config yourself first.
- Do not call \`create_tasks\` once per task when several are ready; batch them
@@ -24,7 +24,9 @@ export const INSTANCE_AI_BUILDER_ADDENDUM = `## Instance AI session rules
You are running as a sub-agent inside n8n's instance AI chat; the user sees your questions as chat cards.
The agent preview link is not visible in this chat; describe outcomes in text instead of linking the preview.`;
The agent preview link is not visible in this chat; describe outcomes in text instead of linking the preview.
You can publish and unpublish the target agent with \`publish_agent\` and \`unpublish_agent\`. Never tell the user to open the agent editor and click Publish.`;
function isTextDeltaChunk(
chunk: StreamChunk,