{ "kind": "open-flow-workflow", "version": 1, "name": "Negative Event Feedback to Mattermost", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_ce9293f35ef828cd": { "name": "Receive event feedback", "kind": "integration", "config": { "formId": { "kind": "value", "value": "YOUR_TYPEFORM_FORM_ID" } }, "definition": { "configInputs": [ { "jsonSchema": { "type": "string", "minLength": 1, "maxLength": 128, "title": "Form ID" }, "nullable": false, "handle": "formId" } ], "definitionVersion": 2, "description": "Receives complete Typeform submissions through an automatically managed signed webhook.", "displayName": "Completed Form Response", "key": "typeform.on_form_response", "name": "on_form_response", "outputs": [ { "jsonSchema": { "type": "object", "properties": { "eventId": { "type": "string" }, "form_id": { "type": "string" }, "token": { "type": "string" }, "submitted_at": { "type": "string" }, "answers": { "type": "array", "items": { "type": "object" } }, "definition": { "type": "object" } }, "required": [ "eventId", "form_id", "token", "submitted_at", "answers" ] }, "nullable": false, "handle": "response" } ], "provider": "typeform", "type": "integration", "endpoint": { "body": { "allowArray": false, "allowEmpty": false, "formats": [ "json" ] }, "methods": [ "POST" ], "successStatus": 200 } } }, "n_8a8b9c2da970fc7b": { "inputs": { "response": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ce9293f35ef828cd", "output": "response" } ] }, "questionTitle": { "kind": "value", "value": "What did you think about the event?" } }, "name": "Read the event feedback answer", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "response" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "questionTitle" } ], "outputs": [ { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "feedback" } ], "name": "Read the event feedback answer", "moduleId": "n_67079c7318749ee4", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_d8317bcf3989771b": { "inputs": { "feedback": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_8a8b9c2da970fc7b", "output": "feedback" } ] }, "region": { "kind": "value", "value": "us-east-1" }, "languageCode": { "kind": "value", "value": "en" }, "accessKeyId": { "kind": "unset" }, "secretAccessKey": { "kind": "unset" }, "sessionToken": { "kind": "unset" } }, "name": "Sign the Comprehend request", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "feedback" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "region" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "languageCode" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "accessKeyId" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "secretAccessKey" }, { "jsonSchema": { "type": "string" }, "nullable": true, "handle": "sessionToken" } ], "outputs": [ { "jsonSchema": { "type": "string", "minLength": 1, "format": "uri" }, "nullable": false, "handle": "url" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": false, "handle": "headers" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "body" } ], "name": "Sign the Comprehend request", "moduleId": "n_057edb96a2c7cc6b", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_98e8903d169e9500": { "inputs": { "url": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d8317bcf3989771b", "output": "url" } ] }, "headers": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d8317bcf3989771b", "output": "headers" } ] }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d8317bcf3989771b", "output": "body" } ] } }, "name": "Detect sentiment with Comprehend", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "url" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": true, "handle": "headers" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "body" } ], "outputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": false, "handle": "headers" } ], "name": "Detect sentiment with Comprehend", "executor": { "method": "POST", "query": { "mode": "fields", "names": [] }, "headers": { "mode": "object", "names": [] }, "bodyFormat": "text", "authentication": { "type": "none" }, "responseFormat": "json", "statusPolicy": "success", "kind": "http-request" } } }, "n_15f8228d2bbab41f": { "inputs": { "assessment": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_98e8903d169e9500", "output": "body" } ] }, "feedback": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_8a8b9c2da970fc7b", "output": "feedback" } ] } }, "name": "Check the returned sentiment", "kind": "task", "task": { "inputs": [ { "jsonSchema": {}, "nullable": true, "handle": "assessment" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "feedback" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "assessment" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "negative" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "message" } ], "name": "Check the returned sentiment", "moduleId": "n_ea2fa9b4c52ab563", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_526e2768810bf920": { "inputs": {}, "name": "Notify for negative feedback", "kind": "condition", "matchMode": "first", "cases": [ { "output": "negative", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_15f8228d2bbab41f", "output": "negative" } }, "operator": "==", "right": { "kind": "value", "value": true } } ] } ] } ] }, "n_3717325ae0c9f0b8": { "inputs": { "channelId": { "kind": "value", "value": "YOUR_MATTERMOST_CHANNEL_ID" }, "message": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_15f8228d2bbab41f", "output": "message" } ] }, "props": { "kind": "value", "value": { "attachments": [] } } }, "name": "Post the negative feedback", "kind": "task", "task": { "inputs": [ { "description": "A Mattermost object ID.", "jsonSchema": { "type": "string", "minLength": 1, "description": "A Mattermost object ID." }, "nullable": false, "handle": "channelId" }, { "description": "The Markdown message body to post.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The Markdown message body to post." }, "nullable": false, "handle": "message" }, { "description": "Optional root post ID for replying in a thread.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Optional root post ID for replying in a thread." }, "nullable": true, "handle": "rootId", "value": null }, { "description": "Optional Mattermost post props object.", "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "Optional Mattermost post props object." }, "nullable": true, "handle": "props", "value": null } ], "outputs": [ { "description": "A Mattermost post object.", "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A Mattermost post object." }, "nullable": false, "handle": "post" }, { "description": "The raw Mattermost API JSON response.", "jsonSchema": { "description": "The raw Mattermost API JSON response." }, "nullable": false, "handle": "raw" } ], "name": "Post the negative feedback", "executor": { "kind": "connector", "action": "mattermost.create_post" } } }, "n_4477edba915b6ecc": { "inputs": { "post": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_3717325ae0c9f0b8", "output": "post" } ] }, "assessment": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_15f8228d2bbab41f", "output": "assessment" } ] } }, "name": "Confirm the feedback post", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A Mattermost post object." }, "nullable": false, "handle": "post" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "assessment" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "posted" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "assessment" }, { "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A Mattermost post object." }, "nullable": true, "handle": "post" } ], "name": "Confirm the feedback post", "moduleId": "n_19f0a7ca712c806c", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_ab581cf7815e3b19": { "inputs": { "assessment": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_15f8228d2bbab41f", "output": "assessment" } ] } }, "name": "Keep nonnegative feedback silent", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "assessment" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "posted" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "assessment" }, { "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A Mattermost post object." }, "nullable": true, "handle": "post" } ], "name": "Keep nonnegative feedback silent", "moduleId": "n_a5ac59cffd4b9240", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_792d82f93f643fb5": { "name": "Feedback notification result", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "posted" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "assessment" }, { "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A Mattermost post object." }, "nullable": true, "handle": "post" } ], "outputMappings": { "posted": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_4477edba915b6ecc", "output": "posted" }, { "kind": "node", "nodeId": "n_ab581cf7815e3b19", "output": "posted" } ] }, "assessment": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_4477edba915b6ecc", "output": "assessment" }, { "kind": "node", "nodeId": "n_ab581cf7815e3b19", "output": "assessment" } ] }, "post": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_4477edba915b6ecc", "output": "post" }, { "kind": "node", "nodeId": "n_ab581cf7815e3b19", "output": "post" } ] } } } }, "edges": [ { "source": "n_ce9293f35ef828cd", "target": "n_8a8b9c2da970fc7b" }, { "source": "n_8a8b9c2da970fc7b", "target": "n_d8317bcf3989771b" }, { "source": "n_d8317bcf3989771b", "target": "n_98e8903d169e9500" }, { "source": "n_98e8903d169e9500", "target": "n_15f8228d2bbab41f" }, { "source": "n_15f8228d2bbab41f", "target": "n_526e2768810bf920" }, { "source": "n_526e2768810bf920", "target": "n_ab581cf7815e3b19", "sourceHandle": "otherwise" }, { "source": "n_526e2768810bf920", "target": "n_3717325ae0c9f0b8", "sourceHandle": "negative" }, { "source": "n_3717325ae0c9f0b8", "target": "n_4477edba915b6ecc" }, { "source": "n_4477edba915b6ecc", "target": "n_792d82f93f643fb5" }, { "source": "n_ab581cf7815e3b19", "target": "n_792d82f93f643fb5" } ] } }, "modules": { "n_67079c7318749ee4": { "name": "Read the event feedback answer", "imports": [], "source": "export default function typeformEventFeedback({ response, questionTitle }) {\n if (!response || !Array.isArray(response.answers) || !Array.isArray(response.definition?.fields)) throw new Error('Expected a complete Typeform response');\n if (typeof questionTitle !== 'string' || !questionTitle.trim()) throw new Error('Configure the feedback question title');\n const fields = response.definition.fields.filter(field => field?.title === questionTitle);\n if (fields.length !== 1 || typeof fields[0].id !== 'string' || !fields[0].id || response.definition.fields.filter(field => field?.id === fields[0].id).length !== 1) throw new Error('The feedback question title must identify one field');\n const answers = response.answers.filter(answer => answer?.field?.id === fields[0].id);\n if (answers.length !== 1 || answers[0].type !== 'text' || typeof answers[0].text !== 'string' || !answers[0].text.trim()) throw new Error('One nonblank text feedback answer is required');\n return { feedback: answers[0].text };\n}\n" }, "n_057edb96a2c7cc6b": { "name": "Sign the Comprehend request", "imports": [], "source": "import { createHash, createHmac } from 'node:crypto';\n\nexport default function comprehendSignedRequest({ feedback, region, languageCode, accessKeyId, secretAccessKey, sessionToken }) {\n if (typeof feedback !== 'string' || !feedback.trim()) throw new Error('Feedback text is missing');\n if (typeof region !== 'string' || !/^[a-z]{2}(?:-[a-z]+){1,2}-[0-9]+$/.test(region)) throw new Error('Configure a valid AWS region');\n if (!['en', 'es', 'fr', 'de', 'it', 'pt', 'ar', 'hi', 'ja', 'ko', 'zh', 'zh-TW'].includes(languageCode)) throw new Error('Configure a supported Comprehend language code');\n const required = (value, name) => { if (typeof value !== 'string' || !value.trim()) throw new Error('Select the AWS ' + name + ' secret Variable'); return value.trim(); };\n const keyId = required(accessKeyId, 'access key ID'), secret = required(secretAccessKey, 'secret access key');\n if (sessionToken != null && typeof sessionToken !== 'string') throw new Error('The AWS session token must be text');\n const token = sessionToken?.trim() ?? '', service = 'comprehend', host = service + '.' + region + '.amazonaws.com' + (region.startsWith('cn-') ? '.cn' : '');\n const body = JSON.stringify({ Text: feedback, LanguageCode: languageCode }), timestamp = new Date().toISOString().replace(/[:-]|\\.\\d{3}/g, ''), date = timestamp.slice(0, 8);\n const headers = { 'content-type': 'application/x-amz-json-1.1', host, 'x-amz-date': timestamp, 'x-amz-target': 'Comprehend_20171127.DetectSentiment', ...(token ? { 'x-amz-security-token': token } : {}) };\n const names = Object.keys(headers).sort(), signedHeaders = names.join(';'), canonicalHeaders = names.map(name => name + ':' + headers[name].trim().replace(/\\s+/g, ' ') + '\\n').join('');\n const hash = value => createHash('sha256').update(value).digest('hex'), hmac = (key, value) => createHmac('sha256', key).update(value).digest();\n const scope = date + '/' + region + '/' + service + '/aws4_request', canonical = ['POST', '/', '', canonicalHeaders, signedHeaders, hash(body)].join('\\n');\n const toSign = ['AWS4-HMAC-SHA256', timestamp, scope, hash(canonical)].join('\\n');\n const signingKey = hmac(hmac(hmac(hmac('AWS4' + secret, date), region), service), 'aws4_request'), signature = createHmac('sha256', signingKey).update(toSign).digest('hex');\n delete headers.host;\n headers.authorization = 'AWS4-HMAC-SHA256 Credential=' + keyId + '/' + scope + ', SignedHeaders=' + signedHeaders + ', Signature=' + signature;\n return { url: 'https://' + host + '/', body, headers };\n}\n" }, "n_ea2fa9b4c52ab563": { "name": "Check the returned sentiment", "imports": [], "source": "export default function comprehendFeedbackDecision({ assessment, feedback }) {\n if (!assessment || typeof assessment !== 'object' || Array.isArray(assessment) || !['POSITIVE', 'NEGATIVE', 'NEUTRAL', 'MIXED'].includes(assessment.Sentiment)) throw new Error('Comprehend did not return a valid sentiment');\n for (const name of ['Positive', 'Negative', 'Neutral', 'Mixed']) {\n const score = assessment.SentimentScore?.[name];\n if (typeof score !== 'number' || !Number.isFinite(score) || score < 0 || score > 1) throw new Error('Comprehend did not return a valid ' + name + ' confidence');\n }\n if (typeof feedback !== 'string' || !feedback.trim()) throw new Error('Original feedback text is missing');\n return { assessment, negative: assessment.Sentiment === 'NEGATIVE', message: 'You got new feedback with a score of ' + assessment.SentimentScore.Negative + '. Here is what it says:' + feedback };\n}\n" }, "n_19f0a7ca712c806c": { "name": "Confirm the feedback post", "imports": [], "source": "export default function mattermostFeedbackPosted({ post, assessment }) {\n if (!post || typeof post !== 'object' || Array.isArray(post) || typeof post.id !== 'string' || !post.id.trim()) throw new Error('Mattermost did not return a saved post ID');\n return { posted: true, assessment, post };\n}\n" }, "n_a5ac59cffd4b9240": { "name": "Keep nonnegative feedback silent", "imports": [], "source": "export default function negativeFeedbackSkipped({ assessment }) {\n return { posted: false, assessment, post: null };\n}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }