This workflow follows the Execute Workflow Trigger → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "aicp-notify",
"name": "Notify (channel-abstracted)",
"meta": {
"description": "Reusable notifier. Call via Execute Workflow with { severity, title, message, source }. Routes to Slack / Teams / Email / generic webhook based on the NOTIFY_CHANNEL env var, so callers never hardcode a channel."
},
"nodes": [
{
"parameters": {
"workflowInputs": {
"values": [
{
"name": "severity",
"type": "string"
},
{
"name": "title",
"type": "string"
},
{
"name": "message",
"type": "string"
},
{
"name": "source",
"type": "string"
},
{
"name": "action",
"type": "string"
},
{
"name": "metric",
"type": "string"
},
{
"name": "runUrl",
"type": "string"
}
]
}
},
"id": "aaaaaaaa-0000-0000-0000-000000000001",
"name": "When called",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
200,
300
]
},
{
"parameters": {
"jsCode": "// Assemble the payload once, here, in the shape the contract requires\n// (ai-standards/references/notification-taxonomy.md \u2014 \"The message contract\").\n// Downstream branches only reshape it to each platform's wire format.\n//\n// BLUF: title states the OUTCOME, then the action line, then the one number,\n// then supporting detail, then provenance small and last. The reader is scanning\n// a phone and deciding in ~2s whether this needs them.\nconst i = $input.first().json || {};\nconst NL = String.fromCharCode(10);\n\nconst severity = (i.severity || 'info').toLowerCase();\nconst title = (i.title || 'Notification').trim();\nconst detail = (i.message || '').trim();\nconst source = i.source || 'n8n';\nconst metric = (i.metric || '').trim();\n\n// Severity is DEFINED, not felt \u2014 same three words everywhere, or the colours\n// stop meaning anything. Each maps to exactly one reader obligation.\nconst OBLIGATION = { error: 'Act now', warn: 'Decide this week', info: 'No action' };\n// A caller may state the action explicitly (\"Act by 2026-08-24\"); otherwise it is\n// derived, so a lane that forgets still emits a contract-shaped message rather\n// than leaving the reader to infer urgency from tone.\nconst action = (i.action || '').trim() || OBLIGATION[severity] || 'No action';\nconst emoji = severity === 'error' ? '\\uD83D\\uDD34'\n : severity === 'warn' ? '\\uD83D\\uDFE1'\n : '\\uD83D\\uDFE2';\n\n// Detail lives behind a link \u2014 but assume the link is never opened. Most of our\n// detail surfaces are localhost-bound and unreachable from the phone the alert\n// was read on, so the MESSAGE must carry the decision and the link only carries\n// the evidence.\nconst base = ($env.N8N_BASE_URL || 'http://localhost:5678').replace(/\\/+$/, '');\nconst runUrl = i.runUrl || (base + '/home/executions');\nconst runLabel = i.runUrl ? 'View run' : 'Open executions';\n\n// Slack hard limits: header plain_text 150, section text 3000. Truncate loudly\n// rather than letting Slack reject the whole message.\nconst clip = (s, n) => (s.length > n ? s.slice(0, n - 20) + NL + '\u2026 (truncated)' : s);\n\nconst blocks = [\n { type: 'header',\n text: { type: 'plain_text', text: clip(`${emoji} ${title}`, 150), emoji: true } },\n { type: 'section',\n text: { type: 'mrkdwn', text: `*${action}*` } },\n];\n\nif (metric) {\n blocks.push({ type: 'section', fields: [\n { type: 'mrkdwn', text: `*Key figure*${NL}${clip(metric, 600)}` },\n { type: 'mrkdwn', text: `*Severity*${NL}\\`${severity}\\`` },\n ] });\n}\nif (detail) {\n blocks.push({ type: 'section', text: { type: 'mrkdwn', text: clip(detail, 2800) } });\n}\n// Provenance last and small. Naming the lane is what lets a noisy alert be\n// traced to a workflow and fixed rather than muted.\nblocks.push({ type: 'context', elements: [\n { type: 'mrkdwn', text: `lane: \\`${source}\\` \u00b7 <${runUrl}|${runLabel}>` } ]});\n\n// Fallback text: what a phone notification shows, and what Teams/webhook use.\n// It carries the decision on its own \u2014 title plus action, nothing else.\nconst text = `${emoji} ${title} \u2014 ${action}`;\n// Long form for channels without Block Kit.\nconst longText = [`${emoji} *${title}* (${severity})`, `*${action}*`,\n metric, detail, `_lane: ${source} \u00b7 ${runUrl}_`]\n .filter(Boolean).join(NL);\n\nreturn [{ json: { severity, title, message: detail, source, action, metric,\n runUrl, text, longText, blocks } }];"
},
"id": "aaaaaaaa-0000-0000-0000-000000000002",
"name": "Format message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
420,
300
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $env.NOTIFY_CHANNEL }}",
"rightValue": "slack",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "slack"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $env.NOTIFY_CHANNEL }}",
"rightValue": "teams",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "teams"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $env.NOTIFY_CHANNEL }}",
"rightValue": "webhook",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"outputKey": "webhook"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "aaaaaaaa-0000-0000-0000-000000000003",
"name": "Route by NOTIFY_CHANNEL",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
640,
300
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SLACK_WEBHOOK_URL }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ text: $json.text, blocks: $json.blocks }) }}",
"options": {}
},
"id": "aaaaaaaa-0000-0000-0000-000000000004",
"name": "Slack",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
140
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.TEAMS_WEBHOOK_URL }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { \"type\": \"message\", \"attachments\": [ { \"contentType\": \"application/vnd.microsoft.card.adaptive\", \"content\": { \"type\": \"AdaptiveCard\", \"version\": \"1.4\", \"body\": [ { \"type\": \"TextBlock\", \"weight\": \"Bolder\", \"text\": $json.title }, { \"type\": \"TextBlock\", \"wrap\": true, \"text\": $json.message }, { \"type\": \"TextBlock\", \"isSubtle\": true, \"spacing\": \"None\", \"text\": ('severity: ' + $json.severity + ' \\u00b7 source: ' + $json.source) } ] } } ] } }}",
"options": {}
},
"id": "aaaaaaaa-0000-0000-0000-000000000005",
"name": "Teams",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
300
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.NOTIFY_WEBHOOK_URL }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { \"severity\": $json.severity, \"title\": $json.title, \"message\": $json.message, \"source\": $json.source } }}",
"options": {}
},
"id": "aaaaaaaa-0000-0000-0000-000000000006",
"name": "Generic webhook",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
460
]
}
],
"connections": {
"When called": {
"main": [
[
{
"node": "Format message",
"type": "main",
"index": 0
}
]
]
},
"Format message": {
"main": [
[
{
"node": "Route by NOTIFY_CHANNEL",
"type": "main",
"index": 0
}
]
]
},
"Route by NOTIFY_CHANNEL": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
],
[
{
"node": "Teams",
"type": "main",
"index": 0
}
],
[
{
"node": "Generic webhook",
"type": "main",
"index": 0
}
],
[
{
"node": "Generic webhook",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {},
"active": false
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Notify (channel-abstracted). Uses executeWorkflowTrigger, httpRequest. Event-driven trigger; 6 nodes.
Source: https://github.com/jgobuilds/ai-control-plane-public/blob/main/n8n-workflows/notify.subworkflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
02_LLM_Pipeline v1.0. Uses executeWorkflowTrigger, httpRequest, seaTable. Event-driven trigger; 65 nodes.
This template is a powerful, reusable utility for managing stateful, long-running processes. It allows a main workflow to be paused indefinitely at "checkpoints" and then be resumed by external, async
Upload files from any source to your account Kommo or AmoCRM with a simple and reusable workflow. It can split a large file into small ones and upload chunks. Works for Kommo and amoCRM There are 3 re
Remixed Backup your workflows to GitHub from Solomon's work. Check out his templates.
Remixed Backup your workflows to GitHub from Solomon's work. Check out his templates.