200 lines
8.9 KiB
Markdown
200 lines
8.9 KiB
Markdown
# Sub-workflow as agent tool
|
|
|
|
The default agent-tool shape for anything beyond one node is the Tool Workflow node (`@n8n/n8n-nodes-langchain.toolWorkflow`). Any sub-workflow becomes a tool the agent calls, with typed inputs filled by `$fromAI()`. It composes with everything good about n8n: branching, error handling, sub-workflow reuse, native nodes, custom logic.
|
|
|
|
For the sub-workflow primitive itself (Execute Workflow Trigger inputs/outputs, stateless design, naming, search-before-build), see **n8n-subworkflows** — this reference only covers the *agent-tool* angle.
|
|
|
|
---
|
|
|
|
## Why this is the default in n8n
|
|
|
|
In raw LangChain a tool is a function. In n8n a tool can be a whole workflow, so it can:
|
|
|
|
- Branch on input (IF / Switch).
|
|
- Call multiple APIs and aggregate.
|
|
- Have its own retries, fallbacks, error handling.
|
|
- Call other sub-workflows.
|
|
- Read/write Data Tables.
|
|
- Be tested independently with `n8n_test_workflow` and pinned data.
|
|
- Be reused across agents AND non-agent workflows.
|
|
|
|
A function-as-tool can't do most of that without growing into a workflow anyway. n8n gives you the workflow primitive directly.
|
|
|
|
---
|
|
|
|
## The shape: two halves
|
|
|
|
### 1. The sub-workflow side — an Execute Workflow Trigger with typed inputs
|
|
|
|
```json
|
|
{
|
|
"parameters": {
|
|
"workflowInputs": {
|
|
"values": [
|
|
{ "name": "imagePrompt", "type": "string" },
|
|
{ "name": "imageName", "type": "string" },
|
|
{ "name": "sessionId", "type": "string" }
|
|
]
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
|
"typeVersion": 1.1,
|
|
"name": "When Executed by Another Workflow"
|
|
}
|
|
```
|
|
|
|
Each declared input becomes a parameter the caller can fill. **The trigger must be in "Define Below" mode (typed fields), not passthrough** — passthrough has no schema, so the agent has nothing to fill via `$fromAI`. Two exceptions: (a) the sub-workflow needs binary (it can't be an agent tool directly — pre-stage to storage and pass storage keys as typed string fields, see **n8n-binary-and-data**), or (b) the tool takes no inputs at all (passthrough is the only option, and the tool's only decision is whether to invoke).
|
|
|
|
Type enforcement happens on the **agent side** via the `type` argument of `$fromAI`, not at the trigger. Allowed types: `string`, `number`, `boolean`, `json`. Match them.
|
|
|
|
### 2. The Tool Workflow side — points at the sub-workflow, binds params
|
|
|
|
```json
|
|
{
|
|
"parameters": {
|
|
"description": "Use to create a new image from a prompt OR edit an existing image. Pass imageName as the storage key (e.g. \"abc123.png\") to edit; leave empty to generate from scratch. Returns { imageUrl, imageKey }.",
|
|
"workflowId": { "__rl": true, "value": "<sub-workflow-id>", "mode": "list" },
|
|
"workflowInputs": {
|
|
"mappingMode": "defineBelow",
|
|
"value": {
|
|
"imagePrompt": "={{ $fromAI('imagePrompt', 'Detailed prompt describing the desired image', 'string') }}",
|
|
"imageName": "={{ $fromAI('imageName', 'Storage key of an existing image to edit, or empty for new generation', 'string') }}",
|
|
"sessionId": "={{ $('Chat Trigger').first().json.sessionId }}"
|
|
},
|
|
"schema": [
|
|
{ "id": "imagePrompt", "displayName": "imagePrompt", "type": "string", "display": true },
|
|
{ "id": "imageName", "displayName": "imageName", "type": "string", "display": true },
|
|
{ "id": "sessionId", "displayName": "sessionId", "type": "string", "display": true }
|
|
]
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
|
|
"typeVersion": 2.2,
|
|
"name": "Generate or edit image"
|
|
}
|
|
```
|
|
|
|
Wire it into the agent with `ai_tool`:
|
|
|
|
```json
|
|
"Generate or edit image": {
|
|
"ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]]
|
|
}
|
|
```
|
|
|
|
The mapping is per-input:
|
|
|
|
- **Agent-filled**: `={{ $fromAI('paramName', 'description', 'string') }}` — the agent decides.
|
|
- **Plumbed**: `={{ $('SourceNode').first().json.field }}` — your workflow fills it.
|
|
|
|
The `sessionId` line is critical: it is **NOT** an agent decision. Plumb it from the trigger so memory and session-keyed work stay consistent. **Never put `sessionId` behind `$fromAI`** — the agent will fabricate a UUID.
|
|
|
|
---
|
|
|
|
## What the agent sees (and doesn't)
|
|
|
|
The agent sees the tool's **name** (the Tool Workflow node's name) and **description** (a parameter on the node) — both follow the **TOOLS.md** rules: specific, API-doc style, treated as prompt.
|
|
|
|
It does **not** see: the sub-workflow internals, the sub-workflow's own name, or plumbed values like `sessionId`. Only `$fromAI` parameters appear in the tool schema. So you can refactor the sub-workflow heavily without changing what the agent sees.
|
|
|
|
---
|
|
|
|
## Worked example: one tool, two modes
|
|
|
|
Goal: an agent that can generate or edit images. Both share most logic; they differ only in whether they download an existing image first.
|
|
|
|
```
|
|
[Execute Workflow Trigger: { imagePrompt, imageName, sessionId }]
|
|
↓
|
|
[Crypto: hash for new filename]
|
|
↓
|
|
[IF: imageName empty?]
|
|
├── empty (generate) → [Gemini: generate] ──┐
|
|
└── not empty (edit): │
|
|
[S3: Download by imageName] │
|
|
↓ │
|
|
[Gemini: edit with downloaded binary] ───────┤
|
|
↓
|
|
[S3: Upload result]
|
|
↓
|
|
[Set: { imageUrl, imageKey }]
|
|
```
|
|
|
|
The agent picks the mode by what it puts in `imageName`. Two near-identical tools would have made selection harder — collapse them.
|
|
|
|
---
|
|
|
|
## Patterns inside the sub-workflow
|
|
|
|
### Return a stable shape (it's a contract)
|
|
|
|
The caller receives whatever the last node outputs. Pick a shape and keep it across modes:
|
|
|
|
```json
|
|
{ "imageUrl": "https://...", "imageKey": "abc123.png" }
|
|
```
|
|
|
|
Don't sometimes return `{ url, key }` and other times `{ result: { url, key } }`. The output shape is a contract every caller depends on — agents read it as part of the prompt, deterministic callers wire downstream nodes to specific paths. Drift breaks callers silently.
|
|
|
|
For calls that fail "expectedly" (search with no results), return a branchable shape:
|
|
|
|
```json
|
|
{ "ok": false, "error": "no_results", "message": "No matches found for query" }
|
|
```
|
|
|
|
### When to throw instead: Stop and Error
|
|
|
|
For unexpected-but-handled errors (auth failure, upstream down, unrecoverable input), use a `Stop and Error` node with a detailed message. It propagates as a thrown error: agents see a tool error and can retry/switch/report; deterministic callers catch it via `onError: 'continueErrorOutput'`. Pick this over `{ ok: false }` when the outcome is a true error, not a normal branch. For the full error story (4xx/5xx mapping, retries, error workflows) → **n8n-error-handling**.
|
|
|
|
### Wire `onError: 'continueErrorOutput'` on fallible nodes
|
|
|
|
Inside the sub-workflow, fallible nodes (HTTP, S3, DB) should set `onError: 'continueErrorOutput'` and route to a clean error response, so both agent and deterministic callers receive a structured error instead of a silent halt.
|
|
|
|
### Treat the input contract as an API and document it
|
|
|
|
The Execute Workflow Trigger's declared inputs ARE this tool's API. Document them in the sub-workflow's `description`:
|
|
|
|
```
|
|
Generates or edits an image.
|
|
Inputs:
|
|
imagePrompt (string, required): detailed image description.
|
|
imageName (string, optional): storage key of existing image to edit. Empty = new generation.
|
|
sessionId (string, required): chat session ID, used for storage keying.
|
|
Returns:
|
|
{ imageUrl, imageKey }
|
|
```
|
|
|
|
### Keep tool sub-workflows discoverable
|
|
|
|
Name them with a standard prefix (`Subworkflow:` or domain-specific). The Tool Workflow node references them by ID (stable), but humans browse the UI by name.
|
|
|
|
---
|
|
|
|
## Testing the sub-workflow independently
|
|
|
|
A sub-workflow tool can be tested without the agent:
|
|
|
|
1. Pin representative input on the Execute Workflow Trigger.
|
|
2. `n8n_test_workflow` runs it with that pinned data.
|
|
3. Verify the output shape matches what the agent will receive.
|
|
|
|
---
|
|
|
|
## When NOT to use sub-workflow as tool
|
|
|
|
- **Simple one-node wrappers** — "call this endpoint and return" is shorter as an HTTP Request Tool.
|
|
- **One-off code-only logic specific to this agent** — a few lines of pure JS/Python that exist nowhere else work fine as a Custom Code Tool (`.toolCode`, see **n8n-code-tool**). Decision rule: reusable business logic → sub-workflow; one-off agent-specific transform → Code Tool.
|
|
- **Capabilities that already exist as native tool nodes** — don't wrap `slackTool` in a sub-workflow.
|
|
|
|
For everything else, sub-workflow as tool is the default.
|
|
|
|
---
|
|
|
|
## Cross-references
|
|
|
|
- The four tool types overview → **TOOLS.md**
|
|
- How `$fromAI` descriptions affect behavior → **TOOLS.md** "`$fromAI()`"
|
|
- The sub-workflow primitive (stateless design, naming, I/O) → **n8n-subworkflows**
|
|
- Passing binary into tools → **n8n-binary-and-data**
|
|
- The Custom Code Tool exception → **n8n-code-tool**
|