{ "kind": "open-flow-workflow", "version": 1, "name": "Send Sheet Investment Updates to Discord", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_fac0af56c94d017d": { "name": "Every minute", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "* * * * *", "timezone": "UTC" } ] }, "n_b37aeb9c2c6b691c": { "inputs": { "spreadsheetId": { "kind": "value", "value": "YOUR_SPREADSHEET_ID" }, "range": { "kind": "value", "value": "'Investments'!A:Z" }, "majorDimension": { "kind": "value", "value": "ROWS" }, "valueRenderOption": { "kind": "value", "value": "UNFORMATTED_VALUE" } }, "name": "Read the full sheet", "kind": "task", "task": { "inputs": [ { "description": "The spreadsheet ID.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The spreadsheet ID." }, "nullable": true, "handle": "spreadsheetId", "value": null }, { "description": "The A1 notation of the range to retrieve.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The A1 notation of the range to retrieve." }, "nullable": false, "handle": "range" }, { "description": "The major dimension for the returned values.", "jsonSchema": { "type": "string", "enum": [ "DIMENSION_UNSPECIFIED", "ROWS", "COLUMNS" ], "description": "The major dimension for the returned values." }, "nullable": true, "handle": "majorDimension", "value": null }, { "description": "How values should be rendered in the output.", "jsonSchema": { "type": "string", "enum": [ "FORMATTED_VALUE", "UNFORMATTED_VALUE", "FORMULA" ], "description": "How values should be rendered in the output." }, "nullable": true, "handle": "valueRenderOption", "value": null }, { "description": "How dates and times should be rendered in the output.", "jsonSchema": { "type": "string", "enum": [ "SERIAL_NUMBER", "FORMATTED_STRING" ], "description": "How dates and times should be rendered in the output." }, "nullable": true, "handle": "dateTimeRenderOption", "value": null } ], "outputs": [ { "description": "The A1 notation of the range.", "jsonSchema": { "type": "string", "description": "The A1 notation of the range." }, "nullable": false, "handle": "range" }, { "description": "The major dimension of the values (ROWS or COLUMNS).", "jsonSchema": { "type": "string", "description": "The major dimension of the values (ROWS or COLUMNS)." }, "nullable": false, "handle": "majorDimension" }, { "description": "The data values in the range.", "jsonSchema": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data values in the range." }, "nullable": false, "handle": "values" } ], "name": "Read the full sheet", "executor": { "kind": "connector", "action": "googlesheets.values_get" } } }, "n_d0df5f617be7016d": { "inputs": { "values": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_b37aeb9c2c6b691c", "output": "values" } ] } }, "name": "Detect changes and format the table", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "array", "items": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "number", "description": "A union variant." }, { "type": "boolean", "description": "A union variant." }, { "const": null, "description": "A union variant." } ], "description": "The cell value schema." }, "description": "An array item." }, "description": "The data values in the range." }, "nullable": false, "handle": "values" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "changed" }, { "jsonSchema": { "type": "string", "maxLength": 2000, "description": "The message content." }, "nullable": true, "handle": "text" } ], "name": "Detect changes and format the table", "moduleId": "n_c172856b857e4861", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_5d9c6ada1892dfcd": { "inputs": {}, "name": "Any watched change?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "yes", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_d0df5f617be7016d", "output": "changed" } }, "operator": "isTrue" } ] } ] } ] }, "n_97b7c1d218a0f4a2": { "inputs": { "channel_id": { "kind": "value", "value": "YOUR_DISCORD_CHANNEL_ID" }, "content": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d0df5f617be7016d", "output": "text" } ] }, "allowed_mentions": { "kind": "value", "value": { "parse": [] } } }, "name": "Send the table to Discord", "kind": "task", "task": { "inputs": [ { "description": "The channel id.", "jsonSchema": { "type": "string", "description": "The channel id." }, "nullable": false, "handle": "channel_id" }, { "description": "The message content.", "jsonSchema": { "type": "string", "maxLength": 2000, "description": "The message content." }, "nullable": true, "handle": "content", "value": null }, { "description": "The embeds to include.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A JSON-like object with arbitrary Discord payload fields." }, "maxItems": 10, "description": "The embeds to include." }, "nullable": true, "handle": "embeds", "value": null }, { "description": "The components to include.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A JSON-like object with arbitrary Discord payload fields." }, "maxItems": 5, "description": "The components to include." }, "nullable": true, "handle": "components", "value": null }, { "description": "The sticker ids to attach.", "jsonSchema": { "type": "array", "items": { "type": "string", "description": "A non-empty string." }, "maxItems": 3, "description": "The sticker ids to attach." }, "nullable": true, "handle": "sticker_ids", "value": null }, { "description": "The attachment metadata to include.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A JSON-like object with arbitrary Discord payload fields." }, "description": "The attachment metadata to include." }, "nullable": true, "handle": "attachments", "value": null }, { "description": "The allowed mentions configuration for a Discord message.", "jsonSchema": { "type": "object", "properties": { "parse": { "type": "array", "items": { "type": "string", "enum": [ "roles", "users", "everyone" ], "description": "A supported mention target." }, "maxItems": 3, "description": "The mention targets to parse automatically." }, "roles": { "type": "array", "items": { "type": "string", "description": "A Discord snowflake identifier." }, "description": "The role ids allowed to be mentioned." }, "users": { "type": "array", "items": { "type": "string", "description": "A Discord snowflake identifier." }, "description": "The user ids allowed to be mentioned." }, "replied_user": { "type": "boolean", "description": "Whether the replied user should be mentioned." } }, "additionalProperties": true, "description": "The allowed mentions configuration for a Discord message." }, "nullable": true, "handle": "allowed_mentions", "value": null }, { "description": "A Discord message reference.", "jsonSchema": { "type": "object", "properties": { "message_id": { "type": "string", "description": "The referenced message id." }, "channel_id": { "type": "string", "description": "The referenced channel id." }, "guild_id": { "type": "string", "description": "The referenced guild id." }, "fail_if_not_exists": { "type": "boolean", "description": "Whether the request should fail if the reference does not exist." }, "type": { "type": "integer", "description": "The reference type." } }, "additionalProperties": true, "description": "A Discord message reference." }, "nullable": true, "handle": "message_reference", "value": null }, { "description": "Whether the message is a text-to-speech message.", "jsonSchema": { "type": "boolean", "description": "Whether the message is a text-to-speech message." }, "nullable": true, "handle": "tts", "value": null }, { "description": "The message flags.", "jsonSchema": { "type": "integer", "description": "The message flags." }, "nullable": true, "handle": "flags", "value": null }, { "description": "The message nonce.", "jsonSchema": { "anyOf": [ { "type": "integer", "description": "A numeric nonce." }, { "type": "string", "maxLength": 25, "minLength": 1, "description": "A string nonce." } ], "description": "The message nonce." }, "nullable": true, "handle": "nonce", "value": null }, { "description": "Whether nonce enforcement is enabled.", "jsonSchema": { "type": "boolean", "description": "Whether nonce enforcement is enabled." }, "nullable": true, "handle": "enforce_nonce", "value": null }, { "description": "The optional poll payload.", "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "The optional poll payload." }, "nullable": true, "handle": "poll", "value": null }, { "description": "The optional shared client theme payload.", "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "The optional shared client theme payload." }, "nullable": true, "handle": "shared_client_theme", "value": null } ], "outputs": [ { "description": "A Discord message object.", "jsonSchema": { "type": "object", "properties": { "id": { "type": "string", "description": "The message id." }, "channel_id": { "type": "string", "description": "The channel id." }, "content": { "type": "string", "description": "The message content." } }, "required": [ "id", "channel_id" ], "additionalProperties": true, "description": "A Discord message object." }, "nullable": false, "handle": "output" } ], "name": "Send the table to Discord", "executor": { "kind": "connector", "action": "discordbot.create_message", "outputMode": "value" } } } }, "edges": [ { "source": "n_fac0af56c94d017d", "target": "n_b37aeb9c2c6b691c" }, { "source": "n_b37aeb9c2c6b691c", "target": "n_d0df5f617be7016d" }, { "source": "n_d0df5f617be7016d", "target": "n_5d9c6ada1892dfcd" }, { "source": "n_5d9c6ada1892dfcd", "target": "n_97b7c1d218a0f4a2", "sourceHandle": "yes" } ] } }, "modules": { "n_c172856b857e4861": { "name": "Detect changes and format the table", "imports": [], "source": "function PLAN({values,previous}){\n if(!Array.isArray(values))throw Error('Expected full sheet values');\n if(!values.length)return{snapshot:[],changed:false,text:'unused'};\n const headers=values[0];if(!Array.isArray(headers))throw Error('Missing header row');const names=['Security Code','Price','Quantity'],indexes=names.map(n=>headers.indexOf(n));if(indexes.some((v,i)=>v<0||headers.lastIndexOf(names[i])!==v))throw Error('Sheet needs unique Security Code, Price and Quantity headers');\n const rows=values.slice(1).map(row=>{if(!Array.isArray(row))throw Error('Malformed sheet row');return names.map((_,i)=>String(row[indexes[i]]??''))});\n const current=rows.map(r=>r[0]);if(previous!==null&&(!Array.isArray(previous)||previous.some(x=>typeof x!=='string')))throw Error('Invalid saved sheet state');\n const changed=previous!==null&¤t.some((v,i)=>i>=previous.length||v!==previous[i]);\n if(!changed)return{snapshot:current,changed:false,text:'unused'};\n const printable=rows.filter(r=>r.some(Boolean));const safe=s=>s.replace(/[\\r\\n\\t]/g,' ').replace(/`/g,'ˋ').replace(/@/g,'@\\u200b');const all=[names,...printable].map(row=>row.map(safe)),width=names.map((_,i)=>Math.max(...all.map(r=>r[i].length)));\n const text='```\\n'+all.map(row=>row.map((s,i)=>s.padEnd(width[i])).join(' ')).join('\\n')+'\\n```';if(text.length>2000)throw Error('Table exceeds Discord message limit');return{snapshot:current,changed:true,text};\n}\n\nexport default async function inspect({values},context){const previous=await context.state.get('security-codes');const result=PLAN({values,previous});await context.state.set('security-codes',result.snapshot);return{changed:result.changed,text:result.text}}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }