This workflow corresponds to n8n.io template #14975 — we link there as the canonical source.
This workflow follows the HTTP Request → Itemlists 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 →
{
"nodes": [
{
"id": "767380a0-fe54-4f9b-b0a7-ad6502cda9c1",
"name": "\ud83d\udccb MAIN \u2014 Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
32,
-1152
],
"parameters": {
"width": 800,
"height": 1176,
"content": "## \ud83c\udfa7 Customer Support FAQ Audio Library\n### Automatically Convert FAQ Answers into Audio & Embed on Support Pages\n\n**What this workflow does:**\nThis template is triggered by a webhook (from your CMS, Typeform, or any form tool) whenever a new FAQ entry is submitted or a batch audio-generation job is requested. It:\n1. Receives FAQ data via **Webhook** (question + answer + category + FAQ ID)\n2. Reads the **full FAQ batch** from **Supabase** (your FAQ database table) filtered by category or status\n3. **Filters** to only unprocessed FAQs (where `audio_url` is null)\n4. Uses **Item Lists node** to deduplicate and sort FAQs by category\n5. Loops through each FAQ via **Loop Over Items**\n6. Inside the loop: uses **Google Cloud Text-to-Speech API** to generate SSML-enhanced MP3 audio (with pauses, emphasis, and pronunciation hints)\n7. Uploads each MP3 via **UploadToURL** \u2014 gets a permanent CDN URL per FAQ\n8. Updates the **Webflow CMS** collection item for that FAQ \u2014 writes the audio URL into the `audio_embed` field so it renders an audio player on your support page automatically\n9. Updates the **Supabase** row \u2014 stamps `audio_url`, `audio_generated_at`, and sets `status = audio_published`\n10. After the loop completes, sends a **Microsoft Teams** card to your support team channel summarising how many FAQs were processed\n11. **Responds to the original Webhook** with a JSON summary of all processed FAQ IDs and their audio URLs\n\n**Architecture (completely unique \u2014 different trigger, TTS provider, DB, CMS, notification, shape):**\n- \ud83d\udd14 Webhook Trigger \u2014 receives FAQ batch job request\n- \ud83d\uddc4\ufe0f Supabase \u2014 read FAQ rows (NEW database node)\n- \ud83d\udd3d Filter Node \u2014 exclude already-processed rows\n- \ud83d\udccb Item Lists Node \u2014 deduplicate + sort by category\n- \ud83d\udd01 Loop Over Items \u2014 process one FAQ at a time\n- \ud83d\udde3\ufe0f Google Cloud TTS \u2014 SSML-enhanced audio (NOT ElevenLabs)\n- \u2601\ufe0f UploadToURL \u2014 host MP3 per FAQ (mandatory)\n- \ud83c\udf10 Webflow CMS \u2014 update support page audio embed field\n- \ud83d\uddc4\ufe0f Supabase Update \u2014 write audio URL + status back\n- \ud83d\udcbc Microsoft Teams \u2014 batch summary card (NOT Slack/Telegram)\n- \ud83d\udce8 Respond to Webhook \u2014 return JSON result to caller\n\n**Setup Requirements:**\n1. Webhook URL \u2014 connect your CMS or FAQ form to this endpoint\n2. Supabase project URL + service role key. Table: `faqs` with columns: `id`, `question`, `answer`, `category`, `audio_url`, `audio_generated_at`, `status`, `webflow_item_id`\n3. Google Cloud TTS API Key (enable Text-to-Speech API in GCP console)\n4. UploadToURL endpoint configured\n5. Webflow API token + Collection ID for your FAQ support page collection\n6. Microsoft Teams Incoming Webhook URL for your support team channel"
},
"typeVersion": 1
},
{
"id": "48e9adc8-8835-4593-8d30-0c5a8767de6c",
"name": "\ud83d\udcdd Note \u2014 Webhook, Supabase Fetch & Filter",
"type": "n8n-nodes-base.stickyNote",
"position": [
848,
64
],
"parameters": {
"color": 7,
"width": 684,
"height": 578,
"content": "### \ud83d\udd14 Step 1 \u2014 Webhook Intake & Supabase FAQ Fetch\n**Webhook Node:** Receives POST requests from your CMS, Typeform, or a manual trigger button. Payload can include `category` (to filter FAQs) or `faq_ids` (array of specific IDs to process). Also accepts `force_regenerate: true` to reprocess already-published FAQs.\n**Supabase \u2014 Read FAQs:** Queries the `faqs` table using a dynamic filter built from the webhook payload. Uses `select=*` with a `status=neq.audio_published` PostgREST filter by default. Returns all matching FAQ rows as individual items.\n**Filter Node:** Drops any row where `audio_url` is already populated AND `force_regenerate` was not sent \u2014 ensuring idempotency without extra Supabase logic."
},
"typeVersion": 1
},
{
"id": "600d39fe-ec90-4984-87f7-4bc9754add06",
"name": "\ud83d\udcdd Note \u2014 Item Lists, SSML Build & Loop",
"type": "n8n-nodes-base.stickyNote",
"position": [
1552,
64
],
"parameters": {
"color": 7,
"width": 860,
"height": 578,
"content": "### \ud83d\udccb Step 2 \u2014 Dedup, Sort & Loop Setup\n**Item Lists Node:** Removes any duplicate FAQ IDs (in case Supabase returned overlapping results from multiple filters) and sorts remaining items alphabetically by `category` \u2014 so audio files are generated in logical groupings.\n**Code \u2014 Build SSML:** For each FAQ, wraps the answer text in Google Cloud TTS SSML markup \u2014 adds `<break>` pauses after the question reading, `<emphasis>` on key terms, and `<prosody rate=\"slow\">` on complex phrases. Also prepends 'Question: [question text]' before the answer for full context audio.\n**Loop Over Items:** Iterates one FAQ at a time through the TTS + upload + CMS update chain. Prevents API rate limit bursts on Google TTS."
},
"typeVersion": 1
},
{
"id": "f530d75b-098f-414a-a4b1-ac1507de68b9",
"name": "\ud83d\udcdd Note \u2014 Google TTS, Base64 Decode & Upload",
"type": "n8n-nodes-base.stickyNote",
"position": [
2432,
64
],
"parameters": {
"color": 7,
"width": 636,
"height": 578,
"content": "### \ud83d\udde3\ufe0f Step 3 \u2014 Google Cloud TTS & UploadToURL\n**Google Cloud TTS:** Calls the `/v1/text:synthesize` endpoint with the SSML input. Uses `WaveNet` voice (`en-US-WaveNet-D` \u2014 deep, professional) at 1x speaking rate. Returns base64-encoded MP3 audio content in the response JSON.\n**Code \u2014 Decode Base64 to Binary:** Google TTS returns audio as base64 string inside JSON \u2014 this Code node decodes it into actual binary buffer data so the UploadToURL node can handle it as a file.\n**UploadToURL \u2014 Host FAQ MP3:** Uploads the decoded binary MP3 and receives a permanent CDN URL. This URL is the source of truth used in both Webflow and Supabase."
},
"typeVersion": 1
},
{
"id": "72c4cda0-69cf-43ee-9cfd-ac93ed2efe25",
"name": "\ud83d\udcdd Note \u2014 Webflow CMS Patch & Supabase Write-Back",
"type": "n8n-nodes-base.stickyNote",
"position": [
3104,
64
],
"parameters": {
"color": 7,
"width": 636,
"height": 580,
"content": "### \ud83c\udf10 Step 4 \u2014 Webflow CMS Update & Supabase Write-Back\n**Webflow \u2014 Patch CMS Item:** Uses the Webflow REST API to PATCH the FAQ's CMS collection item (identified by `webflow_item_id` from Supabase). Writes the hosted audio URL into the `audio-embed-url` field and sets `audio-published` to `true`. Webflow automatically re-renders the support page with an audio player.\n**Supabase \u2014 Update FAQ Row:** Updates the `faqs` table row \u2014 sets `audio_url`, `audio_generated_at` (ISO timestamp), and `status = audio_published`. This prevents reprocessing and gives ops teams a full audit trail."
},
"typeVersion": 1
},
{
"id": "709482c2-89b8-4dea-8242-c85112bff3a0",
"name": "\ud83d\udcdd Note \u2014 Teams Card & Webhook Response",
"type": "n8n-nodes-base.stickyNote",
"position": [
3760,
64
],
"parameters": {
"color": 7,
"width": 668,
"height": 564,
"content": "### \ud83d\udcbc Step 5 \u2014 Teams Summary Card & Webhook Response\n**Code \u2014 Build Summary:** After the loop finishes, collects all processed FAQ IDs, their questions, categories, and audio URLs into a structured summary object. Also counts successes vs failures.\n**Microsoft Teams \u2014 Send Adaptive Card:** Posts a rich Adaptive Card to your support team channel with a table of all newly generated FAQs, their categories, and clickable audio URLs. Uses Teams Incoming Webhook (no OAuth needed).\n**Respond to Webhook:** Returns a structured JSON response to the original caller with `processed_count`, `faq_ids`, `audio_urls`, and `timestamp` \u2014 allowing the calling system (CMS, Typeform, etc.) to confirm success."
},
"typeVersion": 1
},
{
"id": "7c9aa9c3-f359-4385-9d01-f3d45e1bbf3f",
"name": "Webhook \u2014 Receive FAQ Audio Job Request",
"type": "n8n-nodes-base.webhook",
"position": [
944,
432
],
"parameters": {
"path": "faq-audio-generate",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "7a569ad6-cdff-48c1-ab7f-b9a5a3c1edba",
"name": "Supabase \u2014 Read Unprocessed FAQs",
"type": "n8n-nodes-base.httpRequest",
"position": [
1168,
432
],
"parameters": {
"url": "=https://{{ $json.body?.supabase_url ?? 'YOUR_SUPABASE_PROJECT_REF' }}.supabase.co/rest/v1/faqs",
"options": {
"timeout": 15000
},
"sendQuery": true,
"sendHeaders": true,
"queryParameters": {
"parameters": [
{
"name": "select",
"value": "id,question,answer,category,audio_url,audio_generated_at,status,webflow_item_id"
},
{
"name": "status",
"value": "=neq.audio_published"
},
{
"name": "order",
"value": "category.asc,id.asc"
},
{
"name": "limit",
"value": "50"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "=YOUR_SUPABASE_SERVICE_ROLE_KEY"
},
{
"name": "Authorization",
"value": "=Bearer YOUR_SUPABASE_SERVICE_ROLE_KEY"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "41febbde-0e11-40df-ad5a-71d770dbc13b",
"name": "Code \u2014 Filter & Split FAQ Rows",
"type": "n8n-nodes-base.code",
"position": [
1392,
432
],
"parameters": {
"jsCode": "// Supabase returns an array \u2014 split into individual items\nconst rows = Array.isArray($input.first().json)\n ? $input.first().json\n : [$input.first().json];\n\nconst forceRegenerate = $('Webhook \u2014 Receive FAQ Audio Job Request').item.json?.body?.force_regenerate === true;\n\nconst valid = rows.filter(row => {\n if (!row.id || !row.question || !row.answer) return false;\n // Skip if already published unless force_regenerate\n if (row.audio_url && !forceRegenerate) return false;\n // Skip very short answers (under 20 chars \u2014 likely test/placeholder data)\n if ((row.answer || '').trim().length < 20) return false;\n return true;\n});\n\nreturn valid.map(row => ({ json: row }));"
},
"typeVersion": 2
},
{
"id": "891647aa-158a-4206-bd8f-061f48021293",
"name": "Item Lists \u2014 Deduplicate by FAQ ID",
"type": "n8n-nodes-base.itemLists",
"position": [
1600,
432
],
"parameters": {
"compare": "selectedFields",
"options": {},
"operation": "removeDuplicates"
},
"typeVersion": 3.1
},
{
"id": "a4a03362-9efc-4818-ad0d-53a7f05d3cc4",
"name": "Item Lists \u2014 Sort by Category then ID",
"type": "n8n-nodes-base.itemLists",
"position": [
1824,
432
],
"parameters": {
"options": {},
"operation": "sort",
"sortFieldsUi": {
"sortField": [
{
"fieldName": "category"
},
{
"fieldName": "id"
}
]
}
},
"typeVersion": 3.1
},
{
"id": "ea083e79-1671-463a-9533-cc8f7540c11e",
"name": "Code \u2014 Build SSML for Each FAQ",
"type": "n8n-nodes-base.code",
"position": [
2048,
432
],
"parameters": {
"jsCode": "const faq = $input.first().json;\n\n// Clean answer text\nconst cleanAnswer = (faq.answer || '')\n .replace(/<[^>]+>/g, '') // strip HTML\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/ /g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst cleanQuestion = (faq.question || '').replace(/<[^>]+>/g, '').trim();\n\n// Build SSML \u2014 adds pauses, emphasis, natural pacing\nconst ssml = `<speak>\n <prosody rate=\"0.95\" pitch=\"0st\">\n <emphasis level=\"moderate\">Question:</emphasis>\n <break time=\"300ms\"/>\n ${cleanQuestion}\n <break time=\"700ms\"/>\n <emphasis level=\"moderate\">Answer:</emphasis>\n <break time=\"400ms\"/>\n ${cleanAnswer}\n <break time=\"500ms\"/>\n </prosody>\n</speak>`;\n\nconst slugBase = cleanQuestion\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .substring(0, 50)\n .replace(/-$/, '');\n\nreturn [{\n json: {\n ...faq,\n cleanQuestion,\n cleanAnswer,\n ssml,\n audioFilename: `faq-${faq.id}-${slugBase}.mp3`\n }\n}];"
},
"typeVersion": 2
},
{
"id": "189df419-05ed-49aa-a59a-421098ee9693",
"name": "Loop Over Items \u2014 Process One FAQ at a Time",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2272,
432
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "d255671b-aa46-499e-b999-10a1d025776d",
"name": "Google Cloud TTS \u2014 Synthesize FAQ Audio",
"type": "n8n-nodes-base.httpRequest",
"position": [
2480,
432
],
"parameters": {
"url": "https://texttospeech.googleapis.com/v1/text:synthesize",
"method": "POST",
"options": {
"timeout": 30000
},
"jsonBody": "={\n \"input\": {\n \"ssml\": {{ JSON.stringify($json.ssml) }}\n },\n \"voice\": {\n \"languageCode\": \"en-US\",\n \"name\": \"en-US-Wavenet-D\",\n \"ssmlGender\": \"MALE\"\n },\n \"audioConfig\": {\n \"audioEncoding\": \"MP3\",\n \"speakingRate\": 1.0,\n \"pitch\": 0,\n \"volumeGainDb\": 1.0,\n \"effectsProfileId\": [\"headphone-class-device\"]\n }\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "X-Goog-Api-Key",
"value": "=YOUR_GOOGLE_CLOUD_TTS_API_KEY"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "4ee30269-ff88-45d8-b819-ad57e40eaeb4",
"name": "Code \u2014 Decode Base64 Audio to Binary",
"type": "n8n-nodes-base.code",
"position": [
2704,
432
],
"parameters": {
"jsCode": "// Google TTS returns base64 audioContent \u2014 decode to binary\nconst ttsResp = $input.first().json;\nconst faqData = $('Code \u2014 Build SSML for Each FAQ').item.json;\n\nconst base64Audio = ttsResp?.audioContent;\nif (!base64Audio) {\n throw new Error(`Google TTS returned no audioContent for FAQ ID: ${faqData.id}`);\n}\n\n// Decode base64 to Buffer\nconst audioBuffer = Buffer.from(base64Audio, 'base64');\n\nreturn [{\n json: { ...faqData },\n binary: {\n data: {\n data: audioBuffer,\n mimeType: 'audio/mpeg',\n fileName: faqData.audioFilename,\n fileExtension: 'mp3'\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "2e4b32a4-6a97-48e3-a485-844f9b0de093",
"name": "UploadToURL \u2014 Host FAQ MP3 on CDN",
"type": "n8n-nodes-base.httpRequest",
"position": [
2928,
432
],
"parameters": {
"url": "https://upload.uploadtourl.com/api/upload",
"method": "POST",
"options": {
"timeout": 60000,
"redirect": {
"redirect": {}
}
},
"sendBody": true,
"contentType": "multipart-form-data",
"sendHeaders": true,
"bodyParameters": {
"parameters": [
{
"name": "file",
"parameterType": "formBinaryData",
"inputDataFieldName": "data"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "f071afcb-5c2b-4b14-b480-a1485f3edbf7",
"name": "Code \u2014 Capture Audio URL + Timestamp",
"type": "n8n-nodes-base.code",
"position": [
3152,
432
],
"parameters": {
"jsCode": "const uploadResp = $input.first().json;\nconst faqData = $('Code \u2014 Build SSML for Each FAQ').item.json;\n\nconst audioUrl =\n uploadResp?.url ??\n uploadResp?.data?.url ??\n uploadResp?.file?.url ??\n uploadResp?.link ?? '';\n\nif (!audioUrl) throw new Error(`UploadToURL returned no URL for FAQ ID: ${faqData.id}`);\n\nreturn [{\n json: {\n ...faqData,\n audioUrl,\n audioGeneratedAt: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "4714d9be-b101-4663-a703-3a6b74565c0a",
"name": "Webflow \u2014 Patch FAQ CMS Item with Audio URL",
"type": "n8n-nodes-base.httpRequest",
"position": [
3360,
432
],
"parameters": {
"url": "=https://api.webflow.com/v2/collections/YOUR_WEBFLOW_COLLECTION_ID/items/{{ $json.webflow_item_id }}",
"method": "PATCH",
"options": {
"timeout": 15000
},
"jsonBody": "={\n \"isArchived\": false,\n \"isDraft\": false,\n \"fieldData\": {\n \"audio-embed-url\": {{ JSON.stringify($json.audioUrl) }},\n \"audio-published\": true,\n \"audio-generated-at\": {{ JSON.stringify($json.audioGeneratedAt) }},\n \"audio-filename\": {{ JSON.stringify($json.audioFilename) }}\n }\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer YOUR_WEBFLOW_API_TOKEN"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "accept",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "89602260-20f5-43d9-8cad-e56717965a10",
"name": "Supabase \u2014 Write Audio URL & Status Back",
"type": "n8n-nodes-base.httpRequest",
"position": [
3584,
432
],
"parameters": {
"url": "=https://YOUR_SUPABASE_PROJECT_REF.supabase.co/rest/v1/faqs?id=eq.{{ $json.id }}",
"method": "PATCH",
"options": {
"timeout": 10000
},
"jsonBody": "={\n \"audio_url\": {{ JSON.stringify($json.audioUrl) }},\n \"audio_generated_at\": {{ JSON.stringify($json.audioGeneratedAt) }},\n \"status\": \"audio_published\"\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "=YOUR_SUPABASE_SERVICE_ROLE_KEY"
},
{
"name": "Authorization",
"value": "=Bearer YOUR_SUPABASE_SERVICE_ROLE_KEY"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=representation"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "a9d79805-4c57-49aa-89db-05efef8e55da",
"name": "Code \u2014 Build Teams Card & Summary Object",
"type": "n8n-nodes-base.code",
"position": [
3808,
432
],
"parameters": {
"jsCode": "// Collect all processed FAQs from this loop run\nconst allItems = $('Code \u2014 Capture Audio URL + Timestamp').all();\n\nconst processed = allItems.map(item => ({\n id: item.json.id,\n question: item.json.cleanQuestion,\n category: item.json.category,\n audioUrl: item.json.audioUrl,\n generatedAt: item.json.audioGeneratedAt\n}));\n\n// Build Teams Adaptive Card table rows\nconst tableRows = processed.map(p =>\n `| [${p.question.substring(0,45)}...](${p.audioUrl}) | ${p.category} | \u25b6 [Listen](${p.audioUrl}) |`\n).join('\\n');\n\nconst teamsCard = {\n type: 'message',\n attachments: [\n {\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 {\n type: 'TextBlock',\n text: `\ud83c\udfa7 FAQ Audio Library Updated \u2014 ${processed.length} FAQ${processed.length !== 1 ? 's' : ''} Processed`,\n weight: 'Bolder',\n size: 'Medium',\n color: 'Accent'\n },\n {\n type: 'TextBlock',\n text: `Generated at: ${new Date().toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })}`,\n isSubtle: true,\n spacing: 'None'\n },\n {\n type: 'FactSet',\n facts: [\n { title: 'Total FAQs processed', value: String(processed.length) },\n { title: 'Categories covered', value: [...new Set(processed.map(p => p.category))].join(', ') || 'General' }\n ]\n },\n ...processed.slice(0, 8).map(p => ({\n type: 'ColumnSet',\n columns: [\n {\n type: 'Column', width: 'stretch',\n items: [{ type: 'TextBlock', text: p.question.substring(0, 60), wrap: true, size: 'Small' }]\n },\n {\n type: 'Column', width: 'auto',\n items: [{ type: 'TextBlock', text: p.category, isSubtle: true, size: 'Small' }]\n }\n ]\n })),\n processed.length > 8 ? {\n type: 'TextBlock',\n text: `...and ${processed.length - 8} more. Check Supabase for full list.`,\n isSubtle: true, size: 'Small'\n } : null\n ].filter(Boolean),\n actions: [\n {\n type: 'Action.OpenUrl',\n title: '\ud83d\udd17 View Support Page',\n url: 'https://YOUR_SUPPORT_SITE_URL/faq'\n },\n {\n type: 'Action.OpenUrl',\n title: '\ud83d\uddc4\ufe0f Open Supabase',\n url: 'https://app.supabase.com/project/YOUR_SUPABASE_PROJECT_REF/editor'\n }\n ]\n }\n }\n ]\n};\n\nreturn [{\n json: {\n processed,\n processedCount: processed.length,\n teamsCard,\n summaryTimestamp: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "81754a6c-34cc-4f9d-8d6d-50b8e2639094",
"name": "Microsoft Teams \u2014 Send FAQ Audio Summary Card",
"type": "n8n-nodes-base.httpRequest",
"position": [
4032,
432
],
"parameters": {
"url": "=YOUR_TEAMS_INCOMING_WEBHOOK_URL",
"method": "POST",
"options": {
"timeout": 15000
},
"jsonBody": "={{ JSON.stringify($json.teamsCard) }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "a12b3cd1-f898-45ef-ab83-10695fea45d5",
"name": "Respond to Webhook \u2014 Return JSON Summary",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
4240,
432
],
"parameters": {
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"respondWith": "json",
"responseBody": "={\n \"success\": true,\n \"processed_count\": {{ $('Code \u2014 Build Teams Card & Summary Object').item.json.processedCount }},\n \"timestamp\": \"{{ $('Code \u2014 Build Teams Card & Summary Object').item.json.summaryTimestamp }}\",\n \"faqs\": {{ JSON.stringify($('Code \u2014 Build Teams Card & Summary Object').item.json.processed) }}\n}"
},
"typeVersion": 1.1
}
],
"connections": {
"Code \u2014 Build SSML for Each FAQ": {
"main": [
[
{
"node": "Loop Over Items \u2014 Process One FAQ at a Time",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Filter & Split FAQ Rows": {
"main": [
[
{
"node": "Item Lists \u2014 Deduplicate by FAQ ID",
"type": "main",
"index": 0
}
]
]
},
"Supabase \u2014 Read Unprocessed FAQs": {
"main": [
[
{
"node": "Code \u2014 Filter & Split FAQ Rows",
"type": "main",
"index": 0
}
]
]
},
"UploadToURL \u2014 Host FAQ MP3 on CDN": {
"main": [
[
{
"node": "Code \u2014 Capture Audio URL + Timestamp",
"type": "main",
"index": 0
}
]
]
},
"Item Lists \u2014 Deduplicate by FAQ ID": {
"main": [
[
{
"node": "Item Lists \u2014 Sort by Category then ID",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Capture Audio URL + Timestamp": {
"main": [
[
{
"node": "Webflow \u2014 Patch FAQ CMS Item with Audio URL",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Decode Base64 Audio to Binary": {
"main": [
[
{
"node": "UploadToURL \u2014 Host FAQ MP3 on CDN",
"type": "main",
"index": 0
}
]
]
},
"Item Lists \u2014 Sort by Category then ID": {
"main": [
[
{
"node": "Code \u2014 Build SSML for Each FAQ",
"type": "main",
"index": 0
}
]
]
},
"Google Cloud TTS \u2014 Synthesize FAQ Audio": {
"main": [
[
{
"node": "Code \u2014 Decode Base64 Audio to Binary",
"type": "main",
"index": 0
}
]
]
},
"Webhook \u2014 Receive FAQ Audio Job Request": {
"main": [
[
{
"node": "Supabase \u2014 Read Unprocessed FAQs",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Build Teams Card & Summary Object": {
"main": [
[
{
"node": "Microsoft Teams \u2014 Send FAQ Audio Summary Card",
"type": "main",
"index": 0
}
]
]
},
"Supabase \u2014 Write Audio URL & Status Back": {
"main": [
[
{
"node": "Loop Over Items \u2014 Process One FAQ at a Time",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items \u2014 Process One FAQ at a Time": {
"main": [
[
{
"node": "Google Cloud TTS \u2014 Synthesize FAQ Audio",
"type": "main",
"index": 0
}
]
]
},
"Webflow \u2014 Patch FAQ CMS Item with Audio URL": {
"main": [
[
{
"node": "Supabase \u2014 Write Audio URL & Status Back",
"type": "main",
"index": 0
}
]
]
},
"Microsoft Teams \u2014 Send FAQ Audio Summary Card": {
"main": [
[
{
"node": "Respond to Webhook \u2014 Return JSON Summary",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Enhance your support documentation with an audio-first experience. This workflow converts Supabase FAQ entries into high-quality audio using Google Cloud TTS, hosts them via a CDN, and embeds them into your Webflow support pages.
Source: https://n8n.io/workflows/14975/ — 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.
BP_check. Uses googleSheets, @n-octo-n/n8n-nodes-json-database, httpRequest, itemLists. Webhook trigger; 99 nodes.
This workflow demonstrates how to export SQL to XML and present the data nicely formatted using an XSL Template.
v25.1.3. Uses httpRequest, mySql, n8n-nodes-zohozeptomail. Webhook trigger; 98 nodes.
Disparador 1.8. Uses itemLists, postgres, emailSend, httpRequest. Scheduled trigger; 85 nodes.
This solution enables you to manage all your Notion and Todoist tasks from different workspaces as well as your calendar events in a single place. This is 2 way sync with partial support for recurring