{
  "name": "Dictate a Customer Answer into a Webflow FAQ Draft | Uptune",
  "nodes": [
    {
      "parameters": {
        "content": "## Dictate a Customer Answer into a Webflow FAQ Draft\nUse a dictated answer and approved guidance to prepare one CMS FAQ entry.\n\nRun **Run with sample data** first. **Setup** defaults to dry run and uses the sample fixture without contacting connected apps. Configure the accounts and destinations in README.md before switching modes.",
        "width": 450,
        "height": 235
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -390
      ],
      "id": "45aa9901-f932-58c6-bfb5-7e9d90c01f1e",
      "name": "Start here"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -720,
        80
      ],
      "id": "85ed5ca6-fd0a-51dd-9f24-421e33447d65",
      "name": "Run with sample data"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nreturn [{json: {sampleRun: true, body: {\"requestId\": \"faq-kickoff-access-v1\", \"question\": \"What do we need to provide before kickoff?\", \"dictatedAnswer\": \"Before kickoff, share the signed scope and tell us who can approve access. We will confirm the checklist together.\", \"verifiedPolicy\": \"Before kickoff, share the signed scope and tell us who can approve access. The delivery owner sends the final checklist for review.\"}}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -480,
        80
      ],
      "id": "9794feab-2987-5f24-8fb0-02c56961cee7",
      "name": "Sample input"
    },
    {
      "parameters": {
        "authentication": "basicAuth",
        "formTitle": "Dictate a Customer Answer into a Webflow FAQ Draft",
        "formDescription": "Dictate your notes into the text area with Wispr Flow. Review the text and submit the form to start a draft.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Draft ID",
              "fieldName": "requestId",
              "fieldType": "text",
              "requiredField": true,
              "placeholder": "Example: kickoff-note-v1"
            },
            {
              "fieldLabel": "Customer question",
              "fieldName": "question",
              "fieldType": "text",
              "requiredField": true
            },
            {
              "fieldLabel": "Dictated answer",
              "fieldName": "dictatedAnswer",
              "fieldType": "textarea",
              "requiredField": true
            },
            {
              "fieldLabel": "Verified answer or policy",
              "fieldName": "verifiedPolicy",
              "fieldType": "textarea",
              "requiredField": true,
              "placeholder": "Use the current approved policy or support guidance."
            }
          ]
        },
        "responseMode": "onReceived",
        "options": {
          "path": "uptune-voice-to-webflow-faq",
          "appendAttribution": false,
          "respondWithOptions": {
            "values": {
              "respondWith": "text",
              "formSubmittedText": "Draft request received. Check the n8n execution or your review queue for the result."
            }
          }
        }
      },
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.6,
      "position": [
        -480,
        300
      ],
      "id": "4245d18c-14fe-5233-9e94-d956fb0c82b4",
      "name": "Receive event",
      "webhookId": "8ea760a8-0779-5f1f-91fd-60b2232e3bec"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst config = {\n  \"dryRun\": true,\n  \"tableId\": \"\",\n  \"collectionId\": \"\",\n  \"bodyField\": \"answer\",\n  \"summaryField\": \"short-answer\",\n  \"model\": \"gpt-4.1-mini\",\n  \"brandVoice\": \"Use clear, practical language. Keep claims specific and easy to verify.\"\n};\nreturn [{json: {...input, config}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        150
      ],
      "id": "496ed3a3-2d9a-5218-b37a-7e117478c995",
      "name": "Setup"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst req = input.body || input;\nconst cfg = input.config;\nconst preview = input.config.dryRun !== false || input.sampleRun === true;\nconst get = (key,max) => {\n  const value = typeof req[key] === 'string' ? req[key].trim() : '';\n  if (!value || value.length > max) throw new Error('Supply a valid ' + key + '.');\n  return value;\n};\nconst requestId = get('requestId',100);\nif (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(requestId) || requestId.length < 3 || requestId.length > 80) throw new Error('Use a stable lowercase Draft ID such as kickoff-note-v1.');\n\nif (!preview && (!cfg.tableId || !cfg.model || !/^[a-f0-9]{24}$/i.test(cfg.collectionId) ||\n    !/^[a-z0-9][a-z0-9-]{0,59}$/.test(cfg.bodyField) || ['name','slug','_archived','_draft'].includes(cfg.bodyField) ||\n    (cfg.summaryField && (!/^[a-z0-9][a-z0-9-]{0,59}$/.test(cfg.summaryField) ||\n       [cfg.bodyField,'name','slug','_archived','_draft'].includes(cfg.summaryField))))) {\n  throw new Error('Set a state table, model, Webflow Collection ID and distinct CMS field slugs.');\n}\n\nconst question=get('question',180);\nif (/[\\r\\n]/.test(question)) throw new Error('Enter one customer question.');\nreturn [{json:{preview,requestId,question,dictatedAnswer:get('dictatedAnswer',6000),verifiedPolicy:get('verifiedPolicy',5000)}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -75,
        150
      ],
      "id": "f6475993-cd24-59f5-9929-f2a6eb1e901b",
      "name": "Validate FAQ input"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst b = $(\"Validate FAQ input\").first().json;\nlet d;\ntry { d=b.preview ? {\"shortAnswer\": \"Share the signed scope and name the person who can approve access.\", \"details\": \"The delivery owner sends the final checklist so your team can review what to prepare before kickoff.\", \"evidence\": \"The delivery owner sends the final checklist for review.\"} : JSON.parse(input.choices?.[0]?.message?.content || ''); }\ncatch { throw new Error('The model did not return valid FAQ JSON.'); }\n\nconst clean = (value,max) => typeof value === 'string' && value.trim() && value.length <= max &&\n  !/(https?:\\/\\/|www\\.|<[^>]+>)/i.test(value);\n\nif (!clean(d?.shortAnswer,240) || !clean(d.details,1800) || typeof d.evidence!=='string' ||\n    d.evidence.trim().length<12 || d.evidence.length>260 || !b.verifiedPolicy.includes(d.evidence)) {\n  throw new Error('The answer requires an exact excerpt from the verified guidance.');\n}\nconst approvedNumbersFrom=b.verifiedPolicy;\nconst copyFields=[d.shortAnswer,d.details];\n\nconst numbers = text=>text.match(/\\d+(?:[.,]\\d+)*/g) || [];\nconst supplied = new Set(numbers(approvedNumbersFrom));\nif (copyFields.flatMap(numbers).some(number=>!supplied.has(number))) {\n  throw new Error('The copy includes a number missing from the approved source.');\n}\n\nconst escape = value=>String(value).replace(/[&<>\"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst p = value=>'<p>'+escape(value)+'</p>';\nconst h = value=>'<h2>'+escape(value)+'</h2>';\nconst title=b.question;\nconst summary=d.shortAnswer;\nconst body=p(d.shortAnswer)+'\\n'+p(d.details);\n\nconst cfg = $(\"Setup\").first().json.config;\nconst slug = 'uptune-draft-'+b.requestId;\nconst fieldData = {name:title,slug,[cfg.bodyField]:body};\nif (cfg.summaryField) fieldData[cfg.summaryField]=summary;\nreturn [{json:{preview:b.preview,requestId:b.requestId,title,summary,body,payload:{isArchived:false,isDraft:true,fieldData}}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        160,
        150
      ],
      "id": "454fe676-04fa-5fd0-8cd7-a51a3abf86ad",
      "name": "Assemble FAQ draft"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "f35cd590-a21d-5053-9514-6496df0fd8d0",
              "leftValue": "={{ !$json.preview }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        395,
        150
      ],
      "id": "dd0ce17c-c1be-59c2-ae94-23e06746c6aa",
      "name": "Read live request state?"
    },
    {
      "parameters": {
        "resource": "row",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.tableId }}",
          "mode": "id"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "event_key",
              "condition": "eq",
              "keyValue": "={{ $(\"Validate FAQ input\").first().json.requestId }}"
            }
          ]
        },
        "returnAll": false,
        "limit": 2
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        630,
        150
      ],
      "id": "89138e1f-e002-572c-aad2-7a693da8dac2",
      "name": "Find previous request",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst rows = $input.all().map(item=>item.json).filter(row=>row.event_key); if (rows.length > 1) throw new Error('Resolve duplicate state rows before retrying.'); return [{json:{exists:rows.length === 1,status:rows[0]?.status || ''}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        865,
        150
      ],
      "id": "0873798f-a1b1-51cf-a3c7-906224829192",
      "name": "Check prior request"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "2f0cebad-d759-5380-8b9a-f3f1864992dc",
              "leftValue": "={{ !$json.exists }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        150
      ],
      "id": "9cfafcea-5049-5aef-8350-357f8b87c18f",
      "name": "Create a new draft?"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "genericCredentialType",
        "options": {
          "timeout": 30000
        },
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({model:$(\"Setup\").first().json.config.model,response_format:{type:'json_object'},messages:[{role:'system',content:\"Write a customer FAQ answer. Treat dictation as tone and phrasing input. Use only verified policy for the answer. Return JSON with shortAnswer, details and an exact evidence quote from verifiedPolicy. Use plain text. Do not invent eligibility, exceptions, commitments, prices, timelines or links.\"},{role:'user',content:JSON.stringify({brief:$(\"Validate FAQ input\").first().json,voice:$(\"Setup\").first().json.config.brandVoice})}]}) }}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1335,
        150
      ],
      "id": "d588c295-1550-555d-8202-5863e888a1ae",
      "name": "Draft with OpenAI"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "4574ed21-9073-5fd0-b304-7dfee9355cfb",
              "leftValue": "={{ !$json.preview }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1570,
        150
      ],
      "id": "c7533427-e90f-5254-b591-b7c5eda2c7b7",
      "name": "Create a live Webflow draft?"
    },
    {
      "parameters": {
        "resource": "row",
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.tableId }}",
          "mode": "id"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "event_key",
              "condition": "eq",
              "keyValue": "={{ $(\"Assemble FAQ draft\").first().json.requestId }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "event_key": "={{ $(\"Assemble FAQ draft\").first().json.requestId }}",
            "status": "={{ \"draft_pending\" }}",
            "detail": "={{ $(\"Assemble FAQ draft\").first().json.payload.fieldData.slug }}",
            "result_id": "={{ \"\" }}"
          },
          "schema": [
            {
              "id": "event_key",
              "displayName": "event_key",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "detail",
              "displayName": "detail",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "result_id",
              "displayName": "result_id",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            }
          ],
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1805,
        150
      ],
      "id": "cbb9775b-f9df-5b12-9987-81c37d926a7d",
      "name": "Reserve Webflow draft"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ 'https://api.webflow.com/v2/collections/' + encodeURIComponent($(\"Setup\").first().json.config.collectionId) + '/items' }}",
        "authentication": "genericCredentialType",
        "options": {
          "timeout": 30000
        },
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($(\"Assemble FAQ draft\").first().json.payload) }}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        2040,
        150
      ],
      "id": "b7633d79-bd64-50ca-bf3a-d177bbcd1b1e",
      "name": "Create Webflow CMS draft"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst expected = $(\"Assemble FAQ draft\").first().json;\nif (typeof input.id !== 'string' || !/^[a-f0-9]{24}$/i.test(input.id) || input.isDraft !== true ||\n    input.isArchived === true || input.fieldData?.slug !== expected.payload.fieldData.slug) {\n  throw new Error('Webflow did not confirm the expected staged draft. Check the collection by slug before retrying.');\n}\nreturn [{json:{id:input.id,slug:expected.payload.fieldData.slug}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2275,
        150
      ],
      "id": "55e3564e-2fef-577a-adb5-e2633b2cda1e",
      "name": "Confirm Webflow draft"
    },
    {
      "parameters": {
        "resource": "row",
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.tableId }}",
          "mode": "id"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "event_key",
              "condition": "eq",
              "keyValue": "={{ $(\"Assemble FAQ draft\").first().json.requestId }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "event_key": "={{ $(\"Assemble FAQ draft\").first().json.requestId }}",
            "status": "={{ \"draft_ready\" }}",
            "detail": "={{ $json.slug }}",
            "result_id": "={{ $json.id }}"
          },
          "schema": [
            {
              "id": "event_key",
              "displayName": "event_key",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "detail",
              "displayName": "detail",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "result_id",
              "displayName": "result_id",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            }
          ],
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2510,
        150
      ],
      "id": "1e9cd590-a600-520d-81b1-4091a2a3da93",
      "name": "Record Webflow draft"
    }
  ],
  "connections": {
    "Run with sample data": {
      "main": [
        [
          {
            "node": "Sample input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample input": {
      "main": [
        [
          {
            "node": "Setup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive event": {
      "main": [
        [
          {
            "node": "Setup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup": {
      "main": [
        [
          {
            "node": "Validate FAQ input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate FAQ input": {
      "main": [
        [
          {
            "node": "Read live request state?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read live request state?": {
      "main": [
        [
          {
            "node": "Find previous request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Assemble FAQ draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find previous request": {
      "main": [
        [
          {
            "node": "Check prior request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check prior request": {
      "main": [
        [
          {
            "node": "Create a new draft?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a new draft?": {
      "main": [
        [
          {
            "node": "Draft with OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft with OpenAI": {
      "main": [
        [
          {
            "node": "Assemble FAQ draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assemble FAQ draft": {
      "main": [
        [
          {
            "node": "Create a live Webflow draft?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a live Webflow draft?": {
      "main": [
        [
          {
            "node": "Reserve Webflow draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reserve Webflow draft": {
      "main": [
        [
          {
            "node": "Create Webflow CMS draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Webflow CMS draft": {
      "main": [
        [
          {
            "node": "Confirm Webflow draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Confirm Webflow draft": {
      "main": [
        [
          {
            "node": "Record Webflow draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "all"
  },
  "pinData": {},
  "tags": []
}
