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 →
{
"_comment": [
"/intel slash command handler \u2014 RAG over the competitive-intel knowledge base.",
"1. Webhook (responseMode=onReceived) returns immediate 200 to Slack to satisfy the 3s ack requirement.",
"2. Code parses the slash-command body (form-encoded): extracts competitor (Pigment|Anaplan|Planful|Drivetrain|Vena) and timeframe (default 90d) from `text`.",
"3. HF embed query -> Supabase RPC match_competitor_intel (with competitor_filter + days_back) -> Claude Sonnet answer (grounded; never hallucinates) -> POST to response_url.",
"4. Empty-result handling: 'No intelligence found for X in last Yd. Last signal: <date or never>.' Never invents content.",
"5. n8n Cloud FREE TIER NOTE: open Config and replace placeholders. No $env references."
],
"name": "/intel \u2014 RAG slash command",
"nodes": [
{
"parameters": {
"content": "## /intel slash command\n\n**\u26a0\ufe0f Before first run:** open **Config** and fill: `anthropic_api_key`, `huggingface_api_key`, `supabase_url`, `supabase_service_role_key`, `slack_signing_secret` (optional, used for request verification).\n\n**Slack app config:** point the slash command at `POST {n8n_base}/webhook/intel`. Slack will POST `application/x-www-form-urlencoded` with `token, team_id, command, text, response_url, user_id, user_name, ...`.\n\n**Usage:** `/intel <competitor>? <timeframe>?`\n- `/intel Pigment last 30 days` \u2014 filter to Pigment, 30-day window\n- `/intel last 14 days` \u2014 all competitors, 14-day window\n- `/intel` \u2014 all competitors, default 90 days\n\n**Flow:** webhook (immediate 200) -> Config -> parse query -> HF embed -> Supabase RPC -> Claude Sonnet -> POST answer to `response_url`. Latency budget: ~3-6s end-to-end.",
"height": 320,
"width": 720
},
"id": "i0000000-0000-0000-0000-0000000000aa",
"name": "\ud83d\udccc /intel overview",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-220,
-120
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "intel",
"responseMode": "onReceived",
"responseData": "noData",
"responseCode": 200,
"options": {
"rawBody": false,
"noResponseBody": true
}
},
"id": "i1111111-1111-1111-1111-111111111111",
"name": "Webhook \u2014 /intel",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-160,
360
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c-aak",
"name": "anthropic_api_key",
"type": "string",
"value": "__ANTHROPIC_API_KEY__"
},
{
"id": "c-hfk",
"name": "huggingface_api_key",
"type": "string",
"value": "__HUGGINGFACE_API_KEY__"
},
{
"id": "c-sbu",
"name": "supabase_url",
"type": "string",
"value": "https://__YOUR_PROJECT__.supabase.co"
},
{
"id": "c-srk",
"name": "supabase_service_role_key",
"type": "string",
"value": "__SUPABASE_SERVICE_ROLE_KEY__"
},
{
"id": "c-em",
"name": "embedding_model",
"type": "string",
"value": "sentence-transformers/all-MiniLM-L6-v2"
},
{
"id": "c-ms",
"name": "llm_model_synthesize",
"type": "string",
"value": "claude-sonnet-4-6"
},
{
"id": "c-lpk",
"name": "langfuse_public_key",
"type": "string",
"value": "__LANGFUSE_PUBLIC_KEY__"
},
{
"id": "c-lsk",
"name": "langfuse_secret_key",
"type": "string",
"value": "__LANGFUSE_SECRET_KEY__"
},
{
"id": "c-lh",
"name": "langfuse_host",
"type": "string",
"value": "https://cloud.langfuse.com"
}
]
},
"options": {}
},
"id": "i2222222-2222-2222-2222-222222222222",
"name": "Config",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
60,
360
]
},
{
"parameters": {
"jsCode": "// Slack slash commands POST application/x-www-form-urlencoded.\n// n8n Webhook node parses form bodies into $json.body when content-type is form-urlencoded.\nconst body = $('Webhook \u2014 /intel').first().json.body || $('Webhook \u2014 /intel').first().json;\nconst text = (body.text || '').trim();\nconst response_url = body.response_url || null;\nconst user_name = body.user_name || 'unknown';\n\n// Parse competitor name (case-insensitive match) + timeframe.\nconst competitors = ['Pigment','Anaplan','Planful','Drivetrain','Vena'];\nlet competitor_filter = null;\nfor (const c of competitors) {\n const re = new RegExp(`\\\\b${c}\\\\b`, 'i');\n if (re.test(text)) { competitor_filter = c; break; }\n}\n\n// Timeframe: 'last N days' | 'last N weeks' | 'last N months' | default 90 days\nlet days_back = 90;\nconst m = text.match(/last\\s+(\\d+)\\s*(day|week|month)s?/i);\nif (m) {\n const n = parseInt(m[1], 10);\n const unit = m[2].toLowerCase();\n days_back = unit === 'day' ? n : unit === 'week' ? n * 7 : n * 30;\n}\n\n// Build the embedding query: drop the parsed competitor + timeframe, keep the rest as semantic intent.\nlet semantic_query = text\n .replace(new RegExp(`\\\\b(${competitors.join('|')})\\\\b`, 'gi'), '')\n .replace(/last\\s+\\d+\\s*(day|week|month)s?/gi, '')\n .replace(/\\s+/g, ' ').trim();\nif (!semantic_query) semantic_query = competitor_filter ? `recent intelligence about ${competitor_filter}` : 'recent competitive intelligence';\n\nif (!response_url) {\n return [{ json: { _hard_error: 'missing_response_url', received: body } }];\n}\n\nreturn [{ json: { competitor_filter, days_back, semantic_query, response_url, user_name, raw_text: text } }];\n"
},
"id": "i3333333-3333-3333-3333-333333333333",
"name": "Code \u2014 parse query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
280,
360
]
},
{
"parameters": {
"method": "POST",
"url": "=https://router.huggingface.co/hf-inference/models/{{ $('Config').first().json.embedding_model || 'sentence-transformers/all-MiniLM-L6-v2' }}/pipeline/feature-extraction",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Config').first().json.huggingface_api_key }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"inputs\": {{ JSON.stringify($json.semantic_query) }},\n \"options\": { \"wait_for_model\": true }\n}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 25000
}
},
"id": "i4444444-4444-4444-4444-444444444444",
"name": "HF \u2014 embed query",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
500,
360
],
"continueOnFail": true,
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 5000
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Config').first().json.supabase_url }}/rest/v1/rpc/match_competitor_intel",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('Config').first().json.supabase_service_role_key }}"
},
{
"name": "Authorization",
"value": "=Bearer {{ $('Config').first().json.supabase_service_role_key }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"query_embedding\": {{ JSON.stringify((() => { const all = $input.all().map(x => x.json); if (all.length === 1 && Array.isArray(all[0]) && Array.isArray(all[0][0])) return all[0][0]; if (all.length === 1 && Array.isArray(all[0])) return all[0]; if (all.every(v => typeof v === 'number')) return all; return all[0]; })()) }},\n \"match_count\": 8,\n \"competitor_filter\": {{ JSON.stringify($('Code \u2014 parse query').first().json.competitor_filter) }},\n \"days_back\": {{ $('Code \u2014 parse query').first().json.days_back }}\n}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 12000
}
},
"id": "i5555555-5555-5555-5555-555555555555",
"name": "Supabase \u2014 RAG search",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
720,
360
],
"continueOnFail": true,
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 3000
},
{
"parameters": {
"jsCode": "// Decide whether to short-circuit with an empty-result Slack message,\n// or proceed to LLM synthesis.\n// v34: tight filter + cap. n8n was occasionally producing very long input lists\n// (probably re-running the node per item from earlier nodes). We now:\n// 1. only count items that look like real hits (competitor_name + content)\n// 2. cap at 8 so the Sonnet body stays small (matches RPC's match_count: 8)\nconst allItems = $input.all().map(i => i.json).filter(Boolean);\nlet rag = allItems;\nif (rag.length === 1 && Array.isArray(rag[0]?.results)) rag = rag[0].results;\nconst q = $('Code \u2014 parse query').first().json;\nconst validHits = rag.filter(r =>\n r && typeof r === 'object' && r.competitor_name && (r.content || '').length > 0\n);\nconst hits = validHits.slice(0, 8);\n\nif (hits.length === 0) {\n // Look up the most recent signal for this competitor (or any) to give the user a real anchor.\n const targetLabel = q.competitor_filter ? `**${q.competitor_filter}**` : 'any competitor';\n const text = `\ud83d\udd0d *No intelligence found for ${targetLabel} in last ${q.days_back} days.*\\nTry a wider window \u2014 e.g. \\`/intel ${q.competitor_filter || 'Pigment'} last 180 days\\`.`;\n return [{ json: {\n short_circuit: true,\n response_url: q.response_url,\n slack_payload: {\n response_type: 'ephemeral',\n text,\n blocks: [\n { type: 'section', text: { type: 'mrkdwn', text } },\n { type: 'context', elements: [\n { type: 'mrkdwn', text: `_Query: \\`${q.raw_text || '(empty)'}\\` \u00b7 filter: ${q.competitor_filter || 'none'} \u00b7 window: ${q.days_back}d_` }\n ] }\n ]\n }\n } }];\n}\n\nreturn [{ json: { short_circuit: false, hits, query: q } }];\n"
},
"id": "i6666666-6666-6666-6666-666666666666",
"name": "Code \u2014 empty-result guard",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
940,
360
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond-sc",
"leftValue": "={{ $json.short_circuit }}",
"rightValue": "={{ true }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "i7777777-7777-7777-7777-777777777777",
"name": "IF \u2014 empty result?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1160,
360
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "x-api-key",
"value": "={{ $('Config').first().json.anthropic_api_key }}"
},
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({\n model: $('Config').first().json.llm_model_synthesize || 'claude-sonnet-4-6',\n max_tokens: 800,\n temperature: 0.2,\n system: \"You are a competitive intelligence analyst answering Slack queries from sales reps. Use ONLY the snippets provided in <retrieved_intel>. Never invent facts. If the snippets don't directly answer the question, say so explicitly and cite what you DO have. British English. Concise \u2014 3-6 short sentences max. End with one '_Source(s):_' line listing competitor + scrape date for each snippet you used. Output plain markdown (Slack mrkdwn dialect), no JSON.\",\n messages: [{ role: 'user', content:\n '<query>\\n' + ($json.query.raw_text || '') +\n '\\n(filter: ' + ($json.query.competitor_filter || 'all competitors') +\n ' \u00b7 window: last ' + ($json.query.days_back || 90) + ' days)\\n</query>\\n\\n' +\n '<retrieved_intel>\\n' + JSON.stringify(($json.hits || []).map(h => ({\n competitor: h.competitor_name,\n type: h.signal_type,\n content: (h.content || '').slice(0, 600),\n source_url: h.source_url,\n scraped_at: h.scraped_at,\n similarity: Number(h.similarity || 0).toFixed(2)\n }))) +\n '\\n</retrieved_intel>\\n\\nAnswer the query. Be specific about which competitor each fact belongs to. If the retrieved intel does not directly answer the question, say so and report what is available instead.'\n }]\n}) }}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "i8888888-8888-8888-8888-888888888888",
"name": "Claude Sonnet \u2014 answer",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1400,
460
],
"continueOnFail": true,
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 30000
},
{
"parameters": {
"jsCode": "// Render the final Slack payload from Sonnet's answer + retrieved hits.\n// v33: defensive hit count (n8n's per-item iteration can inflate hits.length)\n// + substitute Notion battlecard URLs for battlecard sources (which have\n// no source_url and would otherwise render as about:blank links).\nconst llm = $json;\nconst upstream = $('Code \u2014 empty-result guard').first().json;\nconst q = upstream.query || {};\n// Always coerce hits to a real array for safe .length / .slice / .map.\nconst rawHits = upstream.hits;\nconst hits = Array.isArray(rawHits) ? rawHits : (rawHits ? [rawHits] : []);\nconst hitCount = hits.length;\n\nconst BATTLECARD_NOTION_URLS = {\n Pigment: 'https://www.notion.so/354cab25fcdb818ba2fdc0ac249f8515',\n Anaplan: 'https://www.notion.so/354cab25fcdb81c0bfefc1cab7d1e637',\n Planful: 'https://www.notion.so/354cab25fcdb814791f0c4535059021e',\n Drivetrain: 'https://www.notion.so/354cab25fcdb81abaf92d7ee69630f2e',\n Vena: 'https://www.notion.so/354cab25fcdb81f6a99ae7f5f0d86442'\n};\nconst urlFor = (h) => {\n if (h.source_url) return h.source_url;\n if (h.source === 'battlecard' && BATTLECARD_NOTION_URLS[h.competitor_name]) return BATTLECARD_NOTION_URLS[h.competitor_name];\n return 'https://www.notion.so/354cab25fcdb815db9a3d396b412a0ec';\n};\n\nlet answer;\ntry {\n answer = llm?.content?.[0]?.text || null;\n if (!answer) throw new Error('no_content');\n // v35: Slack mrkdwn uses *bold*, not **bold**. Sonnet sometimes emits standard\n // Markdown despite the system prompt; normalise here so the message renders.\n answer = answer\n .replace(/\\*\\*([^*\\n]+)\\*\\*/g, '*$1*')\n .replace(/__([^_\\n]+)__/g, '*$1*');\n} catch (e) {\n answer = '\u26a0\ufe0f Couldn\\'t synthesise an answer right now (LLM error). Showing raw matches below.';\n}\n\nconst headerText = `\ud83d\udd0d */intel ${q.competitor_filter || 'all'}* \u2014 last ${q.days_back} days`;\nconst sourcesContext = hits.slice(0, 3).map(h => `<${urlFor(h)}|${h.competitor_name} \u00b7 ${h.signal_type} \u00b7 ${(h.scraped_at || '').slice(0,10)}>`).join(' \u00b7 ');\n\nconst slack_payload = {\n response_type: 'in_channel',\n text: headerText,\n blocks: [\n { type: 'section', text: { type: 'mrkdwn', text: `*${headerText}*\\n${answer}` } },\n { type: 'context', elements: [\n { type: 'mrkdwn', text: `_Top sources: ${sourcesContext || '(none)'}_` },\n { type: 'mrkdwn', text: `_Asked by @${q.user_name} \u00b7 ${hitCount} hits \u00b7 query: \\`${q.raw_text || '(empty)'}\\`_` }\n ] }\n ]\n};\n\nreturn [{ json: { response_url: q.response_url, slack_payload } }];\n"
},
"id": "i8888888-8888-8888-8888-888888888889",
"name": "Code \u2014 render answer",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1640,
460
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $json.response_url }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.slack_payload) }}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "text"
}
},
"timeout": 10000
}
},
"id": "i9999999-9999-9999-9999-999999999999",
"name": "Slack \u2014 POST response_url",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1880,
360
],
"continueOnFail": true,
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 5000
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Config').first().json.langfuse_host }}/api/public/traces",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Basic {{ Buffer.from($('Config').first().json.langfuse_public_key + ':' + $('Config').first().json.langfuse_secret_key).toString('base64') }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"name\": \"intel_query\",\n \"input\": {{ ($('Claude Sonnet \u2014 answer').first().json.usage && $('Claude Sonnet \u2014 answer').first().json.usage.input_tokens) || 0 }},\n \"output\": {{ ($('Claude Sonnet \u2014 answer').first().json.usage && $('Claude Sonnet \u2014 answer').first().json.usage.output_tokens) || 0 }},\n \"metadata\": {\n \"model\": {{ JSON.stringify($('Claude Sonnet \u2014 answer').first().json.model || $('Config').first().json.llm_model_synthesize) }},\n \"run_id\": {{ JSON.stringify('intel_' + ($('Code \u2014 parse query').first().json.user_name || 'anon')) }},\n \"competitor\": {{ JSON.stringify($('Code \u2014 parse query').first().json.competitor_filter) }},\n \"days_back\": {{ $('Code \u2014 parse query').first().json.days_back }},\n \"hits\": {{ ($('Code \u2014 empty-result guard').first().json.hits || []).length }},\n \"cost_estimate\": {{ ((($('Claude Sonnet \u2014 answer').first().json.usage || {}).input_tokens || 0) * 0.000003) + ((($('Claude Sonnet \u2014 answer').first().json.usage || {}).output_tokens || 0) * 0.000015) }}\n }\n}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 8000
}
},
"id": "lfi00001-0000-0000-0000-00000000000c",
"name": "Langfuse \u2014 log intel query",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1640,
640
],
"continueOnFail": true,
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"retryOnFail": false
}
],
"connections": {
"Webhook \u2014 /intel": {
"main": [
[
{
"node": "Config",
"type": "main",
"index": 0
}
]
]
},
"Config": {
"main": [
[
{
"node": "Code \u2014 parse query",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 parse query": {
"main": [
[
{
"node": "HF \u2014 embed query",
"type": "main",
"index": 0
}
]
]
},
"HF \u2014 embed query": {
"main": [
[
{
"node": "Supabase \u2014 RAG search",
"type": "main",
"index": 0
}
]
]
},
"Supabase \u2014 RAG search": {
"main": [
[
{
"node": "Code \u2014 empty-result guard",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 empty-result guard": {
"main": [
[
{
"node": "IF \u2014 empty result?",
"type": "main",
"index": 0
}
]
]
},
"IF \u2014 empty result?": {
"main": [
[
{
"node": "Slack \u2014 POST response_url",
"type": "main",
"index": 0
}
],
[
{
"node": "Claude Sonnet \u2014 answer",
"type": "main",
"index": 0
}
]
]
},
"Claude Sonnet \u2014 answer": {
"main": [
[
{
"node": "Code \u2014 render answer",
"type": "main",
"index": 0
},
{
"node": "Langfuse \u2014 log intel query",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 render answer": {
"main": [
[
{
"node": "Slack \u2014 POST response_url",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": ""
},
"staticData": null,
"tags": [
{
"name": "scenario-rag"
},
{
"name": "slash-command"
}
],
"triggerCount": 1,
"versionId": "1.0.0"
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
/intel — RAG slash command. Uses httpRequest. Webhook trigger; 12 nodes.
Source: https://github.com/arjitmat/gtm-intelligence-agent/blob/9ed50a103ef002227d109af15515d165c27a3f53/n8n/intel_query_workflow.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.
Jigsaw API key for image processing, I use this as a gatekeeper/second pair of eyes. LINK to their website https://jigsawstack.com/ SECOND A postgress DATABASE (I use Supabase) LlamaCloud for the pars
Onsite Photos to Jobs (SMS Agent). Uses dataTable, twilio, httpRequest, airtable. Webhook trigger; 62 nodes.
W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 50 nodes.
W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 48 nodes.