{ "kind": "open-flow-workflow", "version": 1, "name": "Feedback Sentiment Routing", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_0c1263e123f98e33": { "name": "Completed Typeform response", "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_c13480e2e88c35c5": { "inputs": { "response": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_0c1263e123f98e33", "output": "response" } ] }, "feedbackTitle": { "kind": "value", "value": "Any suggestions for us?" }, "nameTitle": { "kind": "value", "value": "Name" } }, "name": "Read feedback answers", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "response" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "feedbackTitle" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "nameTitle" } ], "outputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "feedback" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "name" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "request" } ], "name": "Read feedback answers", "moduleId": "n_9a7278393048551e", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_4edc822c486d3a8d": { "inputs": { "url": { "kind": "value", "value": "https://language.googleapis.com/v1/documents:analyzeSentiment" }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c13480e2e88c35c5", "output": "request" } ] }, "auth.token": { "kind": "unset" } }, "name": "Analyze feedback sentiment", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "url" }, { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "auth.token" } ], "outputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": false, "handle": "headers" } ], "name": "Analyze feedback sentiment", "executor": { "method": "POST", "query": { "mode": "fields", "names": [] }, "headers": { "mode": "fields", "names": [] }, "bodyFormat": "json", "authentication": { "type": "apiKey", "name": "x-goog-api-key", "in": "header" }, "responseFormat": "json", "statusPolicy": "success", "kind": "http-request" } } }, "n_7e62f141da553ace": { "inputs": { "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_4edc822c486d3a8d", "output": "body" } ] } }, "name": "Validate sentiment score", "kind": "task", "task": { "inputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" } ], "outputs": [ { "jsonSchema": { "type": "number", "minimum": -1, "maximum": 1 }, "nullable": false, "handle": "score" } ], "name": "Validate sentiment score", "moduleId": "n_3e6398fcc0b4e371", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_78f6fe798f40c343": { "inputs": { "feedback": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c13480e2e88c35c5", "output": "feedback" } ] }, "name": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c13480e2e88c35c5", "output": "name" } ] }, "score": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_7e62f141da553ace", "output": "score" } ] } }, "name": "Prepare feedback message", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "feedback" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "name" }, { "jsonSchema": { "type": "number", "minimum": -1, "maximum": 1 }, "nullable": false, "handle": "score" } ], "outputs": [ { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "message" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "properties" }, { "jsonSchema": { "type": "array", "items": { "type": "object" }, "minItems": 1 }, "nullable": false, "handle": "attachments" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "cardName" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "cardDescription" } ], "name": "Prepare feedback message", "moduleId": "n_45b92f0316d719e4", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_2ea4a42c113c2bf1": { "inputs": {}, "name": "Positive feedback", "kind": "condition", "matchMode": "first", "cases": [ { "output": "positive", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_7e62f141da553ace", "output": "score" } }, "operator": ">", "right": { "kind": "value", "value": 0 } } ] } ] } ] }, "n_c113c654277619b0": { "inputs": { "parent": { "kind": "value", "value": { "data_source_id": "YOUR_NOTION_DATA_SOURCE_ID" } }, "properties": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_78f6fe798f40c343", "output": "properties" } ] } }, "name": "Save positive feedback in Notion", "kind": "task", "task": { "inputs": [ { "description": "The parent page ID.", "jsonSchema": { "type": "string", "description": "The parent page ID." }, "nullable": true, "handle": "parentId", "value": null }, { "description": "The page title.", "jsonSchema": { "type": "string", "description": "The page title." }, "nullable": true, "handle": "title", "value": null }, { "description": "The official Notion parent object payload. Use { page_id: \"...\" } for a child page under an existing page, { data_source_id: \"...\" } for a data source row, or { workspace: true } to create a workspace-level private page when the credential is an OAuth public integration. When parent is provided, do not use the top-level title field; set the title through properties.title instead. Internal integration secrets usually cannot create workspace-level private pages and should use a parent page or data source.", "jsonSchema": { "type": "object", "additionalProperties": { "description": "A Notion parent field value." }, "description": "The official Notion parent object payload. Use { page_id: \"...\" } for a child page under an existing page, { data_source_id: \"...\" } for a data source row, or { workspace: true } to create a workspace-level private page when the credential is an OAuth public integration. When parent is provided, do not use the top-level title field; set the title through properties.title instead. Internal integration secrets usually cannot create workspace-level private pages and should use a parent page or data source." }, "nullable": true, "handle": "parent", "value": null }, { "description": "The page properties.", "jsonSchema": { "type": "object", "additionalProperties": { "description": "A Notion page property value." }, "description": "The page properties." }, "nullable": true, "handle": "properties", "value": null }, { "description": "The child blocks to add.", "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": { "description": "A Notion API field value." }, "description": "A Notion block object." }, "description": "The child blocks to add." }, "nullable": true, "handle": "children", "value": null }, { "description": "Enhanced Markdown content to create page content. Mutually exclusive with children. Markdown-only private pages without parent are supported only for OAuth public connections or personal access tokens; internal integration secrets should provide a parent page or data source.", "jsonSchema": { "type": "string", "description": "Enhanced Markdown content to create page content. Mutually exclusive with children. Markdown-only private pages without parent are supported only for OAuth public connections or personal access tokens; internal integration secrets should provide a parent page or data source." }, "nullable": true, "handle": "markdown", "value": null }, { "description": "The data source template payload to apply to the created page.", "jsonSchema": { "type": "object", "additionalProperties": { "description": "A Notion template field value." }, "description": "The data source template payload to apply to the created page." }, "nullable": true, "handle": "template", "value": null }, { "description": "The page icon object.", "jsonSchema": { "type": "object", "additionalProperties": { "description": "A Notion icon field value." }, "description": "The page icon object." }, "nullable": true, "handle": "icon", "value": null }, { "description": "The page cover object.", "jsonSchema": { "type": "object", "additionalProperties": { "description": "A Notion cover field value." }, "description": "The page cover object." }, "nullable": true, "handle": "cover", "value": null } ], "outputs": [ { "description": "The created Notion page object returned by the API.", "jsonSchema": { "type": "object", "properties": { "object": { "const": "page", "description": "The Notion object type." }, "id": { "type": "string", "description": "The page ID." }, "created_time": { "type": "string", "format": "date-time", "description": "The time when the page was created." }, "last_edited_time": { "type": "string", "format": "date-time", "description": "The time when the page was last edited." }, "created_by": { "type": "object", "properties": { "object": { "const": "user", "description": "The Notion object type." }, "id": { "type": "string", "description": "The Notion user ID." }, "name": { "type": [ "string", "null" ], "description": "The user's display name, or null when unavailable." }, "avatar_url": { "type": [ "string", "null" ], "description": "The user's avatar URL, or null when unavailable." }, "type": { "type": "string", "enum": [ "person", "bot" ], "description": "The user type returned by Notion." }, "person": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "description": "The person's email address." } }, "additionalProperties": true, "description": "Details about a person user." }, "bot": { "type": "object", "additionalProperties": { "description": "A Notion API field value." }, "description": "Details about a bot user." } }, "additionalProperties": true, "description": "A Notion user reference." }, "last_edited_by": { "type": "object", "properties": { "object": { "const": "user", "description": "The Notion object type." }, "id": { "type": "string", "description": "The Notion user ID." }, "name": { "type": [ "string", "null" ], "description": "The user's display name, or null when unavailable." }, "avatar_url": { "type": [ "string", "null" ], "description": "The user's avatar URL, or null when unavailable." }, "type": { "type": "string", "enum": [ "person", "bot" ], "description": "The user type returned by Notion." }, "person": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "description": "The person's email address." } }, "additionalProperties": true, "description": "Details about a person user." }, "bot": { "type": "object", "additionalProperties": { "description": "A Notion API field value." }, "description": "Details about a bot user." } }, "additionalProperties": true, "description": "A Notion user reference." }, "cover": { "type": [ "object", "null" ], "additionalProperties": { "description": "A Notion API field value." }, "description": "The page cover object, or null when the page has no cover." }, "icon": { "type": [ "object", "null" ], "additionalProperties": { "description": "A Notion API field value." }, "description": "The page icon object, or null when the page has no icon." }, "parent": { "type": "object", "properties": { "type": { "type": "string", "description": "The parent type, such as workspace, page_id, data_source_id, database_id, or block_id." } }, "additionalProperties": true, "description": "The parent object that contains this Notion object." }, "is_archived": { "type": "boolean", "description": "Whether the page has been archived." }, "in_trash": { "type": "boolean", "description": "Whether the page is in the trash." }, "is_locked": { "type": "boolean", "description": "Whether the page is locked." }, "properties": { "type": "object", "additionalProperties": { "description": "A Notion property value or property schema object." }, "description": "The Notion page, database, or data source properties keyed by property name." }, "url": { "type": "string", "format": "uri", "description": "The canonical Notion URL for the page." }, "public_url": { "type": [ "string", "null" ], "format": "uri", "description": "The public Notion URL if the page is published to the web." } }, "additionalProperties": true, "description": "The created Notion page object returned by the API." }, "nullable": false, "handle": "output" } ], "name": "Save positive feedback in Notion", "executor": { "kind": "connector", "action": "notion.create_page", "outputMode": "value" } } }, "n_af821823ba1f12a7": { "inputs": { "channelId": { "kind": "value", "value": "YOUR_SLACK_CHANNEL_ID" }, "attachments": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_78f6fe798f40c343", "output": "attachments" } ] } }, "name": "Notify Slack after saving", "kind": "task", "task": { "inputs": [ { "description": "The Slack conversation or channel ID.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The Slack conversation or channel ID." }, "nullable": false, "handle": "channelId" }, { "description": "Plain text message content. When blocks are provided, Slack uses this as notification and accessibility fallback text.", "jsonSchema": { "type": "string", "description": "Plain text message content. When blocks are provided, Slack uses this as notification and accessibility fallback text." }, "nullable": true, "handle": "text", "value": null }, { "description": "Slack Block Kit blocks to render in the message.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A Slack Block Kit block object. Pass the block exactly as Slack documents it." }, "minItems": 1, "description": "Slack Block Kit blocks to render in the message." }, "nullable": true, "handle": "blocks", "value": null }, { "description": "Slack legacy attachments to include in the message.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A Slack legacy attachment object. Prefer blocks for new messages when possible." }, "minItems": 1, "description": "Slack legacy attachments to include in the message." }, "nullable": true, "handle": "attachments", "value": null }, { "description": "Whether Slack should unfurl links in the message.", "jsonSchema": { "type": "boolean", "description": "Whether Slack should unfurl links in the message." }, "nullable": true, "handle": "unfurlLinks", "value": null }, { "description": "Whether Slack should unfurl media in the message.", "jsonSchema": { "type": "boolean", "description": "Whether Slack should unfurl media in the message." }, "nullable": true, "handle": "unfurlMedia", "value": null }, { "description": "Slack message metadata to attach to the message.", "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "Slack message metadata to attach to the message." }, "nullable": true, "handle": "metadata", "value": null } ], "outputs": [ { "description": "The timestamp identifier of the posted message.", "jsonSchema": { "type": "string", "description": "The timestamp identifier of the posted message." }, "nullable": false, "handle": "ts" }, { "description": "The channel ID where the message was posted.", "jsonSchema": { "type": "string", "description": "The channel ID where the message was posted." }, "nullable": false, "handle": "channelId" } ], "name": "Notify Slack after saving", "executor": { "kind": "connector", "action": "slack.post_message" } } }, "n_b31f4717a6760b49": { "inputs": { "listId": { "kind": "value", "value": "YOUR_TRELLO_LIST_ID" }, "name": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_78f6fe798f40c343", "output": "cardName" } ] }, "description": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_78f6fe798f40c343", "output": "cardDescription" } ] } }, "name": "Create feedback review card", "kind": "task", "task": { "inputs": [ { "description": "Trello list ID that will contain the new card.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Trello list ID that will contain the new card." }, "nullable": false, "handle": "listId" }, { "description": "Card name.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Card name." }, "nullable": false, "handle": "name" }, { "description": "Card description.", "jsonSchema": { "type": "string", "description": "Card description." }, "nullable": true, "handle": "description", "value": null }, { "description": "Card due date in ISO 8601 format.", "jsonSchema": { "type": [ "string", "null" ], "format": "date-time", "description": "Card due date in ISO 8601 format." }, "nullable": true, "handle": "due", "value": null }, { "description": "Card position. Use top, bottom, or a numeric position.", "jsonSchema": { "anyOf": [ { "type": "string", "enum": [ "top", "bottom" ], "description": "Named card position." }, { "type": "number", "description": "Numeric card position." } ], "description": "Card position. Use top, bottom, or a numeric position." }, "nullable": true, "handle": "position", "value": null }, { "description": "Trello member IDs to assign to the card.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "description": "One Trello member ID." }, "minItems": 1, "description": "Trello member IDs to assign to the card." }, "nullable": true, "handle": "memberIds", "value": null }, { "description": "Trello label IDs to assign to the card.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "description": "One Trello label ID." }, "minItems": 1, "description": "Trello label IDs to assign to the card." }, "nullable": true, "handle": "labelIds", "value": null } ], "outputs": [ { "description": "A normalized Trello card returned by the REST API.", "jsonSchema": { "type": "object", "properties": { "id": { "type": "string", "description": "Trello card ID." }, "boardId": { "type": "string", "description": "Trello board ID that owns the card." }, "listId": { "type": "string", "description": "Trello list ID that contains the card." }, "name": { "type": "string", "description": "Card name." }, "description": { "type": "string", "description": "Card description." }, "url": { "type": "string", "format": "uri", "description": "Public Trello card URL." }, "shortUrl": { "type": "string", "format": "uri", "description": "Short Trello card URL." }, "closed": { "type": "boolean", "description": "Whether the card is closed." }, "due": { "type": [ "string", "null" ], "format": "date-time", "description": "Card due date in ISO 8601 format." }, "dueComplete": { "type": "boolean", "description": "Whether the card due date is marked complete." } }, "additionalProperties": true, "description": "A normalized Trello card returned by the REST API." }, "nullable": false, "handle": "card" } ], "name": "Create feedback review card", "executor": { "kind": "connector", "action": "trello.create_card" } } }, "n_156730a51c4891fb": { "name": "Analyzed feedback", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "feedback" }, { "jsonSchema": { "type": "number", "minimum": -1, "maximum": 1 }, "nullable": false, "handle": "score" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "name" } ], "outputMappings": { "feedback": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c13480e2e88c35c5", "output": "feedback" } ] }, "score": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_7e62f141da553ace", "output": "score" } ] }, "name": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c13480e2e88c35c5", "output": "name" } ] } } } }, "edges": [ { "source": "n_0c1263e123f98e33", "target": "n_c13480e2e88c35c5" }, { "source": "n_c13480e2e88c35c5", "target": "n_4edc822c486d3a8d" }, { "source": "n_4edc822c486d3a8d", "target": "n_7e62f141da553ace" }, { "source": "n_7e62f141da553ace", "target": "n_78f6fe798f40c343" }, { "source": "n_78f6fe798f40c343", "target": "n_2ea4a42c113c2bf1" }, { "source": "n_2ea4a42c113c2bf1", "target": "n_c113c654277619b0", "sourceHandle": "positive" }, { "source": "n_c113c654277619b0", "target": "n_af821823ba1f12a7" }, { "source": "n_af821823ba1f12a7", "target": "n_156730a51c4891fb" }, { "source": "n_2ea4a42c113c2bf1", "target": "n_b31f4717a6760b49", "sourceHandle": "otherwise" }, { "source": "n_b31f4717a6760b49", "target": "n_156730a51c4891fb" } ] } }, "modules": { "n_9a7278393048551e": { "name": "Read feedback answers", "imports": [], "source": "export default 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" }, "n_3e6398fcc0b4e371": { "name": "Validate sentiment score", "imports": [], "source": "export default 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" }, "n_45b92f0316d719e4": { "name": "Prepare feedback message", "imports": [], "source": "export default 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" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }