{ "name": "Analyze the sentiment of feedback and send a message on Mattermost", "nodes": [ { "name": "Typeform Trigger", "type": "n8n-nodes-base.typeformTrigger", "position": [ 510, 260 ], "parameters": { "formId": "YOUR_TYPEFORM_FORM_ID", "simplifyAnswers": false, "onlyAnswers": false }, "typeVersion": 1, "id": "negative-feedback-alert-source-0" }, { "name": "Google Cloud Natural Language", "type": "n8n-nodes-base.googleCloudNaturalLanguage", "position": [ 710, 260 ], "parameters": { "resource": "document", "operation": "analyzeSentiment", "source": "content", "content": "={{ $json.feedback }}", "options": { "documentType": "PLAIN_TEXT", "encodingType": "UTF16" } }, "typeVersion": 1, "id": "negative-feedback-alert-source-1" }, { "name": "IF", "type": "n8n-nodes-base.if", "position": [ 910, 260 ], "parameters": { "conditions": { "number": [ { "value1": "={{ $('Validate Sentiment Score').first().json.score }}", "operation": "smaller", "value2": 0 } ] }, "combineOperation": "all" }, "typeVersion": 1, "id": "negative-feedback-alert-source-2" }, { "name": "Mattermost", "type": "n8n-nodes-base.mattermost", "position": [ 1110, 160 ], "parameters": { "resource": "message", "operation": "post", "channelId": "YOUR_MATTERMOST_CHANNEL_ID", "message": "={{ $('Prepare Feedback Destinations').first().json.message }}", "attachments": [], "otherOptions": {} }, "typeVersion": 1, "id": "negative-feedback-alert-source-3" }, { "name": "NoOp", "type": "n8n-nodes-base.noOp", "position": [ 1110, 360 ], "parameters": {}, "typeVersion": 1, "id": "negative-feedback-alert-source-4" }, { "id": "read-feedback-answers", "name": "Read Feedback Answers", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 100, 0 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const prepare = function prepareFeedback(inputs) {\n const root = inputs.response;\n const response = root?.form_response ?? root;\n if (!response || typeof response !== 'object' || Array.isArray(response)) throw new Error('Expected a completed Typeform response.');\n if (!Array.isArray(response.answers) || !Array.isArray(response.definition?.fields)) throw new Error('The Typeform response must include answer and field definitions.');\n const requiredTitles = [inputs.feedbackTitle, inputs.nameTitle].filter(Boolean);\n const fields = new Map();\n for (const field of response.definition.fields) {\n if (typeof field?.id !== 'string' || typeof field.title !== 'string') continue;\n const title = field.title.trim();\n if (!requiredTitles.includes(title)) continue;\n if (fields.has(title)) throw new Error('Required Typeform question titles must be unique: ' + title);\n fields.set(title, field.id);\n }\n const readText = (title) => {\n const id = fields.get(title);\n if (!id) throw new Error('Missing Typeform question: ' + title);\n const answers = response.answers.filter((answer) => answer?.field?.id === id);\n if (answers.length !== 1 || answers[0].type !== 'text' || typeof answers[0].text !== 'string' || !answers[0].text.trim()) throw new Error('Expected one nonempty text answer for: ' + title);\n return answers[0].text;\n };\n const feedback = readText(inputs.feedbackTitle);\n const name = inputs.nameTitle ? readText(inputs.nameTitle) : '';\n return { feedback, name, request: { document: { type: 'PLAIN_TEXT', content: feedback }, encodingType: 'UTF16' } };\n}\n\nreturn [{json:prepare({response:$input.first().json,feedbackTitle:\"What did you think about the event?\",nameTitle:\"\"}),pairedItem:{item:0}}];\n" } }, { "id": "validate-feedback-score", "name": "Validate Sentiment Score", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 500, 0 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const validate = function readSentiment(inputs) {\n const body = inputs.body;\n const score = body?.documentSentiment?.score;\n if (!body || typeof body !== 'object' || Array.isArray(body) || body.error || typeof score !== 'number' || !Number.isFinite(score) || score < -1 || score > 1) throw new Error('The sentiment service did not return a valid document score.');\n return { score };\n}\n\nreturn [{json:validate({body:$input.first().json}),pairedItem:{item:0}}];\n" } }, { "id": "prepare-feedback-destinations", "name": "Prepare Feedback Destinations", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 700, 0 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const format = function prepareDestinations(inputs) {\n const { feedback, name, score } = inputs;\n if (typeof feedback !== 'string' || typeof name !== 'string' || typeof score !== 'number' || !Number.isFinite(score)) throw new Error('Expected feedback, name, and a sentiment score.');\n return {\n message: `You got a new feedback with a score of ${score}. Here is what it says:${feedback}`,\n properties: { Name: { title: [{ text: { content: name } }] }, Feedback: { rich_text: [{ text: { content: feedback } }] } },\n attachments: [{ text: feedback, title: `${name} ${score}` }],\n cardName: `Score: ${score}`,\n cardDescription: `Score: ${score}\\nFeedback: ${feedback}\\nUser: ${name}`,\n };\n}\n\nreturn [{json:format({...$('Read Feedback Answers').first().json,score:$input.first().json.score}),pairedItem:{item:0}}];\n" } } ], "active": false, "settings": { "executionOrder": "v1" }, "connections": { "IF": { "main": [ [ { "node": "Mattermost", "type": "main", "index": 0 } ], [ { "node": "NoOp", "type": "main", "index": 0 } ] ] }, "Typeform Trigger": { "main": [ [ { "node": "Read Feedback Answers", "type": "main", "index": 0 } ] ] }, "Google Cloud Natural Language": { "main": [ [ { "node": "Validate Sentiment Score", "type": "main", "index": 0 } ] ] }, "Read Feedback Answers": { "main": [ [ { "node": "Google Cloud Natural Language", "type": "main", "index": 0 } ] ] }, "Validate Sentiment Score": { "main": [ [ { "node": "Prepare Feedback Destinations", "type": "main", "index": 0 } ] ] }, "Prepare Feedback Destinations": { "main": [ [ { "node": "IF", "type": "main", "index": 0 } ] ] } }, "pinData": {} }