This workflow corresponds to n8n.io template #14095 — we link there as the canonical source.
This workflow follows the Gmail → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "4594397e-3aa0-4872-985b-6e11b9bff28b",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
10512,
1088
],
"parameters": {
"color": 7,
"width": 768,
"height": 272,
"content": "## Schedule and meal generation\n\nTriggering the workflow and creating the weekly meal plan."
},
"typeVersion": 1
},
{
"id": "783a568d-c3fc-4191-823d-2bf7aad4f5a1",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
11408,
1088
],
"parameters": {
"color": 7,
"width": 1376,
"height": 272,
"content": "## Meal plan email workflow\n\nFetching meals, normalizing data, emailing and handling user responses."
},
"typeVersion": 1
},
{
"id": "99d6ab0b-f8b4-490a-a6a7-5f08066a5364",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
13248,
1072
],
"parameters": {
"color": 7,
"width": 1440,
"height": 272,
"content": "## Shopping list creation\n\nRetrieving recipe data and building a shopping list in Mealie."
},
"typeVersion": 1
},
{
"id": "3c024a75-7911-4528-b726-d829384b0c0b",
"name": "Add Ingredients To Shopping List",
"type": "n8n-nodes-base.httpRequest",
"position": [
14544,
1184
],
"parameters": {
"url": "=http://<mealie ip address>:9925/api/households/shopping/lists/{{$('Create Shopping List in Mealie').item.json.id}}/recipe",
"method": "POST",
"options": {},
"jsonBody": "={{ $json.recipes }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"executeOnce": false,
"typeVersion": 4.4
},
{
"id": "904ddc41-c71e-432e-8564-4e766905fa44",
"name": "Delete Random Meal Plan",
"type": "n8n-nodes-base.httpRequest",
"position": [
13424,
1744
],
"parameters": {
"url": "=http://<mealie ip address>:9925/api/households/mealplans/{{ $json.id }}",
"method": "DELETE",
"options": {},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "85c04cb6-c234-424e-a5eb-6645829200b5",
"name": "Split Removals Array",
"type": "n8n-nodes-base.splitOut",
"position": [
13200,
1744
],
"parameters": {
"options": {},
"fieldToSplitOut": "removals"
},
"typeVersion": 1
},
{
"id": "a4fd769a-992f-4a82-a3b1-fb310e558047",
"name": "Generate Random Meal Plan",
"type": "n8n-nodes-base.httpRequest",
"position": [
11136,
1200
],
"parameters": {
"url": "http://<mealie ip address>:9925/api/households/mealplans/random",
"method": "POST",
"options": {},
"sendBody": true,
"authentication": "predefinedCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "date",
"value": "={{ $json.date }}"
},
{
"name": "entryType",
"value": "dinner"
}
]
},
"nodeCredentialType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "d0b8ac89-7d39-465b-87b2-19cf2dcda40a",
"name": "Create Shopping List in Mealie",
"type": "n8n-nodes-base.httpRequest",
"position": [
13840,
1184
],
"parameters": {
"url": "http://<mealie ip address>:9925/api/households/shopping/lists",
"method": "POST",
"options": {},
"jsonBody": "={\n \"name\": \"Shopping List Week of {{ $('Generate Upcoming Week').item.json.date }}\"\n} ",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 4.4,
"alwaysOutputData": false
},
{
"id": "3cb018a9-b6f9-491d-a412-ed82539e8660",
"name": "Normalize Recipe Data",
"type": "n8n-nodes-base.code",
"position": [
14320,
1184
],
"parameters": {
"jsCode": "const recipes = $('Fetch Recipe By Slug').all();\n\nconst mealPlanRecipes = recipes.map(item => {\n const recipe = item.json;\n return {\n recipeId: recipe.id,\n recipeIncrementQuantity: recipe.recipeServings ?? 1,\n recipeIngredients: (recipe.recipeIngredient ?? []).map(ing => ({\n quantity: ing.quantity ?? 0,\n unit: ing.unit ?? null,\n food: ing.food ?? null,\n referencedRecipe: ing.referencedRecipe ?? null,\n note: ing.note ?? \"\",\n display: ing.display ?? \"\",\n title: ing.title ?? null,\n originalText: ing.originalText ?? null,\n referenceId: ing.referenceId ?? null,\n }))\n };\n});\n\nreturn [{\n json: {\n recipes: mealPlanRecipes\n }\n}];"
},
"typeVersion": 2
},
{
"id": "e8d36d94-22a2-4bfd-b2f2-b4ae3fbdab1f",
"name": "Fetch Current Week Meal Plans",
"type": "n8n-nodes-base.httpRequest",
"position": [
11456,
1200
],
"parameters": {
"url": "http://<mealie ip address>:9925/api/households/mealplans",
"options": {},
"sendQuery": true,
"authentication": "predefinedCredentialType",
"queryParameters": {
"parameters": [
{
"name": "start_date",
"value": "={{ $now.format('yyyy-MM-dd') }}"
},
{
"name": "end_date",
"value": "={{ $now.plus(7, 'days').format('yyyy-MM-dd') }}"
},
{
"name": "orderBy",
"value": "date"
},
{
"name": "orderDirection",
"value": "asc"
}
]
},
"nodeCredentialType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 4.4
},
{
"id": "0bf51a6a-1e86-43b8-a68b-3e68d11022c4",
"name": "Send Meal Plan Email",
"type": "n8n-nodes-base.gmail",
"position": [
11952,
1200
],
"parameters": {
"sendTo": "user@example.com",
"message": "={{ $json.html }}",
"options": {
"responseFormCustomCss": ":root {\n\t--font-family: 'Open Sans', sans-serif;\n\t--font-weight-normal: 400;\n\t--font-weight-bold: 600;\n\t--font-size-body: 12px;\n\t--font-size-label: 14px;\n\t--font-size-test-notice: 12px;\n\t--font-size-input: 14px;\n\t--font-size-header: 20px;\n\t--font-size-paragraph: 14px;\n\t--font-size-link: 12px;\n\t--font-size-error: 12px;\n\t--font-size-html-h1: 28px;\n\t--font-size-html-h2: 20px;\n\t--font-size-html-h3: 16px;\n\t--font-size-html-h4: 14px;\n\t--font-size-html-h5: 12px;\n\t--font-size-html-h6: 10px;\n\t--font-size-subheader: 14px;\n\n\t/* Colors */\n\t--color-background: #fbfcfe;\n\t--color-test-notice-text: #e6a23d;\n\t--color-test-notice-bg: #fefaf6;\n\t--color-test-notice-border: #f6dcb7;\n\t--color-card-bg: #ffffff;\n\t--color-card-border: #dbdfe7;\n\t--color-card-shadow: rgba(99, 77, 255, 0.06);\n\t--color-link: #7e8186;\n\t--color-header: #525356;\n\t--color-label: #555555;\n\t--color-input-border: #dbdfe7;\n\t--color-input-text: #71747A;\n\t--color-focus-border: rgb(90, 76, 194);\n\t--color-submit-btn-bg: #ff6d5a;\n\t--color-submit-btn-text: #ffffff;\n\t--color-error: #ea1f30;\n\t--color-required: #ff6d5a;\n\t--color-clear-button-bg: #7e8186;\n\t--color-html-text: #555;\n\t--color-html-link: #ff6d5a;\n\t--color-header-subtext: #7e8186;\n\n\t/* Border Radii */\n\t--border-radius-card: 8px;\n\t--border-radius-input: 6px;\n\t--border-radius-clear-btn: 50%;\n\t--card-border-radius: 8px;\n\n\t/* Spacing */\n\t--padding-container-top: 24px;\n\t--padding-card: 24px;\n\t--padding-test-notice-vertical: 12px;\n\t--padding-test-notice-horizontal: 24px;\n\t--margin-bottom-card: 16px;\n\t--padding-form-input: 12px;\n\t--card-padding: 24px;\n\t--card-margin-bottom: 16px;\n\n\t/* Dimensions */\n\t--container-width: 100vw;\n\t--submit-btn-height: 48px;\n\t--checkbox-size: 18px;\n\n\t/* Others */\n\t--box-shadow-card: 0px 4px 16px 0px var(--color-card-shadow);\n\t--opacity-placeholder: 0.5;\n}"
},
"subject": "=Mealie Meal Plan for {{ $now.format('yyyy-MM-dd') }}",
"operation": "sendAndWait",
"defineForm": "json",
"jsonOutput": "={{ $json.fields }}",
"responseType": "customForm"
},
"typeVersion": 2.2
},
{
"id": "ca9ce38b-7eb7-4d93-8d5c-2a0b6c25e450",
"name": "Normalize User Response",
"type": "n8n-nodes-base.code",
"position": [
12160,
1200
],
"parameters": {
"jsCode": "const data = $input.first().json.data;\n\nconst removals = Object.entries(data)\n .filter(([key, value]) => value.includes(\"Remove from meal plan\"))\n .map(([key]) => {\n // Format: \"2026-03-04 \u2014 Recipe Name - 148\"\n const lastDash = key.lastIndexOf(' - ');\n const id = key.substring(lastDash + 3).trim();\n const withoutId = key.substring(0, lastDash).trim();\n\n const [date, ...nameParts] = withoutId.split(' \u2014 ');\n return {\n id,\n date: date.trim(),\n recipeName: nameParts.join(' \u2014 ').trim()\n };\n });\n\nreturn [{\n json: {\n removals,\n isEmpty: removals.length === 0\n }\n}];"
},
"typeVersion": 2
},
{
"id": "1a16fd08-534a-4bbe-a6e7-fce3533e15fc",
"name": "Check for Removals",
"type": "n8n-nodes-base.if",
"position": [
12640,
1200
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "277ef324-0619-46ed-81bd-37447d7306b4",
"operator": {
"type": "array",
"operation": "empty",
"singleValue": true
},
"leftValue": "={{ $json.removals }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "5334884e-470e-47d7-b841-39b11c8b6082",
"name": "Generate Upcoming Week",
"type": "n8n-nodes-base.code",
"position": [
10928,
1200
],
"parameters": {
"jsCode": "const days = [];\nconst today = new Date($now.toFormat('yyyy-MM-dd'));\nfor (let i = 0; i < 7; i++) {\n const date = new Date(today);\n date.setDate(today.getDate() + i);\n days.push({\n json: {\n date: date.toISOString().split(\"T\")[0],\n dayOfWeek: date.toLocaleDateString(\"en-US\", { weekday: \"long\" }),\n }\n });\n}\nreturn days;"
},
"typeVersion": 2
},
{
"id": "dec9f546-f092-429f-84d0-300fae39ce71",
"name": "Weekly Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
10560,
1200
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks"
}
]
}
},
"typeVersion": 1.3
},
{
"id": "57c7f3f0-a5da-4407-b94c-675fe2647b13",
"name": "Prepare Meal Plan Email Data",
"type": "n8n-nodes-base.code",
"position": [
11664,
1200
],
"parameters": {
"jsCode": "// N8N Code Node - Meal Plan Email Generator\n// Input: the meal plan JSON from your previous node\n// Output: { html: \"...\" } for the Gmail node\n\nconst items = $input.first().json.items;\n\nconst days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];\n\nfunction formatDate(dateStr) {\n const d = new Date(dateStr + 'T00:00:00');\n return {\n weekday: days[d.getDay()],\n date: d.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })\n };\n}\n\nfunction shortDesc(text) {\n // Pull out just the first sentence for a clean summary\n const first = text.split('. ')[0];\n return first.length > 160 ? first.substring(0, 157) + '...' : first;\n}\n\nfunction extractStats(desc) {\n const cal = desc.match(/(\\d+)\\s*calories/i);\n const protein = desc.match(/(\\d+)g of protein/i);\n const time = null; // pulled from recipe directly\n return {\n calories: cal ? cal[1] : null,\n protein: protein ? protein[1] : null,\n };\n}\n\nconst mealRows = items.map(item => {\n const { weekday, date } = formatDate(item.date);\n const recipe = item.recipe;\n const stats = extractStats(recipe.description);\n const desc = shortDesc(recipe.description);\n\n const statBadges = [\n recipe.totalTime ? `\u23f1 ${recipe.totalTime}` : null,\n stats.calories ? `\ud83d\udd25 ${stats.calories} cal` : null,\n stats.protein ? `\ud83d\udcaa ${stats.protein}g protein` : null,\n recipe.recipeServings ? `\ud83c\udf7d Serves ${recipe.recipeServings}` : null,\n ].filter(Boolean).map(b => `<span style=\"display:inline-block;background:#f0ebe1;color:#7a5c38;font-size:11px;font-weight:500;padding:3px 9px;border-radius:20px;margin:2px 3px 2px 0;\">${b}</span>`).join('');\n\n return `\n <tr>\n <td style=\"padding:0 0 14px 0;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#ffffff;border:1px solid #e8dfd0;border-radius:12px;overflow:hidden;\">\n <tr>\n <td style=\"background:#2c2416;width:80px;text-align:center;padding:16px 10px;vertical-align:top;\">\n <div style=\"font-family:'Georgia',serif;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:#c9a96e;line-height:1.2;\">${weekday.substring(0,3)}</div>\n <div style=\"font-family:'Georgia',serif;font-size:22px;font-weight:700;color:#fdf8f0;line-height:1.1;margin-top:2px;\">${new Date(item.date + 'T00:00:00').getDate()}</div>\n <div style=\"font-family:'Georgia',serif;font-size:10px;color:#a89070;margin-top:2px;\">${new Date(item.date + 'T00:00:00').toLocaleDateString('en-US',{month:'short'})}</div>\n </td>\n <td style=\"padding:16px 20px;vertical-align:top;\">\n <div style=\"font-family:'Georgia',serif;font-size:16px;font-weight:700;color:#2c2416;margin-bottom:6px;\">${recipe.name}</div>\n <div style=\"margin-bottom:8px;\">${statBadges}</div>\n <div style=\"font-size:13px;color:#6b5740;line-height:1.6;\">${desc}.</div>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n `;\n}).join('');\n\nconst selectOptions = items.map(item => {\n const { weekday } = formatDate(item.date);\n return `<option value=\"${item.id}|${item.recipeId}|${item.date}\">${weekday} \u2013 ${item.recipe.name}</option>`;\n}).join('\\n');\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\"/>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"/>\n</head>\n<body style=\"margin:0;padding:32px 16px;background-color:#f5f0e8;font-family:'Helvetica Neue',Arial,sans-serif;color:#2c2416;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr><td align=\"center\">\n <table width=\"620\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:620px;width:100%;\">\n\n <!-- Header -->\n <tr>\n <td style=\"background:#2c2416;border-radius:16px 16px 0 0;padding:36px 40px 28px;text-align:center;\">\n <div style=\"font-size:11px;font-weight:600;letter-spacing:0.2em;text-transform:uppercase;color:#c9a96e;margin-bottom:10px;\">Weekly Dinner Plan</div>\n <div style=\"font-family:'Georgia',serif;font-size:28px;font-weight:700;color:#fdf8f0;line-height:1.2;\">This Week's Dinners</div>\n <div style=\"margin-top:10px;font-size:14px;color:#a89070;\">Review your meals below, then let us know if you'd like to swap anything out.</div>\n </td>\n </tr>\n\n <!-- Body -->\n <tr>\n <td style=\"background:#fdf8f0;padding:32px 40px;\">\n\n <!-- Intro -->\n <p style=\"font-size:14.5px;line-height:1.7;color:#5a4a35;margin:0 0 24px 0;padding-bottom:24px;border-bottom:1px solid #e8dfd0;\">\n Here's your dinner lineup for the week. Take a look at each meal, and if anything doesn't appeal to you, use the form below to flag it for replacement.\n </p>\n\n <!-- Meal Cards -->\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n ${mealRows}\n </table>\n\n <!-- Divider -->\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 28px;\">\n <tr><td style=\"border-top:1px dashed #d4c8b5;font-size:0;\"> </td></tr>\n </table>\n </td>\n </tr>\n </table>\n </td></tr>\n </table>\n</body>\n</html>`;\n\nconst fields = items.map(item => ({\n fieldLabel: `${item.date} \u2014 ${item.recipe.name} - ${item.id}`,\n fieldType: \"checkbox\",\n fieldOptions: {\n values: [\n {\n option: \"Remove from meal plan\"\n }\n ]\n }\n}));\n\nreturn [{ json: { html, fields } }];\n"
},
"typeVersion": 2
},
{
"id": "c447f816-8a11-46b6-947b-d519b7971f30",
"name": "Fetch Recipe By Slug",
"type": "n8n-nodes-base.httpRequest",
"position": [
13296,
1184
],
"parameters": {
"url": "=http://<mealie ip address>:9925/api/recipes/{{ $json.recipe.slug }}",
"options": {},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "427e210c-fde2-42dd-9f56-3b6d064dcd83",
"name": "Sticky Note14",
"type": "n8n-nodes-base.stickyNote",
"position": [
9952,
1072
],
"parameters": {
"width": 480,
"height": 736,
"content": "## Create a meal plan with Mealie and generate a smart shopping list\n\n### How it works\n\n- Starts on a schedule to generate a week's worth of meals.\n- Sends an email with the meal plan and handles user responses.\n- Creates a shopping list in Mealie based on chosen recipes.\n- Allows optional removal of a random meal from the plan.\n\n### Setup steps\n\n- [ ] Configure the Schedule Trigger with desired frequency and timezone.\n- [ ] Set up Mealie API credentials (IP, port, authentication) in the HTTP Request nodes.\n- [ ] Enter Gmail account details for the Send Gmail node.\n\n### Customization\n\nAdjust the \"Generate Next 7 Days\" code node to change how dates are calculated or modify the email template in the Gmail node."
},
"typeVersion": 1
}
],
"connections": {
"Check for Removals": {
"main": [
[
{
"node": "Fetch Recipe By Slug",
"type": "main",
"index": 0
}
],
[
{
"node": "Split Removals Array",
"type": "main",
"index": 0
}
]
]
},
"Fetch Recipe By Slug": {
"main": [
[
{
"node": "Create Shopping List in Mealie",
"type": "main",
"index": 0
}
]
]
},
"Send Meal Plan Email": {
"main": [
[
{
"node": "Normalize User Response",
"type": "main",
"index": 0
}
]
]
},
"Split Removals Array": {
"main": [
[
{
"node": "Delete Random Meal Plan",
"type": "main",
"index": 0
}
]
]
},
"Normalize Recipe Data": {
"main": [
[
{
"node": "Add Ingredients To Shopping List",
"type": "main",
"index": 0
}
]
]
},
"Generate Upcoming Week": {
"main": [
[
{
"node": "Generate Random Meal Plan",
"type": "main",
"index": 0
}
]
]
},
"Delete Random Meal Plan": {
"main": [
[
{
"node": "Generate Random Meal Plan",
"type": "main",
"index": 0
}
]
]
},
"Normalize User Response": {
"main": [
[
{
"node": "Check for Removals",
"type": "main",
"index": 0
}
]
]
},
"Weekly Schedule Trigger": {
"main": [
[
{
"node": "Generate Upcoming Week",
"type": "main",
"index": 0
}
]
]
},
"Generate Random Meal Plan": {
"main": [
[
{
"node": "Fetch Current Week Meal Plans",
"type": "main",
"index": 0
}
]
]
},
"Prepare Meal Plan Email Data": {
"main": [
[
{
"node": "Send Meal Plan Email",
"type": "main",
"index": 0
}
]
]
},
"Fetch Current Week Meal Plans": {
"main": [
[
{
"node": "Prepare Meal Plan Email Data",
"type": "main",
"index": 0
}
]
]
},
"Create Shopping List in Mealie": {
"main": [
[
{
"node": "Normalize Recipe Data",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpBearerAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This n8n template automatically generates a full week of dinner meal plans in your self hosted Mealie instance, allows you to review and remove meals via email, replaces any removed meals with new random recipes, and finally creates a shopping list based on the finalized meal…
Source: https://n8n.io/workflows/14095/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.
Instead of providing a routine check, it focuses on significant movements by: Sending a Slack alert only if a query crosses a defined movement threshold. Emailing a structured report with the Top 25 i
Looking for a way to track GitHub bounty issues automatically and get notified in real time? This GitHub Bounty Tracker workflow monitors repositories for issues labeled 💎 Bounty, logs them in Google
This workflow automatically sends a beautifully designed HTML newsletter every Sunday at 8 AM, featuring products currently on sale from your Algolia-powered e-commerce store.
This workflow automatically identifies your weekly bestselling product from your Algolia-powered e-commerce store and generates a cinematic product video using Google VEO 3.0 AI, helping marketing tea