This commit is contained in:
Riqwan Thamir 2026-06-29 23:58:09 +02:00
parent 33c6d6fcba
commit baf50839d3
No known key found for this signature in database
9 changed files with 490 additions and 163 deletions

View File

@ -87,6 +87,31 @@ fixed taxonomy of sub-agent types.
- Sub-agent has no memory — receives context only via the briefing
- Past failed attempts from `iterationLog` are appended to the briefing (if available)
### `discover-workflow-context`
Preconfigured pre-build discovery. Spawns a focused, **synchronous** sub-agent
(fixed role, prompt, and tool subset: `nodes`, `credentials`, `research`) that
inventories the nodes and credential types a build needs and returns the relevant
node **type definitions verbatim** — it selects which types are relevant rather
than summarizing them. Unlike free-form `delegate`, the scout is fixed so
discovery is consistent and testable. Call before loading `workflow-builder` for
any build touching external services or unfamiliar nodes; act on the result in
the same turn. Does not build, patch, or run workflows.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `services` | string[] | yes | External services as short node-search terms (e.g. `["Gmail", "OpenAI"]`) |
| `categories` | string[] | no | Workflow technique categories to anchor node suggestions |
| `conversationContext` | string | no | Build goal and user constraints to scope discovery |
**Returns**: `{ result: string }` — the scout's debrief: brief Nodes and
Credentials bullets, the relevant node type definitions verbatim, and Gaps.
**Behavior**: Resolves the fixed tool subset from the orchestrator's domain
tools, runs the sub-agent via the shared synchronous runner (same tracing and
`agent-spawned`/`agent-completed` events as `delegate`), and returns the
synthesized debrief. Returns an error string if the `nodes` tool is unavailable.
### `update-tasks`
Update a visible task checklist for the user. Used for lightweight progress

View File

@ -118,24 +118,27 @@ When mapping downstream fields from an OpenAI node, read
## Pre-build discovery
Before writing code, the orchestrator may have already run parallel `delegate`
sub-agents — **KB scout**, **credential scout**, and **node researcher** (see
orchestrator delegation rules). Their debriefings are compressed summaries, not
full tool payloads: technique bullets, credential availability, and node IDs with
discriminators.
Before writing code, the orchestrator may have already run pre-build discovery —
a `discover-workflow-context` call (nodes, credentials, and type definitions) and
optionally a `delegate` **KB scout** (technique bullets). The discovery debrief
gives node IDs with discriminators and credential availability as brief bullets,
plus the relevant node **type definitions verbatim** (selected, not summarized);
the KB scout returns technique bullets.
When debriefings are in context, use them as authoritative discovery output — do
When debriefings are in context, use them as authoritative discovery output — the
verbatim type definitions are the real schema, so build from them directly and do
not repeat the same `nodes`, `credentials`, or workspace reads unless a debriefing
is missing a fact you still need.
When no delegate debriefings exist, run steps 15 of Mandatory Process inline in
When no discovery debriefings exist, run steps 15 of Mandatory Process inline in
this turn — but only for simple single-service builds or edits that reuse an
existing `.workflow.ts` file. For a multi-system build (two or more external
services, or work needing 2+ knowledge-base guides/templates) with no debriefings
in context, the orchestrator skipped required discovery delegation: stop and let
it run the Pre-build discovery delegation scouts first, then resume here with
their debriefings. Do not call `delegate` from this skill — discovery delegation
is the orchestrator's job before loading `workflow-builder`.
existing `.workflow.ts` file. For a build touching external services or unfamiliar
nodes (especially two or more external services, or work needing 2+ knowledge-base
guides/templates) with no debriefings in context, the orchestrator skipped
required discovery: stop and let it run `discover-workflow-context` (and any KB
`delegate` scout) first, then resume here with their debriefings. Do not call
`discover-workflow-context` or `delegate` from this skill — pre-build discovery is
the orchestrator's job before loading `workflow-builder`.
## Workflow-Level Error Workflows

View File

@ -127,7 +127,7 @@ ${getProjectScopeSection(projectId)}
Match the user's request against skill descriptions in the catalog. Call \`load_skill\` before acting on a matched skill's guidance never call \`data-tables\` or \`parse-file\` without loading \`data-table-manager\` first, and never call \`build-workflow\` without loading \`workflow-builder\` first. A single turn may need more than one skill when routing requires it (e.g. \`data-table-manager\` then \`workflow-builder\`).
- **Single workflow build or edit** (new workflow, add/remove/rewire nodes, expression/credential/schedule/Code fixes, including workflows that create or write to Data Tables):
- **Multi-system build first turn** (two or more external services, e.g. Form + OpenAI + Google Sheets, or work needing 2+ knowledge-base guides/templates) you MUST run **Pre-build discovery delegation** (see below) before \`workflow-builder\`: spawn parallel \`delegate\` scouts, wait for their inline results, then \`data-table-manager\` when tables are involved, then \`workflow-builder\` → workspace file tools → \`build-workflow\`.
- **Build that touches external services or unfamiliar nodes** (one or more external integrations, e.g. Form + OpenAI + Google Sheets, or any node whose exact type/params you are unsure of) you MUST run **Pre-build discovery** (see below) before \`workflow-builder\`: call \`discover-workflow-context\` for the nodes/credentials/types, optionally spawn parallel \`delegate\` KB scouts, wait for their inline results, then \`data-table-manager\` when tables are involved, then \`workflow-builder\` → workspace file tools → \`build-workflow\`.
- **Simple single-service builds and existing-workflow edits** skip delegation; go straight to \`data-table-manager\` when tables are involved, then \`workflow-builder\` → workspace file tools → \`build-workflow\`.
- 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.
- After every successful direct \`build-workflow\` result, if the tool output contains \`postBuildFlow.required: true\`, load \`post-build-flow\` exactly once and follow it before verification, setup, error-workflow follow-up, publishing, testing, or any final user-visible summary. Do not create a plan just for verification.
@ -141,15 +141,15 @@ Match the user's request against skill descriptions in the catalog. Call \`load_
Use \`task-control(action="update-checklist")\` only for lightweight visible checklists that do not need scheduler-driven execution.
## Pre-build discovery delegation
## Pre-build discovery
\`delegate\` runs **synchronously**: each call spawns a focused sub-agent, runs it, and returns its result text to you in the same turn (it is NOT a background task and shows no approval card). Spawn several in one step to run them in parallel, then read their results and continue building.
Discovery sub-agents run **synchronously**: each call spawns a focused sub-agent, runs it, and returns its result text to you in the same turn (it is NOT a background task and shows no approval card). Run them, read their results, then continue building.
Before loading \`workflow-builder\` or \`planning\`, delegate discovery when any of these apply: multiple external systems; reading two or more knowledge-base best-practice guides or templates; credential inventory plus \`nodes(action="explore-resources")\` across services. For multi-system builds, spawn parallel \`delegate\` calls with these roles:
- **KB & skills scout** use sandbox workspace tools (read, list, grep/rg via execute). They attach automatically to delegated sub-agents when a sandbox exists pass \`tools: []\` for KB-only scouts. Do not pass skill-catalog aliases like \`read_file\`/\`write_file\`; those are not delegate tools. Return technique bullets only, not full file contents.
- **Node researcher & Credential scout** \`tools: ["nodes", "credentials"]\`: \`suggested\`, \`search\`, \`type-definition\`, and \`explore-resources\` when credentials exist; return node IDs, discriminators, credential types, and key parameter notes. list available types and names for required services; note gaps.
Before loading \`workflow-builder\` or \`planning\` for any build touching external services or unfamiliar nodes:
- **Nodes, credentials, and types \`discover-workflow-context\`.** This is the primary, preferred discovery call: pass \`services\` (short node-search terms like \`["Gmail", "Google Sheets", "OpenAI"]\`), optional \`categories\`, and \`conversationContext\`. It spawns a fixed scout that returns node IDs with discriminators, required credential types and whether they exist, and the relevant node type definitions **verbatim** (it selects the relevant types rather than summarizing them). Prefer one \`discover-workflow-context\` call over hand-composing \`delegate\` node/credential scouts.
- **Knowledge-base guides/templates \`delegate\` KB scout** (only when the build needs 2+ KB best-practice guides or templates) — use sandbox workspace tools (read, list, grep/rg via execute). They attach automatically to delegated sub-agents when a sandbox exists — pass \`tools: []\` for KB-only scouts. Do not pass skill-catalog aliases like \`read_file\`/\`write_file\`; those are not delegate tools. Return technique bullets only, not full file contents.
Pass complete briefings and \`conversationContext\`; synthesize compressed debriefings before loading \`workflow-builder\`. For simple single-service builds with one credential type, or edits that reuse an existing workspace \`.workflow.ts\` file, skip delegation and discover inline. Never use \`delegate\` to build, patch, fix, or update workflows — workflow building runs in the orchestrator with \`workflow-builder\`, workspace file tools, and \`build-workflow\`.
Run \`discover-workflow-context\` and any KB \`delegate\` scout in parallel in one step. Pass complete briefings and \`conversationContext\`; synthesize the compressed debriefings before loading \`workflow-builder\`. For simple single-service builds with one well-known node and credential type, or edits that reuse an existing workspace \`.workflow.ts\` file, you may skip discovery and proceed inline. Never use \`delegate\` or \`discover-workflow-context\` to build, patch, fix, or update workflows — workflow building runs in the orchestrator with \`workflow-builder\`, workspace file tools, and \`build-workflow\`.
## System follow-ups

View File

@ -37,6 +37,10 @@ const loadCompleteCheckpointTool = lazyMod(
const loadDelegateTool = lazyMod(
() => require('./orchestration/delegate.tool') as typeof import('./orchestration/delegate.tool'),
);
const loadDiscoverWorkflowContextTool = lazyMod(
() =>
require('./orchestration/discover-workflow-context.tool') as typeof import('./orchestration/discover-workflow-context.tool'),
);
const loadEvalDataAgentTool = lazyMod(
() =>
require('./orchestration/eval-data-agent.tool') as typeof import('./orchestration/eval-data-agent.tool'),
@ -142,6 +146,10 @@ export function createOrchestrationTools(context: OrchestrationContext): Instanc
[ORCHESTRATION_TOOL_IDS.CREATE_TASKS, loadPlanTool().createPlanTool(context)],
[ORCHESTRATION_TOOL_IDS.TASK_CONTROL, loadTaskControlTool().createTaskControlTool(context)],
[ORCHESTRATION_TOOL_IDS.DELEGATE, loadDelegateTool().createDelegateTool(context)],
[
ORCHESTRATION_TOOL_IDS.DISCOVER_WORKFLOW_CONTEXT,
loadDiscoverWorkflowContextTool().createDiscoverWorkflowContextTool(context),
],
[
ORCHESTRATION_TOOL_IDS.COMPLETE_CHECKPOINT,
loadCompleteCheckpointTool().createCompleteCheckpointTool(context),

View File

@ -0,0 +1,111 @@
import { executeTool } from '../../../__tests__/tool-test-utils';
import { createToolRegistry } from '../../../tool-registry';
import type { OrchestrationContext } from '../../../types';
import { runSyncSubAgent } from '../delegate.tool';
import {
createDiscoverWorkflowContextTool,
discoverWorkflowContextInputSchema,
} from '../discover-workflow-context.tool';
vi.mock('../delegate.tool', () => ({ runSyncSubAgent: vi.fn() }));
const runSyncSubAgentMock = vi.mocked(runSyncSubAgent);
function createMockContext(domainTools: Record<string, unknown> = {}): OrchestrationContext {
return {
threadId: 'test-thread',
runId: 'test-run',
userId: 'test-user',
orchestratorAgentId: 'test-agent',
modelId: 'test-model',
subAgentMaxSteps: 5,
eventBus: {
publish: vi.fn(),
subscribe: vi.fn(),
getEventsAfter: vi.fn(),
getNextEventId: vi.fn(),
getEventsForRun: vi.fn().mockReturnValue([]),
getEventsForRuns: vi.fn().mockReturnValue([]),
},
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
domainTools: createToolRegistry(
Object.entries(domainTools).map(([name, tool]) => [
name,
{ name, description: name, ...(tool as object) },
]),
),
abortSignal: new AbortController().signal,
taskStorage: { get: vi.fn(), save: vi.fn() },
} as unknown as OrchestrationContext;
}
beforeEach(() => {
runSyncSubAgentMock.mockReset();
});
describe('discoverWorkflowContextInputSchema', () => {
it('accepts valid input', () => {
const result = discoverWorkflowContextInputSchema.safeParse({
services: ['Gmail', 'Google Sheets'],
categories: ['data_persistence'],
conversationContext: 'Build a lead capture workflow',
});
expect(result.success).toBe(true);
});
it('rejects an empty services array', () => {
const result = discoverWorkflowContextInputSchema.safeParse({ services: [] });
expect(result.success).toBe(false);
});
it('rejects missing services', () => {
const result = discoverWorkflowContextInputSchema.safeParse({ categories: ['chatbot'] });
expect(result.success).toBe(false);
});
});
describe('createDiscoverWorkflowContextTool', () => {
it('errors when the nodes tool is unavailable', async () => {
const context = createMockContext({ credentials: {} });
const tool = createDiscoverWorkflowContextTool(context);
const output = await executeTool<{ result: string }>(
tool,
{ services: ['Gmail'] },
{} as never,
);
expect(output.result).toContain('nodes');
expect(runSyncSubAgentMock).not.toHaveBeenCalled();
});
it('runs the scout with the available discovery tools and returns its debrief', async () => {
runSyncSubAgentMock.mockResolvedValue({ result: 'debrief text' });
const context = createMockContext({ nodes: {}, credentials: {}, research: {} });
const tool = createDiscoverWorkflowContextTool(context);
const output = await executeTool<{ result: string }>(
tool,
{ services: ['Gmail', 'OpenAI'], conversationContext: 'ctx' },
{} as never,
);
expect(output.result).toBe('debrief text');
expect(runSyncSubAgentMock).toHaveBeenCalledTimes(1);
const callArg = runSyncSubAgentMock.mock.calls[0][1];
expect(callArg.toolNames).toEqual(['nodes', 'credentials', 'research']);
expect(callArg.conversationContext).toBe('ctx');
expect(callArg.briefing).toContain('Gmail, OpenAI');
});
it('only resolves the discovery tools that are registered', async () => {
runSyncSubAgentMock.mockResolvedValue({ result: 'ok' });
const context = createMockContext({ nodes: {} });
const tool = createDiscoverWorkflowContextTool(context);
await executeTool(tool, { services: ['Slack'] }, {} as never);
const callArg = runSyncSubAgentMock.mock.calls[0][1];
expect(callArg.toolNames).toEqual(['nodes']);
});
});

View File

@ -2,7 +2,12 @@ import { Tool } from '@n8n/agents';
import { nanoid } from 'nanoid';
import { createSubAgentPersistence } from './agent-persistence';
import { delegateInputSchema, delegateOutputSchema, type DelegateInput } from './delegate.schemas';
import {
delegateInputSchema,
delegateOutputSchema,
type DelegateInput,
type DelegateOutput,
} from './delegate.schemas';
import { truncateLabel } from './display-utils';
import {
createDetachedSubAgentTraceFactory,
@ -289,6 +294,172 @@ export async function startDetachedDelegateTask(
};
}
/**
* Input for {@link runSyncSubAgent} tools are already resolved and validated by
* the caller so this helper stays focused on spawning, streaming, and debriefing.
*/
export interface RunSyncSubAgentInput {
role: string;
instructions: string;
briefing: string;
/** Pre-resolved, validated tool registry the sub-agent may use. */
validTools: InstanceAiToolRegistry;
/** Tool names for the agent-spawned UI event (display only). */
toolNames: string[];
artifacts?: unknown;
conversationContext?: string;
}
/**
* Run a focused sub-agent synchronously: spawn it, stream with HITL support, and
* return a synthesized debrief in the same turn. Shared by `delegate` and the
* preconfigured discovery tool so both follow identical tracing/event semantics.
*/
export async function runSyncSubAgent(
context: OrchestrationContext,
input: RunSyncSubAgentInput,
): Promise<DelegateOutput> {
const subAgentId = generateAgentId();
const startTime = Date.now();
context.eventBus.publish(context.threadId, {
type: 'agent-spawned',
runId: context.runId,
agentId: subAgentId,
payload: {
parentId: context.orchestratorAgentId,
role: input.role,
tools: input.toolNames,
kind: 'delegate',
subtitle: truncateLabel(input.briefing),
goal: input.briefing,
},
});
const traceRun = await startSubAgentTrace(context, {
agentId: subAgentId,
role: input.role,
kind: 'delegate',
inputs: {
briefing: input.briefing,
instructions: input.instructions,
tools: input.toolNames,
conversationContext: input.conversationContext,
},
});
const tracedTools = traceSubAgentTools(context, input.validTools, input.role);
try {
const subAgent = createSubAgent({
agentId: subAgentId,
role: input.role,
instructions: input.instructions,
tools: tracedTools,
modelId: context.subAgentModelId ?? context.modelId,
traceRun,
tracing: context.tracing,
...buildSubAgentWorkspaceOptions(context),
runtimeSkills: context.runtimeSkills,
timeZone: context.timeZone,
checkpointStore: context.checkpointStore,
});
const briefingMessage = await buildDelegateBriefing(
context,
input.role,
input.briefing,
input.artifacts,
input.conversationContext,
);
const consumeResult = await withTraceRun(context, traceRun, async () => {
const maxIterations = context.subAgentMaxSteps ?? MAX_STEPS.DELEGATE_FALLBACK;
const persistence = await createSubAgentPersistence(context, {
agentKind: input.role,
});
const stream = await subAgent.stream(briefingMessage, {
maxIterations,
abortSignal: context.abortSignal,
persistence,
providerOptions: {
anthropic: { cacheControl: { type: 'ephemeral' } },
},
});
return await consumeStreamWithHitl({
agent: subAgent,
stream,
runId: context.runId,
agentId: subAgentId,
eventBus: context.eventBus,
logger: context.logger,
threadId: context.threadId,
outputRedaction: context.outputRedaction,
abortSignal: context.abortSignal,
waitForConfirmation: context.waitForConfirmation,
maxIterations,
persistence,
});
});
const resultText = await requireCompletedHitlText(consumeResult, 'Delegate sub-agent');
const debriefing = buildDebriefing({
agentId: subAgentId,
role: input.role,
result: resultText,
workSummary: consumeResult.workSummary,
startTime,
});
await finishTraceRun(context, traceRun, {
outputs: {
result: resultText,
agentId: subAgentId,
role: input.role,
toolCallCount: debriefing.toolCallCount,
toolErrorCount: debriefing.toolErrorCount,
durationMs: debriefing.durationMs,
},
});
context.eventBus.publish(context.threadId, {
type: 'agent-completed',
runId: context.runId,
agentId: subAgentId,
payload: {
role: input.role,
result: resultText,
},
});
return {
result: resultText,
toolCallCount: debriefing.toolCallCount,
toolErrorCount: debriefing.toolErrorCount,
durationMs: debriefing.durationMs,
blockers: debriefing.blockers,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
await failTraceRun(context, traceRun, error, {
agent_id: subAgentId,
agent_role: input.role,
});
context.eventBus.publish(context.threadId, {
type: 'agent-completed',
runId: context.runId,
agentId: subAgentId,
payload: {
role: input.role,
result: '',
error: errorMessage,
},
});
return { result: `Sub-agent error: ${errorMessage}` };
}
}
export function createDelegateTool(context: OrchestrationContext) {
return new Tool('delegate')
.description(
@ -317,150 +488,15 @@ export function createDelegateTool(context: OrchestrationContext) {
};
}
const subAgentId = generateAgentId();
const startTime = Date.now();
// 2. Publish agent-spawned
context.eventBus.publish(context.threadId, {
type: 'agent-spawned',
runId: context.runId,
agentId: subAgentId,
payload: {
parentId: context.orchestratorAgentId,
role: input.role,
tools: input.tools,
kind: 'delegate',
subtitle: truncateLabel(input.briefing),
goal: input.briefing,
},
});
const traceRun = await startSubAgentTrace(context, {
agentId: subAgentId,
return await runSyncSubAgent(context, {
role: input.role,
kind: 'delegate',
inputs: {
briefing: input.briefing,
instructions: input.instructions,
tools: input.tools,
conversationContext: input.conversationContext,
},
instructions: input.instructions,
briefing: input.briefing,
validTools,
toolNames: input.tools,
artifacts: input.artifacts,
conversationContext: input.conversationContext,
});
const tracedTools = traceSubAgentTools(context, validTools, input.role);
try {
// 3. Create sub-agent
const subAgent = createSubAgent({
agentId: subAgentId,
role: input.role,
instructions: input.instructions,
tools: tracedTools,
modelId: context.subAgentModelId ?? context.modelId,
traceRun,
tracing: context.tracing,
...buildSubAgentWorkspaceOptions(context),
runtimeSkills: context.runtimeSkills,
timeZone: context.timeZone,
checkpointStore: context.checkpointStore,
});
const briefingMessage = await buildDelegateBriefing(
context,
input.role,
input.briefing,
input.artifacts,
input.conversationContext,
);
// 4. Stream sub-agent with HITL support
const consumeResult = await withTraceRun(context, traceRun, async () => {
const maxIterations = context.subAgentMaxSteps ?? MAX_STEPS.DELEGATE_FALLBACK;
const persistence = await createSubAgentPersistence(context, {
agentKind: input.role,
});
const stream = await subAgent.stream(briefingMessage, {
maxIterations,
abortSignal: context.abortSignal,
persistence,
providerOptions: {
anthropic: { cacheControl: { type: 'ephemeral' } },
},
});
return await consumeStreamWithHitl({
agent: subAgent,
stream,
runId: context.runId,
agentId: subAgentId,
eventBus: context.eventBus,
logger: context.logger,
threadId: context.threadId,
outputRedaction: context.outputRedaction,
abortSignal: context.abortSignal,
waitForConfirmation: context.waitForConfirmation,
maxIterations,
persistence,
});
});
const resultText = await requireCompletedHitlText(consumeResult, 'Delegate sub-agent');
const debriefing = buildDebriefing({
agentId: subAgentId,
role: input.role,
result: resultText,
workSummary: consumeResult.workSummary,
startTime,
});
await finishTraceRun(context, traceRun, {
outputs: {
result: resultText,
agentId: subAgentId,
role: input.role,
toolCallCount: debriefing.toolCallCount,
toolErrorCount: debriefing.toolErrorCount,
durationMs: debriefing.durationMs,
},
});
// 7. Publish agent-completed
context.eventBus.publish(context.threadId, {
type: 'agent-completed',
runId: context.runId,
agentId: subAgentId,
payload: {
role: input.role,
result: resultText,
},
});
return {
result: resultText,
toolCallCount: debriefing.toolCallCount,
toolErrorCount: debriefing.toolErrorCount,
durationMs: debriefing.durationMs,
blockers: debriefing.blockers,
};
} catch (error) {
// 8. Publish agent-completed with error
const errorMessage = error instanceof Error ? error.message : String(error);
await failTraceRun(context, traceRun, error, {
agent_id: subAgentId,
agent_role: input.role,
});
context.eventBus.publish(context.threadId, {
type: 'agent-completed',
runId: context.runId,
agentId: subAgentId,
payload: {
role: input.role,
result: '',
error: errorMessage,
},
});
return { result: `Sub-agent error: ${errorMessage}` };
}
})
.build();
}

View File

@ -0,0 +1,31 @@
/**
* System prompt for the discovery sub-agent spawned by `discover-workflow-context`.
*
* The sub-agent inventories the nodes and credentials a build needs and returns
* the relevant node type definitions VERBATIM (selection only, no summarizing) so
* the builder agent works from the real schema. It must NOT build, patch, or run
* anything discovery only.
*/
export const DISCOVER_WORKFLOW_CONTEXT_PROMPT = `You are a workflow discovery specialist for n8n. Before a workflow is built, you gather the exact nodes, credentials, and type definitions the build needs, so the builder agent never guesses. You report to a parent agent, not a human.
## Scope
- Discovery ONLY. Never build, patch, update, or run workflows. You have no build tools.
- Cover three things for the services in the briefing: which NODES to use, which CREDENTIALS exist or are missing, and the relevant TYPE DEFINITIONS.
- Your job with types is **selection, not summarization**: decide which node types are relevant, then return their type definitions exactly as the tool gave them.
## Mandatory Process
1. If the briefing names workflow categories, call \`nodes(action="suggested")\` for them first to anchor on canonical nodes.
2. For each external service, call \`nodes(action="search")\` with a short service name (e.g. "Gmail", "Slack") — not full task phrases. Note the resource/operation/mode discriminators in results.
3. Call \`nodes(action="type-definition")\` with the exact node IDs you will recommend (include discriminators; up to 5 per call). Do not fetch definitions for nodes you will not recommend.
4. Call \`credentials(action="list")\` (filter by \`type\` when you know it) to record which required credential types already exist (by name) and which are missing. Use \`credentials(action="search-types")\` to confirm the canonical credential type for a service when unsure. NEVER expose secrets — metadata only.
5. Use \`research\` only when a node's type definition is insufficient to understand a provider's requirements (e.g. an HTTP Request integration with no dedicated node). Keep it to targeted lookups.
## Output Contract
Output these sections in order:
- **Nodes**: one bullet per node you recommend \`<nodeType>\` (+ version) with the required \`resource\`/\`operation\`/\`mode\` discriminators and the credential type it needs. This is a selection list, keep it brief.
- **Credentials**: per required type, state whether a usable credential EXISTS (give its name) or is MISSING. Metadata only never secret values.
- **Type definitions**: for every node listed under **Nodes**, paste the \`nodes(action="type-definition")\` output for that node **VERBATIM**. Do NOT summarize, condense, paraphrase, reformat, or drop any fields — include every parameter, enum value, \`@builderHint\`, \`@default\`, and display condition exactly as returned. The only filtering allowed is at the node level: omit definitions for nodes you are not recommending. Label each block with its node type.
- **Gaps**: any requested service with no suitable node/credential, or anything you could not verify state it plainly.
Keep the **Nodes**, **Credentials**, and **Gaps** sections tight (bullets, not prose), but leave **Type definitions** untouched and complete. Do NOT narrate your steps; output only the final debrief.`;

View File

@ -0,0 +1,111 @@
/**
* Preconfigured Workflow Discovery Tool
*
* Spawns a focused, synchronous sub-agent that inventories the nodes and
* credentials a build needs and returns the relevant node type definitions
* verbatim (selection only, no summarizing). Unlike free-form `delegate`, the
* role, system prompt, and tool subset are fixed here so pre-build discovery is
* consistent and testable.
*
* Runs synchronously: the result is returned to the orchestrator in the same
* turn, which then loads `workflow-builder` and builds with that context.
*/
import { Tool } from '@n8n/agents';
import { z } from 'zod';
import { runSyncSubAgent } from './delegate.tool';
import { DISCOVER_WORKFLOW_CONTEXT_PROMPT } from './discover-workflow-context.prompt';
import { createToolRegistry, toolRegistryKeys } from '../../tool-registry';
import type { OrchestrationContext } from '../../types';
import { DOMAIN_TOOL_IDS, ORCHESTRATION_TOOL_IDS } from '../tool-ids';
const DISCOVERY_ROLE = 'workflow-context-scout';
/** Native domain tools the discovery sub-agent may use, in priority order. */
const DISCOVERY_TOOL_NAMES = [
DOMAIN_TOOL_IDS.NODES,
DOMAIN_TOOL_IDS.CREDENTIALS,
DOMAIN_TOOL_IDS.RESEARCH,
] as const;
export const discoverWorkflowContextInputSchema = z.object({
services: z
.array(z.string().min(1))
.min(1)
.describe(
'External services / integrations the workflow will touch, as short node-search terms (e.g. ["Gmail", "Google Sheets", "OpenAI"]). The scout finds the right nodes and credential types for each.',
),
categories: z
.array(z.string().min(1))
.optional()
.describe(
'Optional workflow technique categories to anchor node suggestions (e.g. ["form_input", "content_generation", "data_persistence"]).',
),
conversationContext: z
.string()
.optional()
.describe(
'Brief summary of the build goal and any user constraints (named accounts, channels, tables, required node families). The scout uses this to scope discovery.',
),
});
export type DiscoverWorkflowContextInput = z.infer<typeof discoverWorkflowContextInputSchema>;
function buildDiscoveryBriefing(input: DiscoverWorkflowContextInput): string {
const lines = [
'Discover the nodes, credentials, and type definitions needed to build a workflow.',
'',
`Services to cover: ${input.services.join(', ')}.`,
];
if (input.categories?.length) {
lines.push(`Workflow categories: ${input.categories.join(', ')}.`);
}
lines.push(
'',
'For each service: identify the exact node type(s) with their resource/operation/mode discriminators, record the credential type(s) plus whether a usable credential already exists, and return the relevant node type definitions verbatim. Follow the output contract in your instructions — do not summarize the type definitions.',
);
return lines.join('\n');
}
export function createDiscoverWorkflowContextTool(context: OrchestrationContext) {
return new Tool(ORCHESTRATION_TOOL_IDS.DISCOVER_WORKFLOW_CONTEXT)
.description(
'Run pre-build discovery: spawn a focused sub-agent that inventories the ' +
'nodes and credentials for the given services and returns the relevant ' +
'node type definitions verbatim (it selects the relevant types, it does ' +
'not summarize them). Call this BEFORE loading `workflow-builder` for any ' +
'build touching external services or unfamiliar nodes, then build with the ' +
'returned context. Runs synchronously — act on the result in the same ' +
'turn. Does not build, patch, or run workflows.',
)
.input(discoverWorkflowContextInputSchema)
.output(z.object({ result: z.string() }))
.handler(async (input: DiscoverWorkflowContextInput) => {
const validTools = createToolRegistry();
for (const name of DISCOVERY_TOOL_NAMES) {
const tool = context.domainTools.get(name);
if (tool) {
validTools.set(name, tool);
}
}
if (!validTools.has(DOMAIN_TOOL_IDS.NODES)) {
return {
result:
'Discovery failed: the `nodes` tool is not available, so node/type discovery cannot run.',
};
}
const { result } = await runSyncSubAgent(context, {
role: DISCOVERY_ROLE,
instructions: DISCOVER_WORKFLOW_CONTEXT_PROMPT,
briefing: buildDiscoveryBriefing(input),
validTools,
toolNames: toolRegistryKeys(validTools),
conversationContext: input.conversationContext,
});
return { result };
})
.build();
}

View File

@ -17,6 +17,7 @@ export const ORCHESTRATION_TOOL_IDS = {
CREATE_TASKS: 'create-tasks',
TASK_CONTROL: 'task-control',
DELEGATE: 'delegate',
DISCOVER_WORKFLOW_CONTEXT: 'discover-workflow-context',
EVAL_SETUP_WITH_AGENT: 'eval-setup-with-agent',
EVAL_DATA: 'eval-data',
COMPLETE_CHECKPOINT: 'complete-checkpoint',
@ -39,6 +40,7 @@ export const ORCHESTRATION_TOOL_NAMES = new Set<string>(Object.values(ORCHESTRAT
export const ALWAYS_LOADED_TOOL_NAMES = new Set<string>([
ORCHESTRATION_TOOL_IDS.CREATE_TASKS,
ORCHESTRATION_TOOL_IDS.DELEGATE,
ORCHESTRATION_TOOL_IDS.DISCOVER_WORKFLOW_CONTEXT,
DOMAIN_TOOL_IDS.ASK_USER,
DOMAIN_TOOL_IDS.CREDENTIALS,
DOMAIN_TOOL_IDS.WORKFLOWS,