{
  "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;\">&nbsp;</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
          }
        ]
      ]
    }
  }
}