{ "kind": "open-flow-workflow", "version": 1, "name": "Email Scheduled Recipe Suggestions", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_bd74e7d866717146": { "name": "Daily at 10:00 UTC", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "0 10 * * *", "timezone": "UTC" } ] }, "n_98d2f86cd0036ab5": { "inputs": { "query": { "kind": "value", "value": "chicken" }, "diet": { "kind": "value", "value": "balanced" }, "health": { "kind": "value", "value": "random" }, "minCalories": { "kind": "value", "value": 0 }, "maxCalories": { "kind": "value", "value": 1500 }, "minMinutes": { "kind": "value", "value": 0 }, "maxMinutes": { "kind": "value", "value": 30 }, "maxIngredients": { "kind": "value", "value": 5 }, "count": { "kind": "value", "value": 3 } }, "name": "Prepare recipe search", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "query" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "diet" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "health" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "minCalories" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "maxCalories" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "minMinutes" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "maxMinutes" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "maxIngredients" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "outputs": [ { "jsonSchema": { "type": "string", "format": "uri" }, "nullable": false, "handle": "url" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "query" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "diet" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "health" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "maxCalories" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "maxMinutes" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "name": "Prepare recipe search", "moduleId": "n_8b0d23035d616426", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_fab611b7ec2e2eee": { "inputs": { "url": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_98d2f86cd0036ab5", "output": "url" } ] }, "auth.username": { "kind": "unset" }, "auth.password": { "kind": "unset" } }, "name": "Find random recipes", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "url" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "auth.username" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "auth.password" } ], "outputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "object", "additionalProperties": { "type": "string" } }, "nullable": false, "handle": "headers" } ], "name": "Find random recipes", "executor": { "method": "GET", "query": { "mode": "fields", "names": [] }, "headers": { "mode": "fields", "names": [] }, "bodyFormat": "none", "authentication": { "type": "basic" }, "responseFormat": "json", "statusPolicy": "success", "kind": "http-request" } } }, "n_df9a9fac54ec40f0": { "inputs": { "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_fab611b7ec2e2eee", "output": "body" } ] }, "query": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_98d2f86cd0036ab5", "output": "query" } ] }, "diet": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_98d2f86cd0036ab5", "output": "diet" } ] }, "health": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_98d2f86cd0036ab5", "output": "health" } ] }, "maxCalories": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_98d2f86cd0036ab5", "output": "maxCalories" } ] }, "maxMinutes": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_98d2f86cd0036ab5", "output": "maxMinutes" } ] }, "count": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_98d2f86cd0036ab5", "output": "count" } ] } }, "name": "Format recipe links", "kind": "task", "task": { "inputs": [ { "jsonSchema": {}, "nullable": true, "handle": "body" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "query" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "diet" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "health" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "maxCalories" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "maxMinutes" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "outputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "html" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "subject" } ], "name": "Format recipe links", "moduleId": "n_b6e6d4b43a85713e", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_f9d2cf578c60e31c": { "inputs": { "to": { "kind": "value", "value": "YOUR_RECIPIENT_EMAIL" }, "subject": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_df9a9fac54ec40f0", "output": "subject" } ] }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_df9a9fac54ec40f0", "output": "html" } ] }, "isHtml": { "kind": "value", "value": true } }, "name": "Email the recipes", "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": "Email the recipes", "executor": { "kind": "connector", "action": "gmail.send_email" } } } }, "edges": [ { "source": "n_bd74e7d866717146", "target": "n_98d2f86cd0036ab5" }, { "source": "n_98d2f86cd0036ab5", "target": "n_fab611b7ec2e2eee" }, { "source": "n_fab611b7ec2e2eee", "target": "n_df9a9fac54ec40f0" }, { "source": "n_df9a9fac54ec40f0", "target": "n_f9d2cf578c60e31c" } ] } }, "modules": { "n_8b0d23035d616426": { "name": "Prepare recipe search", "imports": [], "source": "export default function recipeQuery({query,diet,minCalories,maxCalories,minMinutes,maxMinutes,maxIngredients,health,count}) {\n if(typeof query!=='string'||!query.trim()||/[\\r\\n]/.test(query)||query.startsWith('YOUR_'))throw Error('Configure a recipe search term');\n const diets=['balanced','high-fiber','high-protein','low-carb','low-fat','low-sodium'];if(diet==='random')diet=diets[Math.floor(Math.random()*diets.length)];if(!diets.includes(diet))throw Error('Choose a supported diet filter');\n const allowed=['alcohol-free','celery-free','crustacean-free','dairy-free','egg-free','fish-free','fodmap-free','gluten-free','keto-friendly','kidney-friendly','kosher','low-fat-abs','low-potassium','low-sugar','lupine-free','mustard-free','no-oil-added','paleo','peanut-free','pescatarian','pork-free','red-meat-free','sesame-free','shellfish-free','soy-free','sugar-conscious','tree-nut-free','vegan','vegetarian','wheat-free'];\n if(health==='random')health=allowed[Math.floor(Math.random()*allowed.length)];if(!allowed.includes(health))throw Error('Choose a supported health filter');\n for(const n of [minCalories,minMinutes])if(!Number.isSafeInteger(n)||n<0)throw Error('Set nonnegative search minimums');for(const n of [maxCalories,maxMinutes,maxIngredients,count])if(!Number.isSafeInteger(n)||n<1)throw Error('Set positive integer search limits');if(minCalories>maxCalories||minMinutes>maxMinutes||count>20)throw Error('Invalid search range or recipe count');\n const p={type:'public',q:query.trim(),diet,health,calories:`${minCalories}-${maxCalories}`,time:`${minMinutes}-${maxMinutes}`,ingr:String(maxIngredients),random:'true'};return {url:'https://api.edamam.com/api/recipes/v2?'+Object.entries(p).map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'),query:query.trim(),diet,health,maxCalories,maxMinutes,count};\n}\n" }, "n_b6e6d4b43a85713e": { "name": "Format recipe links", "imports": [], "source": "export default function recipeFormat({body,query,diet,health,maxCalories,maxMinutes,count}) {\n if(!body||!Array.isArray(body.hits))throw Error('Invalid recipe response');if(!body.hits.length)throw Error('No recipes matched the selected filters');\n const escape=s=>String(s).replace(/&/g,'&').replace(//g,'>').replace(/\"/g,'"').replace(/'/g,''');\n const validUrl=u=>{if(typeof u!=='string'||/[\\s\\x00-\\x1f\\x7f\\\\]/.test(u)||/%(?![0-9a-f]{2})/i.test(u))return false;const m=/^https:\\/\\/([^/?#]+)(?:[/?#].*)?$/i.exec(u);if(!m||m[1].length>253)return false;const labels=m[1].replace(/\\.$/,'').split('.');if(!labels.every(x=>x.length>0&&x.length<=63&&/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(x)))return false;if(labels.every(x=>/^\\d+$/.test(x))&&(labels.length!==4||labels.some(x=>Number(x)>255)))return false;return true};\n const links=body.hits.slice(0,count).map(({recipe})=>{const u=recipe?.shareAs,name=recipe?.label;if(typeof name!=='string'||!name.trim()||!validUrl(u))throw Error('Invalid recipe link or label');return `
  • ${escape(name)}
  • `});\n return {html:'Here are your recipes for today:
    ',subject:`${links.length} ${diet}, ${health} ${query} recipes up to ${maxCalories} calories per serving and ${maxMinutes} minutes`};\n}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }