{ "kind": "open-flow-workflow", "version": 1, "name": "Google Contacts Birthday Reminders to Slack", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_e0d51b826febd141": { "name": "Every day at 08:00 UTC", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "0 8 * * *", "timezone": "UTC" } ] }, "n_11ccad4658dd7dde": { "inputs": { "clientId": { "kind": "unset" }, "clientSecret": { "kind": "unset" }, "refreshToken": { "kind": "unset" }, "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_11ccad4658dd7dde", "output": "state" } ] } }, "name": "Read every contacts page", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "clientId" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "clientSecret" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "refreshToken" }, { "jsonSchema": { "type": "object", "properties": { "people": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "cursor": { "type": [ "string", "null" ] }, "seen": { "type": "array", "items": { "type": "string" } } }, "required": [ "people", "cursor", "seen" ], "additionalProperties": false }, "nullable": true, "handle": "previousState" } ], "outputs": [ { "jsonSchema": { "type": "object", "properties": { "people": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "cursor": { "type": [ "string", "null" ] }, "seen": { "type": "array", "items": { "type": "string" } } }, "required": [ "people", "cursor", "seen" ], "additionalProperties": false }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" } ], "name": "Read every contacts page", "moduleId": "n_0658d3e7a82df6fa", "capabilities": [ { "kind": "http", "hosts": [ "oauth2.googleapis.com", "people.googleapis.com" ] } ] } }, "n_8bfd8902ec4923a3": { "inputs": {}, "name": "More contacts pages?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "more", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_11ccad4658dd7dde", "output": "more" } }, "operator": "==", "right": { "kind": "value", "value": true } } ] } ] } ] }, "n_45eff0fd79b6c792": { "inputs": { "state": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_11ccad4658dd7dde", "output": "state" } ] }, "scheduledAt": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_e0d51b826febd141", "output": "scheduledAt" } ] } }, "name": "Find today’s birthdays", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "properties": { "people": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "cursor": { "type": [ "string", "null" ] }, "seen": { "type": "array", "items": { "type": "string" } } }, "required": [ "people", "cursor", "seen" ], "additionalProperties": false }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "scheduledAt" } ], "outputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "nullable": false, "handle": "matches" } ], "name": "Find today’s birthdays", "moduleId": "n_cfe69fec56d4f932", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_ff87bc6b6d08a7b3": { "inputs": { "matches": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_45eff0fd79b6c792", "output": "matches" } ] }, "channelId": { "kind": "value", "value": "YOUR_SLACK_CHANNEL_ID" }, "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ff87bc6b6d08a7b3", "output": "state" } ] } }, "name": "Prepare the next reminder", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "nullable": false, "handle": "matches" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "channelId" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": true, "handle": "previousState" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "string", "minLength": 1, "description": "The Slack conversation or channel ID." }, "nullable": false, "handle": "channelId" }, { "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" }, { "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" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "name": "Prepare the next reminder", "moduleId": "n_5367fabbcad6106a", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_fc8fcff35c685491": { "inputs": {}, "name": "More birthdays?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "more", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_ff87bc6b6d08a7b3", "output": "more" } }, "operator": "==", "right": { "kind": "value", "value": true } } ] } ] } ] }, "n_a41140ebf6de0c31": { "inputs": { "channelId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ff87bc6b6d08a7b3", "output": "channelId" } ] }, "text": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ff87bc6b6d08a7b3", "output": "text" } ] }, "blocks": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ff87bc6b6d08a7b3", "output": "blocks" } ] } }, "name": "Send a birthday reminder", "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": "Send a birthday reminder", "executor": { "kind": "connector", "action": "slack.post_message" } } }, "n_8d462234a7fcc57b": { "name": "Reminder count", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "outputMappings": { "count": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ff87bc6b6d08a7b3", "output": "count" } ] } } } }, "edges": [ { "source": "n_e0d51b826febd141", "target": "n_11ccad4658dd7dde" }, { "source": "n_11ccad4658dd7dde", "target": "n_8bfd8902ec4923a3" }, { "source": "n_8bfd8902ec4923a3", "target": "n_11ccad4658dd7dde", "sourceHandle": "more" }, { "source": "n_8bfd8902ec4923a3", "target": "n_45eff0fd79b6c792", "sourceHandle": "otherwise" }, { "source": "n_45eff0fd79b6c792", "target": "n_ff87bc6b6d08a7b3" }, { "source": "n_ff87bc6b6d08a7b3", "target": "n_fc8fcff35c685491" }, { "source": "n_fc8fcff35c685491", "target": "n_a41140ebf6de0c31", "sourceHandle": "more" }, { "source": "n_a41140ebf6de0c31", "target": "n_ff87bc6b6d08a7b3" }, { "source": "n_fc8fcff35c685491", "target": "n_8d462234a7fcc57b", "sourceHandle": "otherwise" } ] } }, "modules": { "n_0658d3e7a82df6fa": { "name": "Read every contacts page", "imports": [], "source": "export default async function collect({clientId,clientSecret,refreshToken,previousState},context){\n for(const [name,v] of Object.entries({clientId,clientSecret,refreshToken}))if(typeof v!=='string'||!v.trim())throw Error('Configure Google OAuth '+name);\n const state=previousState===null?{people:[],cursor:null,seen:[]}:previousState;\n if(!state||!Array.isArray(state.people)||!Array.isArray(state.seen)||(state.cursor!==null&&typeof state.cursor!=='string'))throw Error('Invalid contact pagination state');\n const form=new URLSearchParams({client_id:clientId,client_secret:clientSecret,refresh_token:refreshToken,grant_type:'refresh_token'});\n const auth=await context.fetch('https://oauth2.googleapis.com/token',{method:'POST',headers:{'content-type':'application/x-www-form-urlencoded'},body:form.toString()});if(!auth.ok)throw Error('Google OAuth refresh failed');const credential=await auth.json();if(typeof credential.access_token!=='string'||!credential.access_token)throw Error('Google OAuth refresh returned no access token');\n const url=new URL('https://people.googleapis.com/v1/people/me/connections');url.searchParams.set('personFields','names,nicknames,birthdays');url.searchParams.set('pageSize','100');if(state.cursor!==null)url.searchParams.set('pageToken',state.cursor);\n const response=await context.fetch(url.toString(),{headers:{authorization:'Bearer '+credential.access_token}});if(!response.ok)throw Error('Google Contacts page failed');const result=await response.json();if(result.connections!==undefined&&!Array.isArray(result.connections))throw Error('Invalid Google Contacts page');const people=state.people.concat(result.connections??[]),next=result.nextPageToken;if(next===undefined)return{state:{people,cursor:null,seen:state.seen},more:false};if(typeof next!=='string'||next.length===0||state.seen.includes(next))throw Error('Invalid or repeated Google Contacts page token');return{state:{people,cursor:next,seen:state.seen.concat(next)},more:true};\n}\n" }, "n_cfe69fec56d4f932": { "name": "Find today’s birthdays", "imports": [], "source": "export default function findBirthdays({state,scheduledAt}){\n const people=state.people;\n const now=new Date(scheduledAt);if(!Number.isFinite(now.getTime()))throw Error('Invalid schedule time');const month=now.getUTCMonth()+1,day=now.getUTCDate(),matches=[];\n for(const person of people){if(!person||typeof person!=='object')throw Error('Invalid contact');const birthdays=person.birthdays??[];if(!Array.isArray(birthdays))throw Error('Invalid contact birthdays');if(!birthdays.some(b=>b?.date?.month===month&&b?.date?.day===day))continue;const names=Array.isArray(person.names)?person.names:[],primary=names.find(n=>n?.metadata?.primary)||names[0],nickname=Array.isArray(person.nicknames)?person.nicknames[0]?.value:undefined;const name=primary?.displayName||[primary?.givenName,primary?.familyName].filter(Boolean).join(' ')||nickname||'your friend';matches.push({name:String(name)});}\n return{matches};\n}\n" }, "n_5367fabbcad6106a": { "name": "Prepare the next reminder", "imports": [], "source": "export default function next({matches,channelId,previousState}){if(typeof channelId!=='string'||!channelId.trim()||channelId.includes('YOUR_'))throw Error('Configure a Slack channel ID');const index=previousState===null?0:previousState.index+1;if(index>=matches.length)return{state:{index},more:false,channelId:channelId.trim(),text:'Todays Birthday of your friend',blocks:[{type:'section',text:{type:'plain_text',text:'No birthday'}}],count:matches.length};const name=matches[index].name;return{state:{index},more:true,channelId:channelId.trim(),text:'Todays Birthday of your friend',blocks:[{type:'section',text:{type:'plain_text',text:`Today is ${name}'s birthday! 🎉`}}],count:matches.length};}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }