{ "kind": "open-flow-workflow", "version": 1, "name": "Follow Up HubSpot Contacts with Gmail", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_8fe801dfdbce0a21": { "name": "Every day at 09:00 UTC", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "0 9 * * *", "timezone": "UTC" } ] }, "n_d09aa24a4a5f773b": { "inputs": { "filterGroups": { "kind": "value", "value": [ { "filters": [ { "propertyName": "notes_last_contacted", "operator": "HAS_PROPERTY" } ] } ] }, "sorts": { "kind": "value", "value": [ "notes_last_contacted" ] }, "properties": { "kind": "value", "value": [ "firstname", "lastname", "email", "notes_last_contacted" ] }, "limit": { "kind": "value", "value": 100 } }, "name": "Find previously contacted people", "kind": "task", "task": { "inputs": [ { "description": "Full-text query applied to the HubSpot CRM search.", "jsonSchema": { "type": "string", "description": "Full-text query applied to the HubSpot CRM search." }, "nullable": true, "handle": "query", "value": null }, { "description": "HubSpot filter groups used to narrow the search.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": {}, "additionalProperties": true, "description": "HubSpot CRM search filter group." }, "maxItems": 5, "description": "HubSpot filter groups used to narrow the search." }, "nullable": true, "handle": "filterGroups", "value": null }, { "description": "HubSpot sort expressions.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "description": "HubSpot sort expression." }, "description": "HubSpot sort expressions." }, "nullable": true, "handle": "sorts", "value": null }, { "description": "Property names to include in the returned record payload.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "description": "HubSpot property name." }, "description": "Property names to include in the returned record payload." }, "nullable": true, "handle": "properties", "value": null }, { "description": "Property names to include with their value history.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "description": "HubSpot property name." }, "description": "Property names to include with their value history." }, "nullable": true, "handle": "propertiesWithHistory", "value": null }, { "description": "Associated object types to include in the response.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "description": "HubSpot associated object type." }, "description": "Associated object types to include in the response." }, "nullable": true, "handle": "associations", "value": null }, { "description": "Maximum number of records to return.", "jsonSchema": { "type": "integer", "minimum": 1, "maximum": 200, "description": "Maximum number of records to return." }, "nullable": true, "handle": "limit", "value": null }, { "description": "Paging cursor returned by a previous search request.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Paging cursor returned by a previous search request." }, "nullable": true, "handle": "after", "value": null } ], "outputs": [ { "description": "Records returned by the search request.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "HubSpot record identifier." }, "archived": { "type": "boolean", "description": "Whether the record is archived." }, "createdAt": { "type": "string", "description": "Timestamp when the record was created." }, "updatedAt": { "type": "string", "description": "Timestamp when the record was last updated." }, "properties": { "type": "object", "additionalProperties": { "description": "Property value." }, "description": "HubSpot properties keyed by property name." }, "propertiesWithHistory": { "type": "object", "additionalProperties": { "description": "Property history payload." }, "description": "HubSpot property histories keyed by property name." }, "associations": { "type": "object", "properties": {}, "additionalProperties": true, "description": "Associations returned by HubSpot when requested." } }, "additionalProperties": true, "description": "HubSpot CRM record returned by the MCP server." }, "description": "Records returned by the search request." }, "nullable": false, "handle": "results" }, { "description": "Paging information returned by HubSpot.", "jsonSchema": { "type": "object", "properties": { "nextAfter": { "type": "string", "description": "Cursor for the next search request when another page is available." } }, "additionalProperties": true, "description": "Paging information returned by HubSpot." }, "nullable": true, "handle": "paging" }, { "description": "Raw MCP response payload.", "jsonSchema": { "description": "Raw MCP response payload." }, "nullable": true, "handle": "raw" } ], "name": "Find previously contacted people", "executor": { "kind": "connector", "action": "hubspot.search_contacts" } } }, "n_454612bba22fa27f": { "inputs": { "contacts": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d09aa24a4a5f773b", "output": "results" } ] }, "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_454612bba22fa27f", "output": "state" } ] }, "scheduledAt": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_8fe801dfdbce0a21", "output": "scheduledAt" } ] } }, "name": "Prepare the next contact", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "HubSpot record identifier." }, "archived": { "type": "boolean", "description": "Whether the record is archived." }, "createdAt": { "type": "string", "description": "Timestamp when the record was created." }, "updatedAt": { "type": "string", "description": "Timestamp when the record was last updated." }, "properties": { "type": "object", "additionalProperties": { "description": "Property value." }, "description": "HubSpot properties keyed by property name." }, "propertiesWithHistory": { "type": "object", "additionalProperties": { "description": "Property history payload." }, "description": "HubSpot property histories keyed by property name." }, "associations": { "type": "object", "properties": {}, "additionalProperties": true, "description": "Associations returned by HubSpot when requested." } }, "additionalProperties": true, "description": "HubSpot CRM record returned by the MCP server." }, "description": "Records returned by the search request." }, "nullable": false, "handle": "contacts" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": true, "handle": "previousState" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "scheduledAt" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "old" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "current" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "url" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "name": "Prepare the next contact", "moduleId": "n_fec3c2912aa607b9", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_69b3b44b39765351": { "inputs": {}, "name": "More contacts?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "check", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_454612bba22fa27f", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_127a9fcd414e0be9": { "inputs": {}, "name": "Contacted over 30 days ago?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "check", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_454612bba22fa27f", "output": "old" } }, "operator": "isTrue" } ] } ] } ] }, "n_3acc5d37299bcfe3": { "inputs": { "url": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_454612bba22fa27f", "output": "url" } ] }, "auth.token": { "kind": "unset" } }, "name": "Read contact engagements", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "url" }, { "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": "Read contact engagements", "executor": { "method": "GET", "query": { "mode": "fields", "names": [] }, "headers": { "mode": "fields", "names": [] }, "bodyFormat": "none", "authentication": { "type": "bearer" }, "responseFormat": "json", "statusPolicy": "success", "kind": "http-request" } } }, "n_6a6e8c17793ed895": { "inputs": { "current": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_454612bba22fa27f", "output": "current" } ] }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_3acc5d37299bcfe3", "output": "body" } ] }, "sender": { "kind": "value", "value": "YOUR_SENDER_EMAIL" } }, "name": "Check eligibility and prepare the email", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "current" }, { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "sender" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "send" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "sender" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "to" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "subject" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "body" } ], "name": "Check eligibility and prepare the email", "moduleId": "n_63ee232aba94abb4", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_809ab4a57bfd64a3": { "inputs": {}, "name": "Follow up?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "send", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_6a6e8c17793ed895", "output": "send" } }, "operator": "isTrue" } ] } ] } ] }, "n_6798cf3831515191": { "inputs": { "to": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_6a6e8c17793ed895", "output": "to" } ] }, "subject": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_6a6e8c17793ed895", "output": "subject" } ] }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_6a6e8c17793ed895", "output": "body" } ] }, "isHtml": { "kind": "value", "value": false } }, "name": "Send the follow-up", "kind": "task", "task": { "inputs": [ { "description": "Primary recipient email address.", "jsonSchema": { "type": "string", "description": "Primary recipient email address." }, "nullable": true, "handle": "to", "value": null }, { "description": "Additional To recipients.", "jsonSchema": { "type": "array", "items": { "type": "string", "description": "An array item." }, "description": "Additional To recipients." }, "nullable": true, "handle": "extraRecipients", "value": null }, { "description": "Cc recipients.", "jsonSchema": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "array", "items": { "type": "string", "description": "An array item." }, "description": "A union variant." } ], "description": "Cc recipients." }, "nullable": true, "handle": "cc", "value": null }, { "description": "Bcc recipients.", "jsonSchema": { "anyOf": [ { "type": "string", "description": "A union variant." }, { "type": "array", "items": { "type": "string", "description": "An array item." }, "description": "A union variant." } ], "description": "Bcc recipients." }, "nullable": true, "handle": "bcc", "value": null }, { "description": "Email subject line.", "jsonSchema": { "type": "string", "description": "Email subject line." }, "nullable": true, "handle": "subject", "value": null }, { "description": "Email body content.", "jsonSchema": { "type": "string", "description": "Email body content." }, "nullable": true, "handle": "body", "value": null }, { "description": "Set to true when the body content is already HTML.", "jsonSchema": { "type": "boolean", "description": "Set to true when the body content is already HTML." }, "nullable": true, "handle": "isHtml", "value": null }, { "description": "Attachments and CID inline images. Up to 25,000,000 decoded bytes in total and 35,000,000 MIME bytes.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "filename": { "type": "string", "description": "Attachment filename; omit for an unnamed attachment." }, "mimeType": { "type": "string", "description": "MIME type without parameters; defaults to application/octet-stream." }, "contentBase64": { "type": "string", "description": "Standard padded Base64 content. Empty string represents a zero-byte file. Supply this or fileUrl." }, "fileUrl": { "type": "string", "format": "uri", "description": "Public URL to download the attachment. Supply this or contentBase64." }, "contentId": { "type": "string", "minLength": 1, "description": "Bare Content-ID without cid: or angle brackets; reference it as cid: in HTML." }, "disposition": { "type": "string", "enum": [ "inline", "attachment" ], "description": "Defaults to inline when contentId is set, otherwise attachment. Inline requires contentId." } }, "additionalProperties": false, "description": "An attachment or CID inline resource.", "oneOf": [ { "required": [ "contentBase64" ] }, { "required": [ "fileUrl" ] } ] }, "maxItems": 100, "description": "Attachments and CID inline images. Up to 25,000,000 decoded bytes in total and 35,000,000 MIME bytes." }, "nullable": true, "handle": "attachments", "value": null }, { "description": "Verified Gmail send-as alias to use in the From header.", "jsonSchema": { "type": "string", "description": "Verified Gmail send-as alias to use in the From header." }, "nullable": true, "handle": "fromEmail", "value": null } ], "outputs": [ { "description": "Gmail message ID.", "jsonSchema": { "type": "string", "description": "Gmail message ID." }, "nullable": false, "handle": "messageId" }, { "description": "Gmail thread ID.", "jsonSchema": { "type": "string", "description": "Gmail thread ID." }, "nullable": true, "handle": "threadId" } ], "name": "Send the follow-up", "executor": { "kind": "connector", "action": "gmail.send_email" } } }, "n_93481b0f1bfa65c6": { "inputs": { "current": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_454612bba22fa27f", "output": "current" } ] }, "to": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_6a6e8c17793ed895", "output": "to" } ] }, "subject": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_6a6e8c17793ed895", "output": "subject" } ] }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_6a6e8c17793ed895", "output": "body" } ] }, "messageId": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_6798cf3831515191", "output": "messageId" } ] }, "scheduledAt": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_8fe801dfdbce0a21", "output": "scheduledAt" } ] }, "sender": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_6a6e8c17793ed895", "output": "sender" } ] } }, "name": "Prepare the engagement", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "current" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "to" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "subject" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "body" }, { "jsonSchema": { "type": "string", "description": "Gmail message ID." }, "nullable": false, "handle": "messageId" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "scheduledAt" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "sender" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "payload" } ], "name": "Prepare the engagement", "moduleId": "n_9716cb294404ad13", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_74abe49dfb9e6e56": { "inputs": { "url": { "kind": "value", "value": "https://api.hubapi.com/crm/v3/objects/emails" }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_93481b0f1bfa65c6", "output": "payload" } ] }, "auth.token": { "kind": "unset" } }, "name": "Log the sent email", "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": "Log the sent email", "executor": { "method": "POST", "query": { "mode": "fields", "names": [] }, "headers": { "mode": "fields", "names": [] }, "bodyFormat": "json", "authentication": { "type": "bearer" }, "responseFormat": "json", "statusPolicy": "success", "kind": "http-request" } } }, "n_74a5dcec962fca12": { "inputs": { "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_74abe49dfb9e6e56", "output": "body" } ] } }, "name": "Confirm the engagement", "kind": "task", "task": { "inputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "ok" } ], "name": "Confirm the engagement", "moduleId": "n_483c6c90db87ca21", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_392db39a6d2a0379": { "name": "Complete", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "outputMappings": { "count": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_454612bba22fa27f", "output": "count" } ] } } } }, "edges": [ { "source": "n_8fe801dfdbce0a21", "target": "n_d09aa24a4a5f773b" }, { "source": "n_d09aa24a4a5f773b", "target": "n_454612bba22fa27f" }, { "source": "n_454612bba22fa27f", "target": "n_69b3b44b39765351" }, { "source": "n_69b3b44b39765351", "target": "n_127a9fcd414e0be9", "sourceHandle": "check" }, { "source": "n_127a9fcd414e0be9", "target": "n_3acc5d37299bcfe3", "sourceHandle": "check" }, { "source": "n_127a9fcd414e0be9", "target": "n_454612bba22fa27f", "sourceHandle": "otherwise" }, { "source": "n_3acc5d37299bcfe3", "target": "n_6a6e8c17793ed895" }, { "source": "n_6a6e8c17793ed895", "target": "n_809ab4a57bfd64a3" }, { "source": "n_809ab4a57bfd64a3", "target": "n_6798cf3831515191", "sourceHandle": "send" }, { "source": "n_6798cf3831515191", "target": "n_93481b0f1bfa65c6" }, { "source": "n_93481b0f1bfa65c6", "target": "n_74abe49dfb9e6e56" }, { "source": "n_74abe49dfb9e6e56", "target": "n_74a5dcec962fca12" }, { "source": "n_74a5dcec962fca12", "target": "n_454612bba22fa27f" }, { "source": "n_809ab4a57bfd64a3", "target": "n_454612bba22fa27f", "sourceHandle": "otherwise" }, { "source": "n_69b3b44b39765351", "target": "n_392db39a6d2a0379", "sourceHandle": "otherwise" } ] } }, "modules": { "n_fec3c2912aa607b9": { "name": "Prepare the next contact", "imports": [], "source": "export default function next({contacts,previousState,scheduledAt}) {\n if(!Array.isArray(contacts)) throw Error('Invalid HubSpot contacts');\n const index=previousState?.index ?? 0;\n if(!Number.isSafeInteger(index)||index<0||index>contacts.length)throw Error('Invalid iteration state');\n if(index===contacts.length)return {state:{index},more:false,old:false,current:{},url:'',count:index};\n const c=contacts[index],t=Date.parse(c?.properties?.notes_last_contacted),now=Date.parse(scheduledAt);\n if(typeof c?.id!=='string'||!/^\\d+$/.test(c.id)||!Number.isFinite(t)||!Number.isFinite(now))throw Error('Invalid contact or last-contacted date');\n return {state:{index:index+1},more:true,old:t!(Number.isSafeInteger(x)&&x>0)&&!(typeof x==='string'&&/^\\d+$/.test(x)&&x!=='0'))||typeof body.hasMore!=='boolean')throw Error('Invalid engagement response');\n if(typeof sender!=='string'||!sender.includes('@')||sender.includes('YOUR_'))throw Error('Configure the Gmail sender address before sending');\n const send=current.eligible && body.results.length===1 && !body.hasMore;\n if(send&&(typeof current.email!=='string'||!current.email.includes('@')||typeof current.firstName!=='string'))throw Error('Contact needs an email and first name');\n return {send,sender,to:send?current.email:'',subject:'Following up',body:'Hello '+current.firstName+',\\n\\nI wanted to follow up on our previous conversation.\\n\\nBest regards'};\n}\n" }, "n_9716cb294404ad13": { "name": "Prepare the engagement", "imports": [], "source": "export default function log({current,to,subject,body,messageId,scheduledAt,sender}) {\n if(typeof messageId!=='string'||!messageId)throw Error('Gmail did not confirm sending');\n if(typeof sender!=='string'||!sender.includes('@')||sender.includes('YOUR_'))throw Error('Configure the Gmail sender address before sending');\n return {payload:{properties:{hs_timestamp:scheduledAt,hs_email_direction:'EMAIL',hs_email_status:'SENT',hs_email_subject:subject,hs_email_text:body,hs_email_headers:JSON.stringify({from:{email:sender},to:[{email:to}]})},associations:[{to:{id:current.id},types:[{associationCategory:'HUBSPOT_DEFINED',associationTypeId:198}]}]}};\n}\n" }, "n_483c6c90db87ca21": { "name": "Confirm the engagement", "imports": [], "source": "export default function confirm({body}) {\n if(!body||typeof body.id!=='string'||!body.id)throw Error('HubSpot did not confirm the email engagement');\n return {ok:true};\n}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }