Back to N8n Mcp

Sub-workflow as agent tool

data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md

2.63.18.9 KB
Original Source

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