📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-07-22 10:09:47 +00:00
parent 316c012df0
commit 60364c6660
353 changed files with 24740 additions and 1264 deletions
@@ -0,0 +1,130 @@
# Naming and discovery
A sub-workflow nobody can find gets rebuilt. The community MCP can't read, write, or filter by tags — tags are a UI-only concept — so the **only searchable surface is the workflow's name and description**, via `n8n_list_workflows` (scan the library) and `n8n_get_workflow` (read a candidate's inputs/outputs and body). That makes naming the discovery mechanism, not a cosmetic nicety. Put your discovery hooks in the name and description deliberately.
---
## Tags don't help here
n8n has tags in the UI, but the MCP can't see them. Don't rely on tags for AI-side discovery — anything you want re-found later has to be findable by name or description.
---
## The naming convention is the discovery mechanism
Use verb-first prefix names. The prefix groups the library; the verb + object says what it does:
```
Subworkflow: <verb> <object> # stateless, generic, reusable anywhere
<Domain>: <verb> <object> # domain-specific (Customer, Billing, Notification, …)
Tool: <description> # exposed as an AI-agent tool
```
Examples:
- `Subworkflow: Parse RFC2822 date`
- `Subworkflow: Compute MRR from subscription`
- `Subworkflow: Format invoice as HTML`
- `Customer: hydrate from Stripe`
- `Customer: write to billing table`
- `Billing: compute MRR`
- `Notification: send + log`
- `Tool: list available credentials`
Why this works when the only search is name/description matching:
- Scanning the list for `Subworkflow:` surfaces every reusable sub-workflow.
- Scanning for `Customer:` surfaces every customer-domain sub-workflow.
- Scanning for `Tool:` surfaces every agent-callable tool.
- Scanning for `date` surfaces anything with "date" in its name or description, regardless of prefix.
Put a prefix on **every** sub-workflow, at create time. It's far easier than retrofitting once callers exist.
---
## Search-before-build, in practice
Before writing logic for a generic problem, scan the library:
```
n8n_list_workflows() # then filter the results by name
n8n_get_workflow({ id: "<candidate>" }) # read description + inputs/outputs + body
```
When to look: any time you're about to build something that fits a domain or an operation keyword. About to parse a date? Look for `date`. Format an invoice? `invoice`. Send a Slack notification? `Slack` and `Notification`. Two scans is cheap; a duplicate is not.
If a candidate matches, fetch it with `n8n_get_workflow` and read the `description` first — that's the contract. If the inputs/outputs fit, use it. If it's close-but-not-quite, decide whether to extend the existing one or build a deliberate variant (and name the variant so *it* is findable too).
If you expected to find a workflow and it isn't showing up, the most common cause isn't naming — it's that the workflow isn't exposed to the MCP at all. Confirm it exists and is reachable before assuming it's missing.
---
## The description as a discoverability tool
After a name match, the reader reads the `description`. Make it scan well — what it does, the output shape, the typical caller:
```
Parses an RFC2822-formatted date string into ISO format.
Returns { ok: true, iso: "..." } or { ok: false, error: "invalid_format" }.
Used by webhook handlers that receive email-style timestamps.
```
The description also feeds name/description matching, so seed it with representative keywords ("RFC2822", "date", "ISO", "webhook") so varied scans surface it. A sub-workflow with no description forces the reader to open and inspect every node to figure out what it is — which usually ends in them rebuilding it.
---
## Naming at create time
Set the name and description when you create the workflow, not later:
```
n8n_update_partial_workflow({
id: "<new workflow id>",
operations: [
{ type: "updateSettings", /* name + description carried on the workflow object */ }
]
})
```
In practice you'll set `name` and `description` on the workflow when you create it, then add the trigger and body nodes via `addNode` / `addConnection`. The point is: don't let a new sub-workflow ship without the prefix and a real description.
---
## What a healthy library looks like
Roughly:
- 520 `Subworkflow:` entries for common shapes (date parsing, ID generation, formatting…).
- A handful of domain sub-workflows per main domain (`Customer:`, `Billing:`, `Notification:`).
- Fewer per-domain "operations" sub-workflows (write to billing table, send email + log).
Counter-signals:
- **100 sub-workflows** → likely lots of near-duplicates to merge.
- **0 sub-workflows** → no extraction; logic is being duplicated inline.
- **50 entries named `Helper`, `Util1`, `Helper2`** → discoverability is broken. Rename to the prefix convention.
When the user asks "what sub-workflows do we have?", scan with `n8n_list_workflows`, filter by prefix, and return a list with each name plus a one-line summary pulled from its description. That's also a good moment to spot duplicates and propose consolidating.
---
## Cross-project sub-workflows
On Cloud or project-enabled instances, sub-workflows live inside a project, and by default a workflow can only call sub-workflows in its own project. Sharing across projects is opt-in.
Only share cross-project when **both** hold:
- **Stateless** — no project-scoped credentials, Data Tables, or other state that wouldn't make sense outside the owning project.
- **Generic problem** — date parsing, ID generation, signature validation, formatting. Clearly not coupled to one project's domain.
A stateful sub-workflow (`Customer: get by id`) shared across projects would pull one project's data into another's workflows, which is almost never intended. Keep those in-project and let each project own its repository layer. For ones that meet the bar, tell the user — they share via the n8n UI — and note the cross-project intent in the description.
---
## Renaming and reorganizing
For duplicates or poorly-named sub-workflows:
- **Renaming preserves the workflow ID**, so existing Execute Workflow callers (which reference the ID, not the name) keep working. The new name shows up in scans immediately.
- n8n has no alias mechanism — just rename, update any sticky-note references inside callers, and move on.
- For a mass rename, audit callers first: `n8n_list_workflows` to find candidates, then `n8n_get_workflow` on each to check its Execute Workflow node for the old workflow ID before you touch anything.
@@ -0,0 +1,147 @@
# Sub-workflow patterns
Three n8n-specific patterns that don't fall out of the "should this be a sub-workflow?" decision tree: choosing `mode: all` vs `each`, splitting one capability into N+1 sub-workflows when its input contracts diverge, and using fire-and-forget to get real parallelism.
---
## `mode: all` vs `each`
The caller's Execute Workflow node has a `mode` that controls how items reach the sub-workflow.
| `mode` | Sub-workflow runs | Items per run |
|---|---|---|
| `all` (default) | once | all N items, flowing through nodes per-item as usual |
| `each` | N times | exactly one item per run |
For a body that just processes items the ordinary way — map, filter, transform — the two are equivalent, because n8n nodes iterate per-item regardless of how many items arrived.
The split matters in exactly one situation: **the body assumes it sees exactly one item.** Three telltales:
- **Per-run aggregation.** A node like "sum these line items" or "build one report from these rows" produces a single output from whatever items it sees. Under `mode: all` it sees all N inputs and produces *one* aggregate across everyone. Under `mode: each` it runs N times and produces one aggregate *per input* — which is almost always what a per-customer / per-order body means.
- **"This is THE thing to act on" logic.** A body written around a single entity (`$json.customer_id`, "send this one email") silently operates on only the first item, or mis-aggregates, when handed N at once.
- **A final write that should fire once per input.** An insert/update meant to run once per record fires once total under `all`.
### Worked contrast
A sub-workflow `Customer: build monthly summary` whose body groups orders and emits one summary row.
- **Called with `mode: all`** on 50 customers' orders → the grouping node sees all orders at once and emits *one* summary blending all 50 customers. Wrong.
- **Called with `mode: each`** → 50 runs, each handed one customer's orders, each emitting that customer's summary. Right.
### Prefer `each` over an internal Loop Over Items
When you need per-item iteration, let the caller's `mode: each` do it rather than dropping a **Loop Over Items** node inside the sub-workflow. Reasons:
- The body stays single-item and simple — no batch-cursor logic, no cross-iteration state to manage.
- The contract reads as "give me one item, I act on it", which is also exactly the agent-tool contract.
- You avoid the classic SplitInBatches gotchas (see **n8n-code-javascript**) inside a workflow that's supposed to be a clean function.
Reach for an internal loop only when iteration is genuinely part of the body's own job (e.g. paginating an API until exhausted), not when it's just "do this body once per input".
---
## Splitting by input shape
**Principle:** when one capability has multiple input paths whose contracts *genuinely* differ, split into one outer sub-workflow per contract, all calling a shared downstream sub-workflow for the common work.
The forcing function is structural in n8n: on a single Execute Workflow Trigger, **passthrough** (required for binary, and the only option when the sub-workflow takes no inputs) and **Define Below** (required for typed inputs that agents and structured callers can fill) are mutually exclusive. You can't have both on one trigger, so divergent contracts can't share one cleanly.
Common cases where contracts genuinely differ:
- **Binary vs non-binary input** (the canonical one — typed fields are JSON-only).
- **Sync vs async paths** with different return contracts.
- **Different auth schemes per path.**
If the body opens with a top-level IF/Switch on *which input shape arrived*, that branch is the seam where two sub-workflows want to separate.
### The reflexive mistake
Faced with two divergent input shapes, the reflex is:
1. Pick passthrough (most permissive — it supports binary).
2. Branch internally on a flag.
3. Accept the loss of typed inputs.
Why it's wrong:
- The workflow can't be exposed as a clean agent tool — passthrough has no `$fromAI` schema.
- Body-shape branches accumulate ("in case A this field is set, in case B it's empty…").
- A future third input shape means *more* branching, not a clean third sub-workflow.
### The fix: N+1 sub-workflows
For N divergent input contracts, build **N+1** sub-workflows: one *outer* per contract, plus one *shared downstream* for the common work. Each outer does its input-specific prep — validation, fetching, normalization, hashing, extraction — and calls the shared core with a normalized shape. The shared core has a single typed input contract and knows nothing about which outer called it.
### Worked example
A "process this paper" capability that arrives either as an external ID *or* as a user-uploaded PDF:
```
Subworkflow: Process Paper from External ID
Trigger: Define Below { arxivId: string, source: string }
→ [validate ID, dedup, fetch metadata, download PDF, extract text]
→ [Execute Workflow → "Subworkflow: Summarize and Store Paper"]
with { arxivId, title, authors, body, source, ... }
Subworkflow: Process Paper from Uploaded PDF
Trigger: Passthrough (required — binary flows through)
→ [hash binary for a synthetic ID, dedup, extract text]
→ [Execute Workflow → "Subworkflow: Summarize and Store Paper"]
with { arxivId: "<synthetic>", title, body, source: "upload", ... }
Subworkflow: Summarize and Store Paper ← the shared core
Trigger: Define Below { arxivId, title, body, source, ... }
→ [LLM with structured output → Data Table insert → Return result]
```
The "pull" path (look up by ID) and the "push" path (data already in hand, here as binary) each get their own typed-or-passthrough trigger, and converge on one typed core. Add a third input shape later and you add a third outer — not a third branch.
The pattern generalizes: any time a capability has both a pull path (look up by ID) and a push path (caller already holds the data, including binary or a template), the split applies. For the binary-handling specifics, see **n8n-binary-and-data**; for wiring the typed outer as an agent tool, **n8n-agents**.
---
## Fire-and-forget parallelization
`mode: each` + `options.waitForSubWorkflow: false` is the only way to get genuinely concurrent sub-workflow execution in n8n. N input items dispatch N sub-workflow runs that execute in parallel (bounded by per-instance concurrency limits).
The catch: the caller doesn't know when — or whether — any of them finished. So this only works with a **separate completion-tracking mechanism**, typically a Data Table the sub-workflow writes to as it progresses (manage it with `n8n_manage_datatable` — see **n8n-mcp-tools-expert**).
### The pattern
1. **Stage.** Insert one "in progress" row per parallel job, keyed by a run ID + a per-job sub-key.
2. **Dispatch.** Call Execute Workflow with `mode: each` and `options.waitForSubWorkflow: false`. The caller continues immediately.
3. **Each sub-workflow.** Does its work, then updates *its* row — `status: completed` / `error`, plus output.
4. **Poll.** The caller enters a loop:
- Get all rows for this run ID.
- If all rows are in a terminal status → exit and aggregate.
- Else if the runtime cap is exceeded → mark the rest `timeout` and exit.
- Else → Wait N seconds, loop back to the Get.
```
[Source: N items]
→ [Data Table: insert N rows, status = "inProgress"]
→ [Execute Workflow] # mode: each, waitForSubWorkflow: false
→ [Data Table: get rows for this run]
→ [IF all terminal?]
├── Yes → continue, aggregate
└── No → [IF under runtime cap?]
├── Yes → [Wait N s] → loop back to the Get
└── No → [update remaining rows → "timeout"] → continue
```
If a sub-workflow crashes without updating its row, the poll sees `inProgress` past the runtime cap and times it out — so a dead job can't hang the loop forever.
### When it earns its place
- **Long per-item work** (LLM calls, large media, slow APIs) where serial would take hours.
- **Independent jobs** that can each complete or fail without affecting the others.
- **You can afford eventual consistency** — the poll loop adds latency by design.
### When it's the wrong tool
- **Short per-item work** (under a second or two): default per-item iteration is simpler.
- **Latency doesn't matter:** the extra complexity and fragility isn't worth it.
- **Jobs depend on each other's output:** use sequential `mode: each` with `waitForSubWorkflow: true` instead.
- **Strict ordering matters:** parallel dispatch gives up ordering.
Pair the per-job error handling (the row's `error` status) with **n8n-error-handling** so a failed job is recorded, not just silently absent.