This workflow corresponds to n8n.io template #16537 — we link there as the canonical source.
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": "0IwIAf8uZWzk1LZq",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Answer BigQuery data questions in Microsoft Teams with Claude dual agents",
"tags": [],
"nodes": [
{
"id": "2bd40180-cb0a-4807-8daf-b8047d03cd06",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
9600,
-5504
],
"parameters": {
"width": 480,
"height": 736,
"content": "## Answer BigQuery data questions in Microsoft Teams with Claude dual agents\n\n### How it works\n\nThis workflow turns Microsoft Teams messages into BigQuery analysis using a dual-Claude agent pattern. It validates the incoming question, checks or refreshes BigQuery schema context, uses a main Claude agent to classify the request, then uses a SQL-focused Claude sub-agent for analytical questions. Query results are converted into a plain-English answer and posted back to Teams, while non-analytical requests and workflow errors receive separate Teams replies.\n\n### Setup steps\n\n- \u26a0\ufe0f **CRITICAL SECURITY:** Restrict the Google Cloud Service Account IAM roles to `BigQuery Data Viewer` and `BigQuery Job User` ONLY. Do not use a highly privileged account. The LLM cannot be trusted to self-police destructive queries.\n- Configure Microsoft Teams trigger and posting credentials, including the team/channel or chat where questions and replies should be handled.\n- Configure Google BigQuery credentials with permission to read INFORMATION_SCHEMA metadata and execute queries.\n- Configure Anthropic credentials for the Claude nodes.\n- Update the \"Configure Variables\" node with your BigQuery project ID and dataset allowlist."
},
"typeVersion": 1
},
{
"id": "ab808953-89fe-41cf-969a-748bdbc4ef4d",
"name": "Sticky Note13",
"type": "n8n-nodes-base.stickyNote",
"position": [
9600,
-4704
],
"parameters": {
"color": 3,
"width": 480,
"height": 316,
"content": "## \u26a0\ufe0f [PRODUCTION NOTE] Distributed Environments\n\nThis template uses n8n's `$getWorkflowStaticData()` to cache the BigQuery schema for 24 hours. This is perfect for single-instance setups and saves BigQuery API calls.\n\n**Running n8n with worker nodes (Queue Mode)?**\nSwap the schema caching logic to read/write from a dedicated Key-Value store (like Upstash Redis or Supabase) to ensure state synchronization across all distributed workers."
},
"typeVersion": 1
},
{
"id": "ef0dfeb8-fd7a-4fdb-b2be-8ec9adc75529",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
10128,
-5504
],
"parameters": {
"color": 7,
"width": 448,
"height": 464,
"content": "## Receive Teams question\n\nCaptures a new Microsoft Teams message and extracts the user request into a validated format for downstream processing."
},
"typeVersion": 1
},
{
"id": "7ad6bd29-34b6-4962-b6bc-907368648b3a",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
10624,
-5504
],
"parameters": {
"color": 7,
"width": 416,
"height": 464,
"content": "## Check schema cache\n\nLooks for previously cached BigQuery schema context and branches based on whether the cache can be reused."
},
"typeVersion": 1
},
{
"id": "cd163d4a-adbf-4915-81f2-100090009540",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
11088,
-5504
],
"parameters": {
"color": 7,
"width": 640,
"height": 464,
"content": "## Refresh BigQuery schema\n\nBuilds and runs the metadata query when the schema cache is missing, then compiles the returned BigQuery schema into LLM-ready context."
},
"typeVersion": 1
},
{
"id": "5c673aae-8cc5-4a0d-9fb8-8ca85445944e",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
11760,
-5504
],
"parameters": {
"color": 7,
"width": 400,
"height": 464,
"content": "## Validate schema context\n\nEnsures schema context is available from either the cache-hit path or the refreshed metadata path before prompting the main agent."
},
"typeVersion": 1
},
{
"id": "dd4d6bc1-d87f-4055-9758-9bfdd8d44dd7",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
12224,
-5504
],
"parameters": {
"color": 7,
"width": 576,
"height": 464,
"content": "## Run main Claude agent\n\nPrepares the main conversational prompt with the user request and schema context, then asks Claude to interpret and classify the request."
},
"typeVersion": 1
},
{
"id": "b3b84df6-9c30-403f-8b3b-a6fe10949c04",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
12864,
-5504
],
"parameters": {
"color": 7,
"width": 464,
"height": 464,
"content": "## Classify agent response\n\nParses Claude's main-agent output and routes the workflow depending on whether the message is an analytical data question."
},
"typeVersion": 1
},
{
"id": "b18052ee-e5e0-4131-9281-07dae42d36e7",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
13376,
-5104
],
"parameters": {
"color": 7,
"width": 816,
"height": 336,
"content": "## Handle non-analytical request\n\nBuilds a friendly non-analytical reply and posts it back to Microsoft Teams without running BigQuery."
},
"typeVersion": 1
},
{
"id": "de7ea659-5649-42a4-af0b-6eff73f4d49f",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
13376,
-5504
],
"parameters": {
"color": 7,
"width": 800,
"height": 352,
"content": "## Generate SQL with Claude\n\nCreates the SQL-agent prompt, invokes a Claude sub-agent specialized for SQL generation, and validates/extracts the resulting query."
},
"typeVersion": 1
},
{
"id": "454cd699-3dfc-47f9-b60d-9ada91b452a3",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
14224,
-5504
],
"parameters": {
"color": 7,
"width": 496,
"height": 352,
"content": "## Execute and collect results\n\nRuns the validated SQL against BigQuery and gathers the returned rows into a structured result payload."
},
"typeVersion": 1
},
{
"id": "3298100e-3829-49d0-a6f9-5e204d950ca8",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
14768,
-5504
],
"parameters": {
"color": 7,
"width": 528,
"height": 352,
"content": "## Draft final answer\n\nPrepares a result-summary prompt and uses Claude to convert the query output into a plain-English answer."
},
"typeVersion": 1
},
{
"id": "16b3f13f-2f30-408c-8457-c6dbcd4729f7",
"name": "Sticky Note11",
"type": "n8n-nodes-base.stickyNote",
"position": [
15344,
-5504
],
"parameters": {
"color": 7,
"width": 480,
"height": 352,
"content": "## Post analytical answer\n\nFormats the generated answer as a Teams Adaptive Card and sends it to the user or channel."
},
"typeVersion": 1
},
{
"id": "682a4a4a-b928-44b6-b009-0bfb78908bf6",
"name": "Sticky Note12",
"type": "n8n-nodes-base.stickyNote",
"position": [
10128,
-4992
],
"parameters": {
"color": 7,
"width": 704,
"height": 320,
"content": "## Report workflow errors\n\nSeparate lower-left error-handling cluster that catches workflow failures, formats an error reply, and posts it to Microsoft Teams."
},
"typeVersion": 1
},
{
"id": "ca64aa7e-eb33-432a-bdc4-56815241d547",
"name": "When Teams Message Received",
"type": "n8n-nodes-base.microsoftTeamsTrigger",
"notes": "Listens for new messages in the configured Teams channel.",
"position": [
10192,
-5296
],
"parameters": {
"teamId": "YOUR_TEAMS_TEAM_ID",
"channelId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"typeVersion": 1
},
{
"id": "e5e312e5-7819-435c-8a8d-4a493aa09ffa",
"name": "Validate Teams Message",
"type": "n8n-nodes-base.code",
"position": [
10416,
-5296
],
"parameters": {
"jsCode": "const body = $input.first().json;\nconst messageText = body.body?.content || body.text || '';\nconst cleanText = messageText.replace(/<[^>]*>/g, '').replace(/ /g, ' ').trim();\nif (!cleanText || cleanText.length < 3) throw new Error('Message too short or empty');\n\n// HUB UPDATE: Prevent the agent from responding to itself or other bots, which causes infinite API billing loops.\nconst fromUser = body.from?.user || {};\nconst fromApp = body.from?.application;\nconst isBot = fromUser.userType === 'bot' || fromUser.userType === 'application' || !!fromApp;\nif (isBot) throw new Error('Ignoring bot/application message to prevent loop');\n\nreturn [{ json: {\n userQuestion: cleanText,\n userId: body.from?.user?.id || 'unknown',\n userName: body.from?.user?.displayName || 'User',\n channelId: body.channelIdentity?.channelId || body.chatId || '',\n teamId: body.channelIdentity?.teamId || '',\n messageId: body.id || '',\n timestamp: body.createdDateTime || new Date().toISOString()\n} }];"
},
"typeVersion": 2
},
{
"id": "244cdca5-9d94-4fc0-85b2-946646aa3e27",
"name": "Configure Variables",
"type": "n8n-nodes-base.code",
"notes": "HUB UPDATE: Centralized configuration for easy deployment.",
"position": [
10656,
-5296
],
"parameters": {
"jsCode": "const data = $input.first().json;\n\n// \u2699\ufe0f TEMPLATE CONFIGURATION\n// Set your Google Cloud and BigQuery details here.\n// These variables are injected into downstream SQL processes.\n\nreturn [{\n json: {\n ...data,\n GCP_PROJECT_ID: 'your-gcp-project-id',\n BQ_DATASET_ID: 'your-bq-dataset-id'\n }\n}];"
},
"typeVersion": 2
},
{
"id": "a062d5da-99be-4393-b088-62c03f5fa10b",
"name": "Check Schema Cache Validity",
"type": "n8n-nodes-base.code",
"notes": "Checks n8n Static Data for cached schema (TTL 24h). On hit, skips BigQuery INFORMATION_SCHEMA fetch.",
"position": [
10912,
-5296
],
"parameters": {
"jsCode": "const CACHE_TTL_MS = 24 * 60 * 60 * 1000;\nconst staticData = $getWorkflowStaticData('global');\nconst userMessage = $input.first().json;\nconst now = Date.now();\nconst cacheValid = staticData.schemaCache && staticData.schemaCachedAt && (now - staticData.schemaCachedAt) < CACHE_TTL_MS;\nif (cacheValid) {\n return [{ json: { ...userMessage, schemaText: staticData.schemaCache, tableCount: staticData.schemaTableCount || 0, columnCount: staticData.schemaColumnCount || 0, schemaFromCache: true } }];\n}\nreturn [{ json: { ...userMessage, schemaText: null, schemaFromCache: false } }];"
},
"typeVersion": 2
},
{
"id": "c9a1bf94-ff9c-4e95-b093-4b925852aaf6",
"name": "If Cache Hit",
"type": "n8n-nodes-base.if",
"notes": "true branch = cache hit, passes through to Validate Schema Present. false branch = cache miss, fetches from BigQuery.",
"position": [
11120,
-5296
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "equal"
},
"leftValue": "={{ $json.schemaFromCache }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "faf93e06-64eb-484b-9ad0-8a13456928e4",
"name": "Create Schema Query Statement",
"type": "n8n-nodes-base.code",
"notes": "Builds the INFORMATION_SCHEMA query string using values from Configure Variables.",
"position": [
11344,
-5216
],
"parameters": {
"jsCode": "const projectId = $('Configure Variables').first().json.GCP_PROJECT_ID;\nconst datasetId = $('Configure Variables').first().json.BQ_DATASET_ID;\nconst schemaQuery = [\n 'SELECT table_name, column_name, data_type, is_nullable,',\n \"'' AS description\",\n 'FROM `' + projectId + '.' + datasetId + '.INFORMATION_SCHEMA.COLUMNS`',\n 'ORDER BY table_name, ordinal_position'\n].join(' ');\nreturn [{ json: { ...$input.first().json, schemaQuery } }];"
},
"typeVersion": 2
},
{
"id": "4ee5d904-cb8b-421d-a937-c1ba6c52c5ad",
"name": "Fetch Schema from BigQuery",
"type": "n8n-nodes-base.googleBigQuery",
"notes": "maxResults: 5000 caps INFORMATION_SCHEMA rows before n8n memory limits are hit.",
"position": [
11568,
-5216
],
"parameters": {
"options": {
"location": "EU",
"maxResults": 5000
},
"sqlQuery": "{{ $json.schemaQuery }}",
"projectId": "={{ $('Configure Variables').first().json.GCP_PROJECT_ID }}"
},
"typeVersion": 2
},
{
"id": "2673ed58-2da3-4595-8960-9f8e7a516edf",
"name": "Compile Schema Data for LLM",
"type": "n8n-nodes-base.code",
"notes": "Builds schema text and writes to 24h static cache. Throws fatal error if payload blows out context window.",
"position": [
11824,
-5216
],
"parameters": {
"jsCode": "const schemaRows = $input.all().map(i => i.json);\nconst userMessage = $('Check Schema Cache Validity').first().json;\nconst tableMap = {};\nfor (const row of schemaRows) {\n const tbl = row.table_name;\n if (!tableMap[tbl]) tableMap[tbl] = [];\n tableMap[tbl].push({ column: row.column_name, type: row.data_type, nullable: row.is_nullable === 'YES', description: row.description || '' });\n}\nlet schemaText = Object.entries(tableMap).map(([table, cols]) => {\n const colLines = cols.map(c => ` - ${c.column} (${c.type})${c.nullable ? '' : ' NOT NULL'}${c.description ? ': ' + c.description : ''}`).join('\\n');\n return ` Table: ${table}\\n${colLines}`;\n}).join('\\n\\n');\n\nconst SCHEMA_CHAR_LIMIT = 40000;\nif (schemaText.length > SCHEMA_CHAR_LIMIT) {\n throw new Error(`Schema payload exceeds context window limit (${schemaText.length} > 40,000 chars). Restrict your dataset allowlist in the Configure Variables node to prevent LLM hallucinations.`);\n}\n\nconst tableCount = Object.keys(tableMap).length;\nconst columnCount = schemaRows.length;\nconst staticData = $getWorkflowStaticData('global');\nstaticData.schemaCache = schemaText;\nstaticData.schemaCachedAt = Date.now();\nstaticData.schemaTableCount = tableCount;\nstaticData.schemaColumnCount = columnCount;\nreturn [{ json: { ...userMessage, schemaText, tableCount, columnCount, schemaFromCache: false } }];"
},
"typeVersion": 2
},
{
"id": "abd2d0d3-97e9-47e7-894d-ca33674fdf80",
"name": "Ensure Schema Availability",
"type": "n8n-nodes-base.code",
"notes": "Guards schema presence before LLM invocation.",
"position": [
12048,
-5312
],
"parameters": {
"jsCode": "const data = $input.first().json;\nif (!data.schemaText) throw new Error('Schema text is missing \u2014 check Check Schema Cache and Compile Schema for LLM nodes.');\nreturn [{ json: data }];"
},
"typeVersion": 2
},
{
"id": "918ea44b-4dff-4382-8203-ef1f662b4930",
"name": "Build Main Agent Query Prompt",
"type": "n8n-nodes-base.code",
"position": [
12320,
-5312
],
"parameters": {
"jsCode": "const ctx = $input.first().json;\nconst prompt = `You are an intent classifier and query planner for a BigQuery data assistant.\\n\\nClassify the user message into one of three intents: analytical | conversational | unsafe\\nIf analytical: identify relevant tables and describe what SQL should be written.\\nIf unsafe: explain why in safetyNote.\\n\\nBigQuery Schema:\\n${ctx.schemaText}\\n\\nUser message: ${ctx.userQuestion}\\n\\nRespond ONLY with valid JSON \u2014 no prose, no markdown fences:\\n{\\n \"intent\": \"analytical\",\\n \"refinedQuestion\": \"<restatement>\",\\n \"suggestedTables\": [\"table_name\"],\\n \"sqlInstruction\": \"<plain English SQL description>\",\\n \"safetyNote\": \"\"\\n}`;\nreturn [{ json: { ...ctx, _prompt: prompt } }];"
},
"typeVersion": 2
},
{
"id": "f3e8b86b-efbd-442c-8a0b-6926a6754f14",
"name": "Claude Conversational Agent",
"type": "@n8n/n8n-nodes-langchain.anthropic",
"position": [
12544,
-5312
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "claude-3-5-sonnet-20241022",
"cachedResultName": "Claude 3.5 Sonnet"
},
"options": {
"maxTokens": 512,
"temperature": 0.1
},
"messages": {
"values": [
{
"content": "={{ $json._prompt }}"
}
]
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "504d971f-5406-42d3-9654-5e302e0bf7a1",
"name": "Parse Claude Agent Output",
"type": "n8n-nodes-base.code",
"notes": "Checks raw.error before parsing so API errors surface cleanly.",
"position": [
12912,
-5312
],
"parameters": {
"jsCode": "const raw = $input.first().json;\n\nif (raw.error) {\n throw new Error(`Anthropic API error on intent classification: ${raw.error.type} \u2014 ${raw.error.message}`);\n}\n\nconst rawText = raw.text || raw.content?.[0]?.text || '';\nif (!rawText) throw new Error('Main Agent returned an empty response.');\nlet agentResponse;\ntry {\n const jsonMatch = rawText.match(/\\{[\\s\\S]*\\}/);\n if (!jsonMatch) throw new Error('No JSON object found in response');\n agentResponse = JSON.parse(jsonMatch[0]);\n} catch (e) {\n throw new Error(`Main Agent unparseable: ${rawText.substring(0, 400)} | ${e.message}`);\n}\nconst intent = (agentResponse.intent || 'conversational').toLowerCase().trim();\nconst validIntents = ['analytical', 'conversational', 'unsafe'];\nagentResponse.intent = validIntents.includes(intent) ? intent : 'conversational';\nconst context = $('Build Main Agent Query Prompt').first().json;\nreturn [{ json: {\n ...context,\n agentIntent: agentResponse.intent,\n safetyNote: agentResponse.safetyNote || '',\n refinedQuestion: agentResponse.refinedQuestion || context.userQuestion,\n suggestedTables: agentResponse.suggestedTables || [],\n sqlInstruction: agentResponse.sqlInstruction || ''\n} }];"
},
"typeVersion": 2
},
{
"id": "de33bdc2-495d-4cdc-85da-5a2d3703d12d",
"name": "If Analytical Intent Detected",
"type": "n8n-nodes-base.if",
"notes": "true branch \u2192 Prepare SQL Agent Prompt. false branch \u2192 Build Non-Analytical Reply.",
"position": [
13136,
-5312
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.agentIntent }}",
"rightValue": "analytical"
}
]
}
},
"typeVersion": 2
},
{
"id": "6f4ca66a-634c-4138-921d-a2e4d907336c",
"name": "Build SQL Agent Prompt",
"type": "n8n-nodes-base.code",
"notes": "Trims schema to suggestedTables only before sending to SQL agent, reducing token cost.",
"position": [
13424,
-5328
],
"parameters": {
"jsCode": "const ctx = $input.first().json;\n\nconst projectId = $('Configure Variables').first().json.GCP_PROJECT_ID;\nconst datasetId = $('Configure Variables').first().json.BQ_DATASET_ID;\n\nlet schemaForSQL = ctx.schemaText;\nif (ctx.suggestedTables && ctx.suggestedTables.length > 0) {\n const lines = ctx.schemaText.split('\\n');\n const relevant = [];\n let inRelevantTable = false;\n for (const line of lines) {\n const tableMatch = line.match(/^ Table: (.+)$/);\n if (tableMatch) {\n inRelevantTable = ctx.suggestedTables.includes(tableMatch[1].trim());\n }\n if (inRelevantTable) relevant.push(line);\n }\n if (relevant.length > 0) schemaForSQL = relevant.join('\\n');\n}\n\nconst prompt = `You are a BigQuery SQL expert. Generate a safe, read-only SELECT query.\\n\\nRULES:\\n- Only SELECT statements permitted\\n- Use fully-qualified table names: \\`${projectId}.${datasetId}.table_name\\`\\n- Standard SQL only (not Legacy SQL)\\n\\nSchema (relevant tables only):\\n${schemaForSQL}\\n\\nUser question: ${ctx.refinedQuestion}\\nInstruction: ${ctx.sqlInstruction}\\nSuggested tables: ${(ctx.suggestedTables || []).join(', ')}\\n\\nRespond ONLY with valid JSON \u2014 no prose, no markdown fences:\\n{\\n \"sql\": \"<complete SQL query>\",\\n \"queryDescription\": \"<one sentence>\"\\n}`;\nreturn [{ json: { ...ctx, _prompt: prompt, _projectId: projectId, _datasetId: datasetId } }];"
},
"typeVersion": 2
},
{
"id": "8f2a2972-80ca-490e-b7b4-9da291505436",
"name": "Claude SQL Sub-Agent",
"type": "@n8n/n8n-nodes-langchain.anthropic",
"position": [
13648,
-5328
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "claude-3-5-sonnet-20241022",
"cachedResultName": "Claude 3.5 Sonnet"
},
"options": {
"maxTokens": 1024,
"temperature": 0
},
"messages": {
"values": [
{
"content": "={{ $json._prompt }}"
}
]
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "1f562c11-69bf-462b-9390-720b136b2e14",
"name": "Extract and Validate SQL Code",
"type": "n8n-nodes-base.code",
"notes": "Regex blocklist removed. Security delegates to proper GCP IAM constraints.",
"position": [
14000,
-5328
],
"parameters": {
"jsCode": "const raw = $input.first().json;\n\nif (raw.error) {\n throw new Error(`Anthropic API error on SQL generation: ${raw.error.type} \u2014 ${raw.error.message}`);\n}\n\nconst rawText = raw.text || raw.content?.[0]?.text || '';\nif (!rawText) throw new Error('SQL Sub-Agent returned an empty response.');\nlet sqlResponse;\ntry {\n const jsonMatch = rawText.match(/\\{[\\s\\S]*\\}/);\n if (!jsonMatch) throw new Error('No JSON object found in SQL agent response');\n sqlResponse = JSON.parse(jsonMatch[0]);\n} catch (e) {\n throw new Error(`SQL Sub-Agent unparseable: ${rawText.substring(0, 400)} | ${e.message}`);\n}\nconst sql = (sqlResponse.sql || '').trim();\nif (!sql) throw new Error('SQL Sub-Agent returned an empty SQL string.');\n\n// Security relies strictly on Google Cloud IAM roles (BigQuery Data Viewer).\n// Row volume limits are handled natively by maxResults in the downstream BigQuery execution node.\n\nconst context = $('Build SQL Agent Prompt').first().json;\nreturn [{ json: { ...context, generatedSQL: sql, queryDescription: sqlResponse.queryDescription || '' } }];"
},
"typeVersion": 2
},
{
"id": "41e28301-780f-4e1d-ac14-97fc87b0038f",
"name": "Run BigQuery SQL Query",
"type": "n8n-nodes-base.googleBigQuery",
"notes": "maxResults: 100 hard cap enforced at the BigQuery API level. alwaysOutputData: true prevents workflow halting on zero results.",
"position": [
14320,
-5328
],
"parameters": {
"options": {
"location": "EU",
"maxResults": 100
},
"sqlQuery": "{{ $json.generatedSQL }}",
"projectId": "={{ $('Configure Variables').first().json.GCP_PROJECT_ID }}"
},
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "9c825217-6d7b-4b98-8541-587a824df828",
"name": "Gather SQL Query Results",
"type": "n8n-nodes-base.code",
"position": [
14544,
-5328
],
"parameters": {
"jsCode": "const allRows = $input.all().map(i => i.json);\nconst context = $('Extract and Validate SQL Code').first().json;\n\n// Filter out the empty object generated by 'alwaysOutputData' when 0 rows are returned\nconst validRows = allRows.filter(row => Object.keys(row).length > 0);\n\nconst rowCount = validRows.length;\nconst truncated = rowCount >= 100;\nconst rowsForLLM = validRows.slice(0, 50);\n\nreturn [{ json: { ...context, queryResults: rowsForLLM, totalRowsReturned: rowCount, truncated, hasResults: rowCount > 0 } }];"
},
"typeVersion": 2
},
{
"id": "2c70f138-41b4-4d5b-9458-bd22b2511cb7",
"name": "Formulate Response Prompt",
"type": "n8n-nodes-base.code",
"position": [
14816,
-5328
],
"parameters": {
"jsCode": "const ctx = $input.first().json;\nconst prompt = `You are a business intelligence assistant. Summarise these BigQuery results in plain English for a non-technical Teams user.\\n\\nUser question: ${ctx.userQuestion}\\nQuery description: ${ctx.queryDescription}\\nTotal rows: ${ctx.totalRowsReturned}${ctx.truncated ? ' (truncated at 100)' : ''}\\n\\nData:\\n${JSON.stringify(ctx.queryResults, null, 2)}\\n\\nInstructions:\\n- Lead with a direct answer\\n- Highlight key numbers, trends, or outliers\\n- If no data, say so and suggest why\\n- Max 150 words\\n- Never mention SQL, BigQuery, or technical internals`;\nreturn [{ json: { ...ctx, _prompt: prompt } }];"
},
"typeVersion": 2
},
{
"id": "9c9a1fe6-c9b1-4333-abb2-d78035346707",
"name": "Claude Generates Response",
"type": "@n8n/n8n-nodes-langchain.anthropic",
"position": [
15040,
-5328
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "claude-3-5-sonnet-20241022",
"cachedResultName": "Claude 3.5 Sonnet"
},
"options": {
"maxTokens": 512,
"temperature": 0.1
},
"messages": {
"values": [
{
"content": "={{ $json._prompt }}"
}
]
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "8d1dcfbb-92aa-42fe-8994-da50aee49cff",
"name": "Create Teams Adaptive Card",
"type": "n8n-nodes-base.code",
"notes": "Data preview utilizes FactSet layout instead of raw key:value monospace string.",
"position": [
15408,
-5328
],
"parameters": {
"jsCode": "const raw = $input.first().json;\nif (raw.error) throw new Error(`Anthropic API error on answer synthesis: ${raw.error.type} \u2014 ${raw.error.message}`);\nconst answer = raw.text || raw.content?.[0]?.text || 'Could not generate an answer.';\nconst context = $('Formulate Response Prompt').first().json;\nconst rows = (context.queryResults || []).slice(0, 5);\n\nconst previewBlocks = [];\nif (rows.length === 0) {\n previewBlocks.push({ type: 'TextBlock', text: 'No data returned.', color: 'Warning', spacing: 'Small' });\n} else {\n const firstRowFacts = Object.entries(rows[0]).map(([k, v]) => ({ title: k, value: String(v ?? '') }));\n previewBlocks.push({ type: 'FactSet', facts: firstRowFacts, spacing: 'Small' });\n for (let i = 1; i < rows.length; i++) {\n const line = Object.entries(rows[i]).map(([k, v]) => `${k}: ${v ?? ''}`).join(' | ');\n previewBlocks.push({ type: 'TextBlock', text: line, wrap: true, fontType: 'Monospace', size: 'Small', color: 'Default', spacing: 'None' });\n }\n if (context.totalRowsReturned > rows.length) {\n previewBlocks.push({ type: 'TextBlock', text: `\u2026and ${context.totalRowsReturned - rows.length} more rows`, size: 'Small', color: 'Light', spacing: 'None', isSubtle: true });\n }\n}\n\nconst sqlPreview = (context.generatedSQL || '').substring(0, 150);\nconst sqlDisplay = (context.generatedSQL || '').length > 150 ? sqlPreview + '...' : sqlPreview;\nconst card = {\n type: 'message',\n attachments: [{\n contentType: 'application/vnd.microsoft.card.adaptive',\n content: {\n '$schema': 'http://adaptivecards.io/schemas/adaptive-card.json',\n type: 'AdaptiveCard',\n version: '1.4',\n body: [\n { type: 'TextBlock', text: 'BigQuery Analyst', weight: 'Bolder', size: 'Medium', color: 'Accent' },\n { type: 'TextBlock', text: `**Q:** ${context.userQuestion}`, wrap: true, spacing: 'Small' },\n { type: 'TextBlock', text: answer, wrap: true, spacing: 'Medium' },\n { type: 'TextBlock', text: `Data preview (${context.totalRowsReturned} rows${context.truncated ? ', truncated at 100' : ''})`, weight: 'Bolder', spacing: 'Medium', size: 'Small' },\n ...previewBlocks,\n { type: 'TextBlock', text: `SQL: \\`${sqlDisplay}\\``, wrap: true, size: 'Small', color: 'Light', spacing: 'Medium' }\n ]\n }\n }]\n};\nreturn [{ json: { card, channelId: context.channelId, teamId: context.teamId } }];"
},
"typeVersion": 2
},
{
"id": "2cabe382-12fa-4d9a-9500-6bc1c61517d4",
"name": "Send Answer to Teams Channel",
"type": "n8n-nodes-base.microsoftTeams",
"position": [
15632,
-5328
],
"parameters": {
"operation": "sendMessage"
},
"typeVersion": 2
},
{
"id": "9076321e-02c6-4e99-b689-78a6d522ede9",
"name": "Generate Non-Analytical Response",
"type": "n8n-nodes-base.code",
"position": [
13568,
-4960
],
"parameters": {
"jsCode": "const context = $('Parse Claude Agent Output').first().json;\nconst isUnsafe = context.agentIntent === 'unsafe';\nconst message = isUnsafe\n ? `Sorry, I can't run that query. ${context.safetyNote || 'That request is outside what I can help with.'} Please ask a read-only analytical question about your data.`\n : `I can only answer analytical questions about your BigQuery data. Examples:\\n- \"What were total sales last month?\"\\n- \"Which customers had the highest order value this week?\"\\n- \"How many new signups did we get in June?\"`;\nreturn [{ json: { channelId: context.channelId, teamId: context.teamId, message } }];"
},
"typeVersion": 2
},
{
"id": "39303412-c908-4181-ac80-a7a1e9115201",
"name": "Share Non-Analytical Reply to Teams",
"type": "n8n-nodes-base.microsoftTeams",
"position": [
13856,
-4960
],
"parameters": {
"operation": "sendMessage"
},
"typeVersion": 2
},
{
"id": "b4356c3a-5f5c-4dc5-aa21-de04999ad6d1",
"name": "Format Error Message",
"type": "n8n-nodes-base.code",
"notes": "Error handler node. Extracts channelId/teamId from execution data to post the failure message.",
"position": [
10432,
-4832
],
"parameters": {
"jsCode": "const err = $input.first().json;\nconst nodeName = err.node?.name || 'unknown node';\nconst runData = err.execution?.data?.resultData?.runData || {};\nconst firstRun = runData['Validate Teams Message']?.[0]?.data?.main?.[0]?.[0]?.json || {};\nreturn [{ json: {\n channelId: firstRun.channelId || '',\n teamId: firstRun.teamId || '',\n message: `Something went wrong (failed at: ${nodeName}). Please try again. If this persists, contact your administrator.`\n} }];"
},
"typeVersion": 2
},
{
"id": "bbfd43ac-454c-4a9f-a74e-7347a0cdd852",
"name": "Send Error Notification to Teams",
"type": "n8n-nodes-base.microsoftTeams",
"position": [
10656,
-4832
],
"parameters": {
"operation": "sendMessage"
},
"typeVersion": 2
},
{
"id": "e4157e62-4fe8-4d6d-830e-42693750f198",
"name": "On Workflow Error Occurrence",
"type": "n8n-nodes-base.errorTrigger",
"notes": "REQUIRED SETUP: Go to Workflow Settings \u2192 Error Workflow and select this same workflow as the error handler.",
"position": [
10208,
-4832
],
"parameters": {},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "90f1ec0e-f0b6-4b4f-9e15-3b2c18ac5b94",
"nodeGroups": [],
"connections": {
"If Cache Hit": {
"main": [
[
{
"node": "Ensure Schema Availability",
"type": "main",
"index": 0
}
],
[
{
"node": "Create Schema Query Statement",
"type": "main",
"index": 0
}
]
]
},
"Configure Variables": {
"main": [
[
{
"node": "Check Schema Cache Validity",
"type": "main",
"index": 0
}
]
]
},
"Claude SQL Sub-Agent": {
"main": [
[
{
"node": "Extract and Validate SQL Code",
"type": "main",
"index": 0
}
]
]
},
"Format Error Message": {
"main": [
[
{
"node": "Send Error Notification to Teams",
"type": "main",
"index": 0
}
]
]
},
"Build SQL Agent Prompt": {
"main": [
[
{
"node": "Claude SQL Sub-Agent",
"type": "main",
"index": 0
}
]
]
},
"Run BigQuery SQL Query": {
"main": [
[
{
"node": "Gather SQL Query Results",
"type": "main",
"index": 0
}
]
]
},
"Validate Teams Message": {
"main": [
[
{
"node": "Configure Variables",
"type": "main",
"index": 0
}
]
]
},
"Gather SQL Query Results": {
"main": [
[
{
"node": "Formulate Response Prompt",
"type": "main",
"index": 0
}
]
]
},
"Claude Generates Response": {
"main": [
[
{
"node": "Create Teams Adaptive Card",
"type": "main",
"index": 0
}
]
]
},
"Formulate Response Prompt": {
"main": [
[
{
"node": "Claude Generates Response",
"type": "main",
"index": 0
}
]
]
},
"Parse Claude Agent Output": {
"main": [
[
{
"node": "If Analytical Intent Detected",
"type": "main",
"index": 0
}
]
]
},
"Create Teams Adaptive Card": {
"main": [
[
{
"node": "Send Answer to Teams Channel",
"type": "main",
"index": 0
}
]
]
},
"Ensure Schema Availability": {
"main": [
[
{
"node": "Build Main Agent Query Prompt",
"type": "main",
"index": 0
}
]
]
},
"Fetch Schema from BigQuery": {
"main": [
[
{
"node": "Compile Schema Data for LLM",
"type": "main",
"index": 0
}
]
]
},
"Check Schema Cache Validity": {
"main": [
[
{
"node": "If Cache Hit",
"type": "main",
"index": 0
}
]
]
},
"Claude Conversational Agent": {
"main": [
[
{
"node": "Parse Claude Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Compile Schema Data for LLM": {
"main": [
[
{
"node": "Ensure Schema Availability",
"type": "main",
"index": 0
}
]
]
},
"When Teams Message Received": {
"main": [
[
{
"node": "Validate Teams Message",
"type": "main",
"index": 0
}
]
]
},
"On Workflow Error Occurrence": {
"main": [
[
{
"node": "Format Error Message",
"type": "main",
"index": 0
}
]
]
},
"Build Main Agent Query Prompt": {
"main": [
[
{
"node": "Claude Conversational Agent",
"type": "main",
"index": 0
}
]
]
},
"Create Schema Query Statement": {
"main": [
[
{
"node": "Fetch Schema from BigQuery",
"type": "main",
"index": 0
}
]
]
},
"Extract and Validate SQL Code": {
"main": [
[
{
"node": "Run BigQuery SQL Query",
"type": "main",
"index": 0
}
]
]
},
"If Analytical Intent Detected": {
"main": [
[
{
"node": "Build SQL Agent Prompt",
"type": "main",
"index": 0
}
],
[
{
"node": "Generate Non-Analytical Response",
"type": "main",
"index": 0
}
]
]
},
"Generate Non-Analytical Response": {
"main": [
[
{
"node": "Share Non-Analytical Reply to Teams",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
anthropicApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Turn Microsoft Teams into a natural language BI tool. This dual-agent Claude workflow intelligently classifies user questions, generates safe BigQuery SQL, executes the query, and returns a plain-English summary alongside a data preview directly to your Teams channel. The…
Source: https://n8n.io/workflows/16537/ — 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.
Stop managing onboarding as a checklist. Let automation handle the infrastructure so you can focus on the people.
This workflow automates Invoice & Payment Tracking (with Approvals) across Notion and Slack. Ingest — You drop invoices/receipts (PDF/IMG/JSON) into the flow. Extract — OCR + parsing pulls out key fie
Agent Nodes. Uses lmChatOpenAi, slack, stopAndError, errorTrigger. Event-driven trigger; 72 nodes.
Template Nodes Example. Uses CUSTOM, formTrigger, executeWorkflowTrigger, chatTrigger. Event-driven trigger; 70 nodes.
pokeapi - DataCleansing Practice. Uses httpRequest, googleBigQuery, lmChatGoogleGemini, agent. Event-driven trigger; 56 nodes.