opencode-skills-collection 4.0.7 → 4.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/.antigravity-install-manifest.json +19 -1
- package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +15 -6
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/aas-agent-first-control-plane-preview-profile.md +2 -2
- package/bundled-skills/docs/maintainers/aas-agent-first-control-plane-v1-worklog.md +1 -1
- package/bundled-skills/docs/maintainers/release-process.md +15 -0
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/aas-core.md +32 -3
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/docs/vietnamese/README.vi.md +4 -4
- package/bundled-skills/hf-cloud-aws-context-discovery/SKILL.md +96 -0
- package/bundled-skills/markstream-angular/SKILL.md +76 -0
- package/bundled-skills/markstream-custom-components/SKILL.md +74 -0
- package/bundled-skills/markstream-custom-components/references/patterns.md +14 -0
- package/bundled-skills/markstream-install/SKILL.md +3 -1
- package/bundled-skills/markstream-migration/SKILL.md +78 -0
- package/bundled-skills/markstream-migration/references/adoption-checklist.md +10 -0
- package/bundled-skills/markstream-nuxt/SKILL.md +71 -0
- package/bundled-skills/markstream-react/SKILL.md +74 -0
- package/bundled-skills/markstream-svelte/SKILL.md +68 -0
- package/bundled-skills/markstream-vue/SKILL.md +71 -0
- package/bundled-skills/markstream-vue2/SKILL.md +70 -0
- package/bundled-skills/markstream-vue2-cli/SKILL.md +70 -0
- package/bundled-skills/markstream-vue2-vite/SKILL.md +69 -0
- package/bundled-skills/n8n-agents/SKILL.md +301 -0
- package/bundled-skills/n8n-agents/references/CHAT_AGENT_PATTERNS.md +228 -0
- package/bundled-skills/n8n-agents/references/EXAMPLES.md +432 -0
- package/bundled-skills/n8n-agents/references/HUMAN_REVIEW.md +180 -0
- package/bundled-skills/n8n-agents/references/MEMORY.md +139 -0
- package/bundled-skills/n8n-agents/references/RAG.md +102 -0
- package/bundled-skills/n8n-agents/references/STRUCTURED_OUTPUT.md +163 -0
- package/bundled-skills/n8n-agents/references/SUBWORKFLOW_AS_TOOL.md +199 -0
- package/bundled-skills/n8n-agents/references/SYSTEM_PROMPT.md +151 -0
- package/bundled-skills/n8n-agents/references/TOOLS.md +199 -0
- package/bundled-skills/n8n-binary-and-data/SKILL.md +269 -0
- package/bundled-skills/n8n-binary-and-data/references/AGENT_TOOL_BINARY.md +227 -0
- package/bundled-skills/n8n-binary-and-data/references/BINARY_BASICS.md +187 -0
- package/bundled-skills/n8n-binary-and-data/references/CDN_REQUIREMENT.md +109 -0
- package/bundled-skills/n8n-binary-and-data/references/MERGE_FOR_CONTEXT.md +130 -0
- package/bundled-skills/n8n-code-tool/SKILL.md +358 -0
- package/bundled-skills/n8n-code-tool/references/ERROR_PATTERNS.md +178 -0
- package/bundled-skills/n8n-code-tool/references/INPUT_SCHEMA.md +132 -0
- package/bundled-skills/n8n-error-handling/SKILL.md +289 -0
- package/bundled-skills/n8n-error-handling/references/API_WORKFLOWS.md +256 -0
- package/bundled-skills/n8n-error-handling/references/ERROR_WORKFLOWS.md +178 -0
- package/bundled-skills/n8n-error-handling/references/NODE_ERROR_OUTPUTS.md +171 -0
- package/bundled-skills/n8n-error-handling/references/RESPONSE_SHAPES.md +220 -0
- package/bundled-skills/n8n-multi-instance/SKILL.md +208 -0
- package/bundled-skills/n8n-node-configuration/references/NODE_FAMILY_GOTCHAS.md +241 -0
- package/bundled-skills/n8n-subworkflows/SKILL.md +271 -0
- package/bundled-skills/n8n-subworkflows/references/NAMING_AND_DISCOVERY.md +130 -0
- package/bundled-skills/n8n-subworkflows/references/SUBWORKFLOW_PATTERNS.md +147 -0
- package/bundled-skills/using-n8n-mcp-skills/SKILL.md +193 -0
- package/package.json +1 -1
- package/skills_index.json +547 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Three practical node-object snippets for the shell + core + sub-agent topology. These are **community n8n JSON fragments** to adapt, not full importable exports — credential IDs, workflow IDs, and channel/bot IDs are placeholders. Build with `n8n_update_partial_workflow` (`addNode` + `addConnection` on the `ai_*` outputs), then verify with `n8n_get_workflow` and `validate_workflow`.
|
|
4
|
+
|
|
5
|
+
For the architecture these fit into, see **CHAT_AGENT_PATTERNS.md**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Stateless agent core
|
|
10
|
+
|
|
11
|
+
A reusable agent sub-workflow: `chatInput` + `threadId` in, agent output out. Memory keyed on `threadId`, native tools, a sub-agent tool, and Block Kit structured output with an autoFix fixer model. This is the "brain" called by the shell.
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"name": "Chat agent core",
|
|
16
|
+
"nodes": [
|
|
17
|
+
{
|
|
18
|
+
"parameters": {
|
|
19
|
+
"workflowInputs": {
|
|
20
|
+
"values": [{ "name": "chatInput" }, { "name": "threadId" }]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
|
24
|
+
"typeVersion": 1.1,
|
|
25
|
+
"position": [-480, -96],
|
|
26
|
+
"id": "core-trigger",
|
|
27
|
+
"name": "When Executed by Another Workflow"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"parameters": {
|
|
31
|
+
"promptType": "define",
|
|
32
|
+
"text": "={{ $json.chatInput }}",
|
|
33
|
+
"hasOutputParser": true,
|
|
34
|
+
"options": {
|
|
35
|
+
"systemMessage": "=You are a concise, direct assistant. Be a thinking partner, not an answer machine.\n\nCurrent date: {{ $now.format('DDDD') }}\n\n## Output\nYou are replying in Slack using Block Kit. Your entire response must be valid JSON with a 'blocks' array at the root. Bold is *single asterisks*. Links are <https://url|text>. Max 10 blocks.\n\n## Tool usage\nFact-check verifiable claims with the web search tool before answering. Use the idea database manager for anything about content ideas.",
|
|
36
|
+
"maxIterations": 50
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
40
|
+
"typeVersion": 3.1,
|
|
41
|
+
"position": [-48, -96],
|
|
42
|
+
"id": "core-agent",
|
|
43
|
+
"name": "AI Agent"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"parameters": { "model": "anthropic/claude-opus-4.6", "options": { "temperature": 0.1 } },
|
|
47
|
+
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
|
|
48
|
+
"typeVersion": 1,
|
|
49
|
+
"position": [-288, 192],
|
|
50
|
+
"id": "core-main-llm",
|
|
51
|
+
"name": "Main LLM",
|
|
52
|
+
"credentials": { "openRouterApi": { "id": "REPLACE_OPENROUTER_CRED", "name": "OpenRouter" } }
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"parameters": {
|
|
56
|
+
"sessionIdType": "customKey",
|
|
57
|
+
"sessionKey": "={{ $json.threadId }}",
|
|
58
|
+
"contextWindowLength": 50
|
|
59
|
+
},
|
|
60
|
+
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
|
61
|
+
"typeVersion": 1.3,
|
|
62
|
+
"position": [-128, 192],
|
|
63
|
+
"id": "core-memory",
|
|
64
|
+
"name": "Simple Memory"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"parameters": {
|
|
68
|
+
"descriptionType": "manual",
|
|
69
|
+
"toolDescription": "Search the web fast to fact-check a claim or find a source. Use for verifying anything from training data.",
|
|
70
|
+
"query": "={{ $fromAI('query', 'The search query, phrased to match relevant sources', 'string') }}",
|
|
71
|
+
"options": { "search_depth": "fast" }
|
|
72
|
+
},
|
|
73
|
+
"type": "@tavily/n8n-nodes-tavily.tavilyTool",
|
|
74
|
+
"typeVersion": 1,
|
|
75
|
+
"position": [32, 192],
|
|
76
|
+
"id": "core-web-search",
|
|
77
|
+
"name": "Search the web",
|
|
78
|
+
"credentials": { "tavilyApi": { "id": "REPLACE_TAVILY_CRED", "name": "Tavily" } }
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"parameters": {},
|
|
82
|
+
"type": "@n8n/n8n-nodes-langchain.toolCalculator",
|
|
83
|
+
"typeVersion": 1,
|
|
84
|
+
"position": [192, 192],
|
|
85
|
+
"id": "core-calc",
|
|
86
|
+
"name": "Calculator"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"parameters": {
|
|
90
|
+
"description": "Manages the content-ideas database. Use for ANY task about content ideas: querying, creating, dedupe-checks.\n\nIMPORTANT: This tool is stateless. Send all relevant context in a single message. If creating, include ALL required fields upfront. Returns the page URL for anything referenced or created.",
|
|
91
|
+
"workflowId": { "__rl": true, "value": "REPLACE_SUBAGENT_WF_ID", "mode": "list", "cachedResultName": "Notion ideas sub-agent" },
|
|
92
|
+
"workflowInputs": {
|
|
93
|
+
"mappingMode": "defineBelow",
|
|
94
|
+
"value": { "chatInput": "={{ $fromAI('chatInput', 'The full request to the ideas database, with all context', 'string') }}" },
|
|
95
|
+
"schema": [
|
|
96
|
+
{ "id": "chatInput", "displayName": "chatInput", "type": "string", "display": true, "canBeUsedToMatch": true }
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
|
|
101
|
+
"typeVersion": 2.2,
|
|
102
|
+
"position": [352, 192],
|
|
103
|
+
"id": "core-idea-tool",
|
|
104
|
+
"name": "Idea database manager"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"parameters": {
|
|
108
|
+
"schemaType": "manual",
|
|
109
|
+
"inputSchema": "{ \"type\": \"object\", \"properties\": { \"text\": { \"type\": \"string\" }, \"blocks\": { \"type\": \"array\", \"items\": { \"oneOf\": [ { \"type\": \"object\", \"properties\": { \"type\": { \"const\": \"header\" }, \"text\": { \"type\": \"object\" } }, \"required\": [\"type\", \"text\"] }, { \"type\": \"object\", \"properties\": { \"type\": { \"const\": \"section\" }, \"text\": { \"type\": \"object\" } }, \"required\": [\"type\", \"text\"] }, { \"type\": \"object\", \"properties\": { \"type\": { \"const\": \"divider\" } }, \"required\": [\"type\"] } ] } } }, \"required\": [\"text\", \"blocks\"] }",
|
|
110
|
+
"autoFix": true
|
|
111
|
+
},
|
|
112
|
+
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
|
|
113
|
+
"typeVersion": 1.3,
|
|
114
|
+
"position": [560, 176],
|
|
115
|
+
"id": "core-parser",
|
|
116
|
+
"name": "Structured Output Parser (Block Kit)"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"parameters": { "model": "anthropic/claude-sonnet-4.6", "options": { "temperature": 0 } },
|
|
120
|
+
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
|
|
121
|
+
"typeVersion": 1,
|
|
122
|
+
"position": [620, 336],
|
|
123
|
+
"id": "core-fixer-llm",
|
|
124
|
+
"name": "Fixer LLM (coding-capable)",
|
|
125
|
+
"credentials": { "openRouterApi": { "id": "REPLACE_OPENROUTER_CRED", "name": "OpenRouter" } }
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"connections": {
|
|
129
|
+
"When Executed by Another Workflow": { "main": [[{ "node": "AI Agent", "type": "main", "index": 0 }]] },
|
|
130
|
+
"Main LLM": { "ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]] },
|
|
131
|
+
"Simple Memory": { "ai_memory": [[{ "node": "AI Agent", "type": "ai_memory", "index": 0 }]] },
|
|
132
|
+
"Search the web": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] },
|
|
133
|
+
"Calculator": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] },
|
|
134
|
+
"Idea database manager": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] },
|
|
135
|
+
"Structured Output Parser (Block Kit)": { "ai_outputParser": [[{ "node": "AI Agent", "type": "ai_outputParser", "index": 0 }]] },
|
|
136
|
+
"Fixer LLM (coding-capable)": { "ai_languageModel": [[{ "node": "Structured Output Parser (Block Kit)", "type": "ai_languageModel", "index": 0 }]] }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
What to notice:
|
|
142
|
+
|
|
143
|
+
- **Memory keyed on `threadId`**, not on a user/channel ID (those cross conversations). The shell supplies `threadId`.
|
|
144
|
+
- **`maxIterations: 50`** — raised from the low default because this agent chains several tools per turn.
|
|
145
|
+
- **`$now.format('DDDD')`** in the system prompt — no hardcoded date.
|
|
146
|
+
- **Two models**: the main model on the agent, a separate coding-capable fixer wired into the parser. Both connect via `ai_languageModel` but to different nodes.
|
|
147
|
+
- **`hasOutputParser: true`** on the agent activates the `ai_outputParser` slot.
|
|
148
|
+
- The sub-agent tool's description repeats **"This tool is stateless"** — the router can't rely on shared context.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 2. Slack router shell
|
|
153
|
+
|
|
154
|
+
The "shell": trigger, trigger-level anti-loop filter, event-type Switch, loading reaction, the agent-core call with an error branch, and the Block Kit reply envelope. No LLM here.
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"name": "Slack chat router",
|
|
159
|
+
"nodes": [
|
|
160
|
+
{
|
|
161
|
+
"parameters": {
|
|
162
|
+
"trigger": ["message"],
|
|
163
|
+
"watchWorkspace": true,
|
|
164
|
+
"options": { "userIds": "={{ [\"U00000000BOT\"] }}" }
|
|
165
|
+
},
|
|
166
|
+
"type": "n8n-nodes-base.slackTrigger",
|
|
167
|
+
"typeVersion": 1,
|
|
168
|
+
"position": [-288, 48],
|
|
169
|
+
"id": "shell-trigger",
|
|
170
|
+
"name": "Slack Trigger",
|
|
171
|
+
"credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"parameters": {
|
|
175
|
+
"rules": {
|
|
176
|
+
"values": [
|
|
177
|
+
{
|
|
178
|
+
"conditions": {
|
|
179
|
+
"options": { "version": 3 },
|
|
180
|
+
"conditions": [{ "leftValue": "={{ $json.user === \"U00000000OWNER\" && $json.type === \"message\" }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }],
|
|
181
|
+
"combinator": "and"
|
|
182
|
+
},
|
|
183
|
+
"renameOutput": true, "outputKey": "Owner message"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"conditions": {
|
|
187
|
+
"options": { "version": 3 },
|
|
188
|
+
"conditions": [{ "leftValue": "={{ $json.user !== \"U00000000OWNER\" && $json.type === \"message\" }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }],
|
|
189
|
+
"combinator": "and"
|
|
190
|
+
},
|
|
191
|
+
"renameOutput": true, "outputKey": "Unknown user"
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"type": "n8n-nodes-base.switch",
|
|
197
|
+
"typeVersion": 3.4,
|
|
198
|
+
"position": [-32, 48],
|
|
199
|
+
"id": "shell-switch",
|
|
200
|
+
"name": "Switch"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"parameters": {
|
|
204
|
+
"resource": "reaction",
|
|
205
|
+
"channelId": { "__rl": true, "value": "={{ $json.channel }}", "mode": "id" },
|
|
206
|
+
"timestamp": "={{ $json.ts }}",
|
|
207
|
+
"name": "spinner"
|
|
208
|
+
},
|
|
209
|
+
"type": "n8n-nodes-base.slack",
|
|
210
|
+
"typeVersion": 2.4,
|
|
211
|
+
"position": [240, -64],
|
|
212
|
+
"id": "shell-add-reaction",
|
|
213
|
+
"name": "Add Loading Reaction",
|
|
214
|
+
"credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"parameters": {
|
|
218
|
+
"workflowId": { "__rl": true, "value": "REPLACE_AGENT_CORE_WF_ID", "mode": "list", "cachedResultName": "Chat agent core" },
|
|
219
|
+
"workflowInputs": {
|
|
220
|
+
"mappingMode": "defineBelow",
|
|
221
|
+
"value": {
|
|
222
|
+
"chatInput": "={{ $('Slack Trigger').item.json.text }}",
|
|
223
|
+
"threadId": "={{ $('Slack Trigger').item.json.thread_ts || $('Slack Trigger').item.json.ts }}"
|
|
224
|
+
},
|
|
225
|
+
"schema": [
|
|
226
|
+
{ "id": "chatInput", "displayName": "chatInput", "type": "string", "display": true },
|
|
227
|
+
{ "id": "threadId", "displayName": "threadId", "type": "string", "display": true }
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
"type": "n8n-nodes-base.executeWorkflow",
|
|
232
|
+
"typeVersion": 1.3,
|
|
233
|
+
"position": [480, -64],
|
|
234
|
+
"id": "shell-call-core",
|
|
235
|
+
"name": "Call Agent core",
|
|
236
|
+
"retryOnFail": true,
|
|
237
|
+
"maxTries": 2,
|
|
238
|
+
"waitBetweenTries": 5000,
|
|
239
|
+
"onError": "continueErrorOutput"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"parameters": {
|
|
243
|
+
"resource": "reaction",
|
|
244
|
+
"operation": "remove",
|
|
245
|
+
"channelId": { "__rl": true, "value": "={{ $('Switch').item.json.channel }}", "mode": "id" },
|
|
246
|
+
"timestamp": "={{ $('Switch').item.json.ts }}",
|
|
247
|
+
"name": "spinner"
|
|
248
|
+
},
|
|
249
|
+
"type": "n8n-nodes-base.slack",
|
|
250
|
+
"typeVersion": 2.4,
|
|
251
|
+
"position": [720, -160],
|
|
252
|
+
"id": "shell-remove-reaction-ok",
|
|
253
|
+
"name": "Remove Loading Reaction (success)",
|
|
254
|
+
"credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"parameters": {
|
|
258
|
+
"select": "user",
|
|
259
|
+
"user": { "__rl": true, "value": "={{ $('Slack Trigger').item.json.user }}", "mode": "id" },
|
|
260
|
+
"messageType": "block",
|
|
261
|
+
"blocksUi": "={{ { \"blocks\": $('Call Agent core').item.json.output.blocks } }}",
|
|
262
|
+
"otherOptions": {
|
|
263
|
+
"thread_ts": { "replyValues": { "thread_ts": "={{ $('Slack Trigger').item.json.thread_ts || $('Slack Trigger').item.json.ts }}" } }
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
"type": "n8n-nodes-base.slack",
|
|
267
|
+
"typeVersion": 2.4,
|
|
268
|
+
"position": [960, -160],
|
|
269
|
+
"id": "shell-send-reply",
|
|
270
|
+
"name": "Send Block Kit reply",
|
|
271
|
+
"credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"parameters": {
|
|
275
|
+
"select": "user",
|
|
276
|
+
"user": { "__rl": true, "value": "={{ $('Slack Trigger').item.json.user }}", "mode": "id" },
|
|
277
|
+
"text": "=There was a workflow error. https://<your-n8n-host>/workflow/<this-workflow-id>/executions/{{ $execution.id }}",
|
|
278
|
+
"otherOptions": {
|
|
279
|
+
"thread_ts": { "replyValues": { "thread_ts": "={{ $('Slack Trigger').item.json.thread_ts || $('Slack Trigger').item.json.ts }}" } }
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
"type": "n8n-nodes-base.slack",
|
|
283
|
+
"typeVersion": 2.4,
|
|
284
|
+
"position": [720, 64],
|
|
285
|
+
"id": "shell-send-error",
|
|
286
|
+
"name": "Send error message with execution link",
|
|
287
|
+
"credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
"connections": {
|
|
291
|
+
"Slack Trigger": { "main": [[{ "node": "Switch", "type": "main", "index": 0 }]] },
|
|
292
|
+
"Switch": { "main": [[{ "node": "Add Loading Reaction", "type": "main", "index": 0 }], []] },
|
|
293
|
+
"Add Loading Reaction": { "main": [[{ "node": "Call Agent core", "type": "main", "index": 0 }]] },
|
|
294
|
+
"Call Agent core": {
|
|
295
|
+
"main": [
|
|
296
|
+
[{ "node": "Remove Loading Reaction (success)", "type": "main", "index": 0 }],
|
|
297
|
+
[{ "node": "Send error message with execution link", "type": "main", "index": 0 }]
|
|
298
|
+
]
|
|
299
|
+
},
|
|
300
|
+
"Remove Loading Reaction (success)": { "main": [[{ "node": "Send Block Kit reply", "type": "main", "index": 0 }]] }
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
What to notice:
|
|
306
|
+
|
|
307
|
+
- **Anti-loop at the trigger**: `options.userIds: ["U00000000BOT"]` is an exclusion list — the bot's own posts never enter the workflow. No separate filter node needed.
|
|
308
|
+
- **`Call Agent core`** has `onError: 'continueErrorOutput'`, so `main[1]` carries the error branch (→ **n8n-error-handling**). The loading reaction is removed on the success path; the error branch surfaces a link instead of hanging forever.
|
|
309
|
+
- **`threadId`** = `thread_ts || ts`, plumbed straight to the core (which keys memory on it).
|
|
310
|
+
- **`blocksUi`** is the `{ "blocks": [...] }` envelope, not the bare array — the bare array fails silently.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 3. Domain sub-agent (Notion ideas)
|
|
315
|
+
|
|
316
|
+
A specialist sub-agent called via `.toolWorkflow` from the core. It fetches its DB schema fresh on every call and runs on a cheaper model than the router.
|
|
317
|
+
|
|
318
|
+
```json
|
|
319
|
+
{
|
|
320
|
+
"name": "Notion ideas sub-agent",
|
|
321
|
+
"nodes": [
|
|
322
|
+
{
|
|
323
|
+
"parameters": { "workflowInputs": { "values": [{ "name": "chatInput" }] } },
|
|
324
|
+
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
|
325
|
+
"typeVersion": 1.1,
|
|
326
|
+
"position": [-240, 0],
|
|
327
|
+
"id": "sub-trigger",
|
|
328
|
+
"name": "When Executed by Another Workflow"
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"parameters": {
|
|
332
|
+
"resource": "database",
|
|
333
|
+
"databaseId": { "__rl": true, "value": "REPLACE_NOTION_DB_ID", "mode": "id" },
|
|
334
|
+
"simple": false
|
|
335
|
+
},
|
|
336
|
+
"type": "n8n-nodes-base.notion",
|
|
337
|
+
"typeVersion": 2.2,
|
|
338
|
+
"position": [-32, 0],
|
|
339
|
+
"id": "sub-get-db",
|
|
340
|
+
"name": "Get a database",
|
|
341
|
+
"credentials": { "notionApi": { "id": "REPLACE_NOTION_CRED", "name": "Notion" } }
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"parameters": {
|
|
345
|
+
"promptType": "define",
|
|
346
|
+
"text": "={{ $('When Executed by Another Workflow').item.json.chatInput }}",
|
|
347
|
+
"options": {
|
|
348
|
+
"systemMessage": "=You manage a Notion ideas database. Query and create idea entries.\n\n## Database schema (fetched fresh this call)\n{{ $('Get a database').first().json.properties.toJsonString() }}\n\n## Rules\n1. Always respond in chat with the result.\n2. Always return the Notion URL for any page created or referenced.\n3. Select/multi-select values must EXACTLY match an existing schema option.\n4. IMPORTANT: you are stateless. If information is missing, list exactly what's needed and remind the caller to resend the complete request with all details.",
|
|
349
|
+
"maxIterations": 15
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
353
|
+
"typeVersion": 3.1,
|
|
354
|
+
"position": [208, 0],
|
|
355
|
+
"id": "sub-agent",
|
|
356
|
+
"name": "AI Agent"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"parameters": { "model": "anthropic/claude-haiku-4.6", "options": { "temperature": 0.1 } },
|
|
360
|
+
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
|
|
361
|
+
"typeVersion": 1,
|
|
362
|
+
"position": [112, 256],
|
|
363
|
+
"id": "sub-llm",
|
|
364
|
+
"name": "Sub-agent LLM (cheaper than router)",
|
|
365
|
+
"credentials": { "openRouterApi": { "id": "REPLACE_OPENROUTER_CRED", "name": "OpenRouter" } }
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"parameters": {
|
|
369
|
+
"descriptionType": "manual",
|
|
370
|
+
"toolDescription": "Returns all ideas that are still active (not rejected, cancelled, or started).",
|
|
371
|
+
"resource": "databasePage",
|
|
372
|
+
"operation": "getAll",
|
|
373
|
+
"databaseId": { "__rl": true, "value": "REPLACE_NOTION_DB_ID", "mode": "id" },
|
|
374
|
+
"returnAll": true,
|
|
375
|
+
"filterType": "manual",
|
|
376
|
+
"filters": { "conditions": [{ "key": "Status|status", "condition": "does_not_equal", "statusValue": "Rejected" }] }
|
|
377
|
+
},
|
|
378
|
+
"type": "n8n-nodes-base.notionTool",
|
|
379
|
+
"typeVersion": 2.2,
|
|
380
|
+
"position": [304, 256],
|
|
381
|
+
"id": "sub-get-active",
|
|
382
|
+
"name": "Get active ideas",
|
|
383
|
+
"credentials": { "notionApi": { "id": "REPLACE_NOTION_CRED", "name": "Notion" } }
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"parameters": {
|
|
387
|
+
"descriptionType": "manual",
|
|
388
|
+
"toolDescription": "Creates an idea entry. Always enters as status 'Idea'. Select fields must match schema options exactly.",
|
|
389
|
+
"resource": "databasePage",
|
|
390
|
+
"databaseId": { "__rl": true, "value": "REPLACE_NOTION_DB_ID", "mode": "id" },
|
|
391
|
+
"title": "={{ $fromAI('Title', 'Short title of the idea', 'string') }}",
|
|
392
|
+
"propertiesUi": {
|
|
393
|
+
"propertyValues": [
|
|
394
|
+
{ "key": "Status|status", "statusValue": "Idea" },
|
|
395
|
+
{ "key": "Type|select", "selectValue": "={{ $fromAI('type', 'Type column; must EXACTLY match a schema option', 'string') }}" }
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
"type": "n8n-nodes-base.notionTool",
|
|
400
|
+
"typeVersion": 2.2,
|
|
401
|
+
"position": [480, 256],
|
|
402
|
+
"id": "sub-create",
|
|
403
|
+
"name": "Create idea",
|
|
404
|
+
"credentials": { "notionApi": { "id": "REPLACE_NOTION_CRED", "name": "Notion" } }
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
"connections": {
|
|
408
|
+
"When Executed by Another Workflow": { "main": [[{ "node": "Get a database", "type": "main", "index": 0 }]] },
|
|
409
|
+
"Get a database": { "main": [[{ "node": "AI Agent", "type": "main", "index": 0 }]] },
|
|
410
|
+
"Sub-agent LLM (cheaper than router)": { "ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]] },
|
|
411
|
+
"Get active ideas": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] },
|
|
412
|
+
"Create idea": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
What to notice:
|
|
418
|
+
|
|
419
|
+
- **Fresh schema injection**: `Get a database` runs **before** the agent (on `main`), and its `properties` are templated into the system prompt with `.toJsonString()`. The sub-agent never operates on a stale schema, so it can't pick a select option that was renamed last week.
|
|
420
|
+
- **Cheaper model** (`claude-haiku-4.6`) than the router — a focused single-domain agent doesn't need the orchestrator's model.
|
|
421
|
+
- **Stateless contract** restated in the system prompt — matching the tool description on the core side.
|
|
422
|
+
- **`maxIterations: 15`** — fine for a focused sub-agent (vs 50 on the broad router).
|
|
423
|
+
- The `Status|status` / `Type|select` key shape is Notion's `Name|type` convention; match the live schema.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Cross-references
|
|
428
|
+
|
|
429
|
+
- The topology these fit into → **CHAT_AGENT_PATTERNS.md**
|
|
430
|
+
- The `.toolWorkflow` mapping → **SUBWORKFLOW_AS_TOOL.md**
|
|
431
|
+
- Block Kit schema and autoFix → **STRUCTURED_OUTPUT.md**
|
|
432
|
+
- Error branch on the core call → **n8n-error-handling**
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Human review for agent tools
|
|
2
|
+
|
|
3
|
+
Human review gates a tool behind explicit human approval. Until a human approves, the wrapped tool does not run — no matter how confident the agent is. This is the default safety pattern for any agent tool with user-visible side effects.
|
|
4
|
+
|
|
5
|
+
n8n names this **HITL** / human-in-the-loop in the node IDs (`slackHitlTool`, `discordHitlTool`, …) and "Human Review" in the UI. Same concept.
|
|
6
|
+
|
|
7
|
+
**Before adding or skipping review, ask the user.** Whether sign-off is needed is a product/policy call (blast radius, audit requirements, how much they trust the model). Surface the question, recommend based on the criteria below, and let them decide.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Topology
|
|
12
|
+
|
|
13
|
+
The review node sits **between** the wrapped tool and the agent on the `ai_tool` connection:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
[wrapped tool] --ai_tool--> [review node] --ai_tool--> [Agent]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **The agent doesn't know the review node is there.** It sees the wrapped tool by the wrapped tool's name, description, and parameter schema. The review node is a transparent intercept on the execution path.
|
|
20
|
+
- When the agent calls the wrapped tool, the review node intercepts: collects the parameters the agent built, pauses, sends an approval prompt to a human, and only on approval does the wrapped tool run with those parameters.
|
|
21
|
+
|
|
22
|
+
In workflow JSON, the wrapped tool's `ai_tool` output points at the **review node**, and the review node's `ai_tool` output points at the **agent**:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
"Refund customer": {
|
|
26
|
+
"ai_tool": [[{ "node": "Slack approval", "type": "ai_tool", "index": 0 }]]
|
|
27
|
+
},
|
|
28
|
+
"Slack approval": {
|
|
29
|
+
"ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Do NOT wire the wrapped tool into the agent's `main` input — that flags the wrapped tool as a disconnected node in `validate_workflow`. The wrapped-tool-into-review wiring happens through `ai_tool` only.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Tell the agent the review is there
|
|
38
|
+
|
|
39
|
+
Because the agent doesn't see the review node, it doesn't know its tool is gated. Models with safety priors hedge on destructive-looking tools (send, delete, refund, charge): they refuse, ask the user for confirmation first, or pick a less-direct option. With review wrapping the tool, that caution doubles up — the model self-censors AND a human reviews, and sometimes the model never even reaches the review step.
|
|
40
|
+
|
|
41
|
+
If you see the agent over-hedging on a wrapped tool, add a note to the **wrapped tool's description** (per the modular-prompt principle in **SYSTEM_PROMPT.md**):
|
|
42
|
+
|
|
43
|
+
> This tool is gated by a human review step. Use it freely when relevant. A human will see the exact parameters and approve before anything is sent. Don't ask the user for confirmation first.
|
|
44
|
+
|
|
45
|
+
Don't pre-emptively add this to every wrapped tool — many agents use the tool freely without it. Deploy when the symptom (hedging, refusing, talking itself out of trying) actually shows up.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## When to default to / recommend human review
|
|
50
|
+
|
|
51
|
+
- **Sends, pays, refunds, account changes** — anything user-visible and hard to roll back.
|
|
52
|
+
- **The approver differs from the chatter** — a customer triggers a workflow; support staff approves the refund. The customer never sees the approval.
|
|
53
|
+
- **Non-chat triggers** — order received, form submitted, schedule fired. The action is taken on someone's behalf, and a person approves before it runs.
|
|
54
|
+
- **Production agent tools** where the cost of a wrong call (money, trust, reputation) outweighs a one-step delay.
|
|
55
|
+
|
|
56
|
+
Skip review when the tool is read-only, idempotent and cheap to undo, or the deployment is internal/exploratory with mocked services.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Available review tool nodes
|
|
61
|
+
|
|
62
|
+
| Node | When to use |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `n8n-nodes-base.slackHitlTool` | Approver is on Slack (the common multi-channel case) |
|
|
65
|
+
| `n8n-nodes-base.discordHitlTool` | Approver is on Discord |
|
|
66
|
+
| `n8n-nodes-base.telegramHitlTool` | Approver is on Telegram |
|
|
67
|
+
| `n8n-nodes-base.gmailHitlTool` | Approval via Gmail |
|
|
68
|
+
| `n8n-nodes-base.emailSendHitlTool` | Approval via generic SMTP email |
|
|
69
|
+
| `n8n-nodes-base.googleChatHitlTool` | Approval in Google Chat |
|
|
70
|
+
| `n8n-nodes-base.microsoftOutlookHitlTool` | Approval via Outlook |
|
|
71
|
+
|
|
72
|
+
More platforms are added over time — verify with `search_nodes({ query: 'hitl' })`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Response types
|
|
77
|
+
|
|
78
|
+
`responseType` chooses the response shape the human sees:
|
|
79
|
+
|
|
80
|
+
- **`approval`** — button-based, sub-configured via `approvalOptions.values.approvalType`:
|
|
81
|
+
- `'single'` (default): one Approve button. The approver acts or ignores.
|
|
82
|
+
- `'double'`: Approve / Disapprove. For actions where disapproval should be a loud, recordable choice.
|
|
83
|
+
- **`freeText`** — the human types a free-form response. For when the agent is genuinely asking a question and any answer is valid.
|
|
84
|
+
- **`customForm`** — a multi-field form (text, dropdown, radio, checkbox, file). **This is the practical answer to "editable parameters"**: define a form whose fields match the wrapped tool's parameters and the human can override what the agent picked.
|
|
85
|
+
|
|
86
|
+
A two-button "semantic choice" ("Schedule today" / "Schedule tomorrow") is NOT a separate type — use `approval` with `approvalType: 'double'` and custom `approveLabel` / `disapproveLabel`.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Wait timeout
|
|
91
|
+
|
|
92
|
+
`options.limitWaitTime` (seconds) bounds how long the workflow pauses before erroring out. Default is 45 minutes. **Set it explicitly on production workflows** — without it, paused executions sit indefinitely if approvers don't act, and the queue piles up.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Approval message content — show the ACTUAL parameters
|
|
97
|
+
|
|
98
|
+
The model picked the parameters; the human approves the literal call. Reference the real values via `{{ $tool.parameters.<name> }}`:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
The agent wants to refund {{ $tool.parameters.amount }} to {{ $tool.parameters.customerId }}.
|
|
102
|
+
Reason: {{ $tool.parameters.reason }}.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`$tool.name` is the wrapped tool's display name; `$tool.parameters` is the full object the agent built. To avoid silently leaving a new parameter out of the message, iterate over all of them:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
The agent wants to call {{ $tool.name }}:
|
|
109
|
+
{{
|
|
110
|
+
$tool.parameters.keys()
|
|
111
|
+
.map(param => `${param}: ${$tool.parameters[param]}\n`)
|
|
112
|
+
.join('')
|
|
113
|
+
}}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Never fill the approval message via `$fromAI()`
|
|
117
|
+
|
|
118
|
+
`$fromAI()` asks the *model* to produce a value — including, if you let it, the approval text itself. The human would then approve a model-paraphrased description instead of the literal parameters about to be sent. That defeats the entire point of review.
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
// ❌ WRONG — the model paraphrases what it's about to do
|
|
122
|
+
message: ={{ $fromAI('approvalText', 'describe the action for approval') }}
|
|
123
|
+
|
|
124
|
+
// ✅ RIGHT — the literal call is visible
|
|
125
|
+
message: =Refund {{ $tool.parameters.amount }} to {{ $tool.parameters.customerId }}?
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Put values in the button labels
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
"approvalOptions": {
|
|
132
|
+
"values": {
|
|
133
|
+
"approvalType": "double",
|
|
134
|
+
"approveLabel": "=Approve {{ $tool.parameters.amount }} refund",
|
|
135
|
+
"disapproveLabel": "Cancel"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A button that says "Approve $50 refund" is unambiguous; "Approve" alone is not. `slackHitlTool` also exposes `buttonApprovalStyle` / `buttonDisapprovalStyle` (`'primary' | 'secondary'`) for visual emphasis.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Multi-channel pattern: the approver isn't the chatter
|
|
145
|
+
|
|
146
|
+
A common production shape: a customer chats with an agent on a website (or via email/order/form), and support staff approves sensitive actions in Slack.
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
[customer chat / order trigger]
|
|
150
|
+
→ [Agent]
|
|
151
|
+
→ [Slack review tool] → [refund / cancel / escalate tool]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The customer never sees the Slack channel. The Slack review message routes via `slackHitlTool.parameters.user` (a resource locator). On approval, the wrapped tool fires and the agent's response goes back to the customer via the original path. This works without any chat at all — the trigger can be a webhook, schedule, form, or queue; the review tool is the only human-facing surface.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Editable parameters: use customForm
|
|
159
|
+
|
|
160
|
+
For "approve, but at $40 instead of $50" workflows, use `responseType: 'customForm'`. The human fills a multi-field form whose values feed the wrapped tool. Don't try to build editable approvals on top of the `approval` type — the form mode is the supported path.
|
|
161
|
+
|
|
162
|
+
> Note: the form mode UX is reported to feel like a workaround. Sometimes it's better UX to have the user decline and respond with the change in chat.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## UI quirk: test-data autofill
|
|
167
|
+
|
|
168
|
+
When building a review tool, click "Approve" once on the canvas test execution. n8n autofills the test data so subsequent runs work without manual input. New builders often think the tool is broken because `$tool.parameters.<name>` shows red — that's just missing test data.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Anti-patterns
|
|
173
|
+
|
|
174
|
+
| Anti-pattern | What goes wrong | Fix |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| Tool that mutates user-visible state without review | Agent fires irreversible action on a wrong inference | Wrap with the right review tool node |
|
|
177
|
+
| Approval message via `$fromAI()` | You approve a paraphrase, not the literal call | Use `$tool.parameters.<name>` |
|
|
178
|
+
| "Approve" button with no context | Approver clicks without seeing what they approve | Embed actual values in the label |
|
|
179
|
+
| Review on a channel the approver doesn't watch | Tool sits indefinitely, executions pile up | Pick a watched channel; set `limitWaitTime` + a fallback |
|
|
180
|
+
| Wrapped tool wired into the agent's `main` input | Flags as a disconnected node in validation | Wire wrapped-tool → review → agent via `ai_tool` only |
|