{ "kind": "open-flow-workflow", "version": 1, "name": "Email a Weekly Todoist Review", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_8045665d8179ceeb": { "name": "Run on demand", "kind": "start", "inputDefs": [] }, "n_728cfa05b04f5178": { "name": "Friday at 15:00 UTC", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "0 15 * * 5", "timezone": "UTC" } ] }, "n_c070f3acef81d314": { "inputs": { "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c070f3acef81d314", "output": "state" } ] }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_08769faed3f75a5d", "output": "body" } ] }, "bodyEncoding": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_08769faed3f75a5d", "output": "bodyEncoding" } ] }, "statusCode": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_08769faed3f75a5d", "output": "statusCode" } ] } }, "name": "Collect completed tasks", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": true, "handle": "previousState" }, { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "const": "base64" }, "nullable": true, "handle": "bodyEncoding" }, { "jsonSchema": { "type": "integer" }, "nullable": true, "handle": "statusCode" }, { "jsonSchema": { "type": "number" }, "nullable": true, "handle": "until" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": [ "string", "number", "boolean", "null" ] } }, "nullable": false, "handle": "query" }, { "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "nullable": false, "handle": "items" } ], "name": "Collect completed tasks", "moduleId": "n_328044f67b9aa3d0", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_c51488ee412aeb67": { "inputs": {}, "name": "More completed tasks?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "read", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_c070f3acef81d314", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_08769faed3f75a5d": { "inputs": { "endpoint": { "kind": "value", "value": "/tasks/completed/by_completion_date" }, "query": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c070f3acef81d314", "output": "query" } ] } }, "name": "Read Todoist completed tasks", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "endpoint" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": [ "string", "number", "boolean", "null" ] } }, "nullable": true, "handle": "query" } ], "outputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": false, "handle": "headers" }, { "jsonSchema": { "const": "base64" }, "nullable": true, "handle": "bodyEncoding" } ], "name": "Read Todoist completed tasks", "executor": { "providerId": "todoist", "method": "GET", "query": { "mode": "object", "names": [] }, "headers": { "mode": "fields", "names": [] }, "bodyFormat": "none", "kind": "connector-proxy" } } }, "n_923fcc79a24f9974": { "inputs": { "items": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c070f3acef81d314", "output": "items" } ] }, "ignoredProjects": { "kind": "value", "value": [] } }, "name": "Prepare the email", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "nullable": false, "handle": "items" }, { "jsonSchema": { "type": "array", "items": { "type": "string" } }, "nullable": false, "handle": "ignoredProjects" } ], "outputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "html" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "hasItems" } ], "name": "Prepare the email", "moduleId": "n_c0c8f399a3e217f2", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_d135877a0cb77041": { "inputs": {}, "name": "Any tasks to report?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "send", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_923fcc79a24f9974", "output": "hasItems" } }, "operator": "isTrue" } ] } ] } ] }, "n_7c46416a86306d62": { "inputs": { "to": { "kind": "value", "value": [ "recipient@example.com" ] }, "subject": { "kind": "value", "value": "Todoist Weekly Review" }, "html": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_923fcc79a24f9974", "output": "html" } ] } }, "name": "Send the review", "kind": "task", "task": { "inputs": [ { "description": "The primary recipient email addresses.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "format": "email", "description": "An email address." }, "minItems": 1, "description": "The primary recipient email addresses." }, "nullable": false, "handle": "to" }, { "description": "The carbon-copy recipient email addresses.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "format": "email", "description": "An email address." }, "minItems": 1, "description": "The carbon-copy recipient email addresses." }, "nullable": true, "handle": "cc", "value": null }, { "description": "The blind-carbon-copy recipient email addresses.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "format": "email", "description": "An email address." }, "minItems": 1, "description": "The blind-carbon-copy recipient email addresses." }, "nullable": true, "handle": "bcc", "value": null }, { "description": "An email address.", "jsonSchema": { "type": "string", "minLength": 1, "format": "email", "description": "An email address." }, "nullable": true, "handle": "replyTo", "value": null }, { "description": "The email subject.", "jsonSchema": { "type": "string", "description": "The email subject." }, "nullable": false, "handle": "subject" }, { "description": "The plain text email body.", "jsonSchema": { "type": "string", "description": "The plain text email body." }, "nullable": true, "handle": "text", "value": null }, { "description": "The HTML email body.", "jsonSchema": { "type": "string", "description": "The HTML email body." }, "nullable": true, "handle": "html", "value": null }, { "description": "Attachments to fetch and include in the outgoing email.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "filename": { "type": "string", "minLength": 1, "description": "The filename to use for the outgoing attachment." }, "contentType": { "type": "string", "minLength": 1, "description": "The MIME content type of the attachment." }, "contentUrl": { "type": "string", "format": "uri", "description": "The public HTTP or HTTPS URL to fetch attachment content from." } }, "required": [ "filename", "contentUrl" ], "additionalProperties": false, "description": "An attachment to fetch and include in an outgoing email." }, "minItems": 1, "maxItems": 10, "description": "Attachments to fetch and include in the outgoing email." }, "nullable": true, "handle": "attachments", "value": null } ], "outputs": [ { "description": "The SMTP Message-ID returned by the mail client.", "jsonSchema": { "type": [ "string", "null" ], "description": "The SMTP Message-ID returned by the mail client." }, "nullable": true, "handle": "messageId" }, { "description": "Recipient addresses accepted by the SMTP server.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "format": "email", "description": "An email address." }, "description": "Recipient addresses accepted by the SMTP server." }, "nullable": false, "handle": "accepted" }, { "description": "Recipient addresses rejected by the SMTP server.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "format": "email", "description": "An email address." }, "description": "Recipient addresses rejected by the SMTP server." }, "nullable": false, "handle": "rejected" }, { "description": "The SMTP response text returned by the server.", "jsonSchema": { "type": "string", "description": "The SMTP response text returned by the server." }, "nullable": false, "handle": "response" } ], "name": "Send the review", "executor": { "kind": "connector", "action": "generic_imap.send_email" } } } }, "edges": [ { "source": "n_8045665d8179ceeb", "target": "n_c070f3acef81d314" }, { "source": "n_728cfa05b04f5178", "target": "n_c070f3acef81d314" }, { "source": "n_c070f3acef81d314", "target": "n_c51488ee412aeb67" }, { "source": "n_c51488ee412aeb67", "target": "n_08769faed3f75a5d", "sourceHandle": "read" }, { "source": "n_08769faed3f75a5d", "target": "n_c070f3acef81d314" }, { "source": "n_c51488ee412aeb67", "target": "n_923fcc79a24f9974", "sourceHandle": "otherwise" }, { "source": "n_923fcc79a24f9974", "target": "n_d135877a0cb77041" }, { "source": "n_d135877a0cb77041", "target": "n_7c46416a86306d62", "sourceHandle": "send" } ] } }, "modules": { "n_328044f67b9aa3d0": { "name": "Collect completed tasks", "imports": [], "source": "export default function collect({previousState,body,statusCode,bodyEncoding,until}) {\n let s=previousState;\n if(!s){const t=until==null?Date.now():Number(until);if(!Number.isFinite(t))throw Error('Invalid review time');s={since:new Date(t-604800000).toISOString(),until:new Date(t).toISOString(),items:[],cursors:[],pages:0,cursor:null};}\n else {if(statusCode<200||statusCode>=300)throw Error('Todoist request failed');let b=body;if(typeof b==='string'){try{b=JSON.parse(b)}catch{throw Error('Invalid Todoist response')}}if(!b||!Array.isArray(b.items)||!(b.next_cursor===null||typeof b.next_cursor==='string'))throw Error('Invalid Todoist page');if(b.next_cursor!==null&&(!b.next_cursor||s.cursors.includes(b.next_cursor)))throw Error('Invalid or repeated cursor');s={...s,items:[...s.items,...b.items],pages:s.pages+1,cursor:b.next_cursor,cursors:b.next_cursor?[...s.cursors,b.next_cursor]:s.cursors};if(s.pages>=100&&s.cursor)throw Error('Too many Todoist pages');}\n const more=s.pages===0||s.cursor!==null;return {state:s,more,query:{since:s.since,until:s.until,limit:100,...(s.cursor?{cursor:s.cursor}:{})},items:s.items};\n}\n" }, "n_c0c8f399a3e217f2": { "name": "Prepare the email", "imports": [], "source": "export default function summarize({items,ignoredProjects}) {if(!Array.isArray(items)||!Array.isArray(ignoredProjects)||ignoredProjects.some(x=>typeof x!=='string'))throw Error('Invalid review configuration');const groups={};for(const item of items){if(typeof item.project_id!=='string'||typeof item.content!=='string'||typeof item.completed_at!=='string'||!Number.isFinite(Date.parse(item.completed_at)))throw Error('Invalid completed task');if(ignoredProjects.includes(item.project_id))continue;const date=new Date(item.completed_at).toISOString().slice(0,10);(groups[date]??=[]).push(item.content)}const esc=s=>s.replace(/&/g,'&').replace(//g,'>').replace(/\"/g,'"').replace(/'/g,''');let html='

Completed Items

';for(const [date,contents] of Object.entries(groups))html+=`

${date}

';return {html,hasItems:Object.keys(groups).length>0};}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }