{
  "name": "AI Craft Pad — Labelled Gmail → Google Sheets",
  "nodes": [
    {
      "id": "manual-sample",
      "name": "Manual sample",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "configuration",
      "name": "Configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "const config={\"mode\": \"preview\", \"spreadsheet_id\": \"REPLACE_SPREADSHEET_ID\", \"range\": \"Mail!A2:D1001\", \"chat_id\": \"REPLACE_CHAT_ID\", \"gmail_query\": \"label:aicp-export newer_than:7d\", \"feed_url\": \"https://example.com/feed.xml\"};\nconst sample={\"messages\": [{\"id\": \"m1\", \"payload\": {\"headers\": [{\"name\": \"From\", \"value\": \"sam@example.com\"}, {\"name\": \"Subject\", \"value\": \"Approved project notes\"}, {\"name\": \"Date\", \"value\": \"2026-11-02\"}]}}]};\nif(![\"preview\",\"live\"].includes(config.mode))throw Error(\"Choose preview or live mode explicitly\");\nif(config.mode===\"live\"){if(config.spreadsheet_id.startsWith(\"REPLACE\")||!/^[A-Za-z0-9_-]+$/.test(config.spreadsheet_id))throw Error(\"Configure your target spreadsheet ID first\");}\nconst incoming=$input.first().json;\nreturn [{json:{config,input:incoming.body ?? sample}}];"
      }
    },
    {
      "id": "live-mode",
      "name": "Live mode",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.config.mode === \"live\" }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "local-preview",
      "name": "Local preview",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ],
      "parameters": {
        "jsCode": "/*\nMIT License\n\nCopyright (c) 2026 AI Craft Pad\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n*/\nconst module={exports:{}};\n/* AI Craft Pad original service-recipe previews and validation. MIT. No network or storage. */\n(function(root,factory){const api=factory();if(typeof module==='object'&&module.exports)module.exports=api;else root.AICPServiceRecipes=api;})(typeof window==='undefined'?this:window,function(){\n 'use strict';\n const text=(v,max=200)=>typeof v==='string'?v.trim().slice(0,max):'';\n const esc=s=>s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n function run(id,input){\n  if(!input||typeof input!=='object'||Array.isArray(input))throw Error('Use a JSON object.');\n  if(id==='form-to-hubspot'||id==='form-to-sheets'){\n   const email=text(input.email,254).toLowerCase(),event_id=text(input.event_id,80);\n   if(!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email))throw Error('A valid email shape is required.');\n   if(!/^[A-Za-z0-9_-]{1,80}$/.test(event_id))throw Error('event_id must contain 1–80 letters, numbers, underscores or hyphens.');\n   const properties={email,firstname:text(input.firstname,100),lastname:text(input.lastname,100)};\n   return {recipe:id,mode:'preview',event_id,properties,rows:[[event_id,email,properties.firstname,properties.lastname]],side_effects:'None in this preview. Live mode writes only after explicit configuration.'};\n  }\n  if(id==='gmail-to-sheets'){\n   if(!Array.isArray(input.messages)||input.messages.length>5)throw Error('Supply up to five message metadata records.');\n   const seen=new Set(),rows=[];\n   for(const m of input.messages){if(!m||!text(m.id,120))throw Error('Each message needs an id.');const key=text(m.id,120);if(seen.has(key))continue;seen.add(key);const headers=m.payload?.headers||[];if(!Array.isArray(headers))throw Error('payload.headers must be an array.');const header=n=>text(headers.find(h=>typeof h?.name==='string'&&h.name.toLowerCase()===n)?.value,250);rows.push([key,header('from'),header('subject'),header('date')]);}\n   return {recipe:id,mode:'preview',rows,side_effects:'No mailbox access or spreadsheet write in this preview.'};\n  }\n  if(id==='sheets-to-telegram'){\n   if(!Array.isArray(input.values)||input.values.length>100)throw Error('Supply up to 100 metric rows: name, current, previous.');\n   const rows=[],rejected=[];\n   input.values.forEach((r,i)=>{if(!Array.isArray(r)||!text(r[0],80)||typeof r[1]!=='number'||typeof r[2]!=='number'||![r[1],r[2],r[1]-r[2]].every(Number.isFinite)){rejected.push(i+1);return;}const percent=r[2]===0?null:Math.round((r[1]-r[2])/Math.abs(r[2])*10000)/100;if(percent!==null&&!Number.isFinite(percent)){rejected.push(i+1);return;}rows.push({metric:text(r[0],80),current:r[1],previous:r[2],percent});});\n   const message=rows.length?('Metric review\\n'+rows.map(r=>r.metric+': '+r.current+' vs '+r.previous+' ('+(r.percent===null?'n/a':r.percent+'%')+')').join('\\n')+'\\nRejected rows: '+rejected.length).slice(0,3900):'';\n   return {recipe:id,mode:'preview',rows,rejected,text:message,telegram_html:esc(message),side_effects:'No Google Sheets read or Telegram message in this preview.'};\n  }\n  if(id==='rss-to-telegram'){\n   if(!Array.isArray(input.entries)||input.entries.length>100)throw Error('Supply up to 100 parsed feed entries.');\n   const asof=Date.parse(input.as_of);if(!Number.isFinite(asof))throw Error('Supply an explicit ISO as_of time.');\n   const seen=new Set(),rows=[];\n   for(const e of input.entries){if(!e||typeof e!=='object')continue;const url=text(e.link,500),time=Date.parse(e.isoDate||e.pubDate||'');if(!/^https:\\/\\/[A-Za-z0-9.-]+(?::\\d+)?(?:[/?#][^\\s<>\\\\]*)?$/.test(url)||url.includes('@')||!Number.isFinite(time)||time>asof||time<asof-86400000||seen.has(url))continue;seen.add(url);rows.push({title:text(e.title,160)||'Untitled',url,date:new Date(time).toISOString()});}\n   rows.sort((a,b)=>b.date.localeCompare(a.date));const selected=rows.slice(0,5),message=selected.length?('Feed review — last 24 hours\\n'+selected.map(e=>e.title+'\\n'+e.url).join('\\n\\n')).slice(0,3900):'';\n   return {recipe:id,mode:'preview',rows:selected,text:message,telegram_html:esc(message),side_effects:'No feed request or Telegram message in this preview.'};\n  }\n  throw Error('Unknown service recipe.');\n }\n return {run};\n});\n\nreturn [{json:module.exports.run(\"gmail-to-sheets\", $json.input)}];"
      }
    },
    {
      "id": "result",
      "name": "Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all();"
      }
    },
    {
      "id": "list-labelled-mail",
      "name": "List labelled mail",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=5&q=\"+encodeURIComponent($(\"Configuration\").first().json.config.gmail_query) }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "gmailOAuth2",
        "options": {
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": false,
      "onError": "stopWorkflow"
    },
    {
      "id": "message-ids",
      "name": "Message IDs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "jsCode": "if($json.messages===undefined&&$json.resultSizeEstimate===0)return [];if(!Array.isArray($json.messages))throw Error(\"Unexpected Gmail list response\");return $json.messages.slice(0,5).map(m=>{if(typeof m.id!==\"string\"||!/^[A-Za-z0-9_-]+$/.test(m.id))throw Error(\"Invalid Gmail message ID\");return {json:{id:m.id}};});"
      }
    },
    {
      "id": "read-mail-metadata",
      "name": "Read mail metadata",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1540,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://gmail.googleapis.com/gmail/v1/users/me/messages/\"+encodeURIComponent($json.id)+\"?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date\" }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "gmailOAuth2",
        "options": {
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": false,
      "onError": "stopWorkflow"
    },
    {
      "id": "validated-input",
      "name": "Validated input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        0
      ],
      "parameters": {
        "jsCode": "/*\nMIT License\n\nCopyright (c) 2026 AI Craft Pad\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n*/\nconst module={exports:{}};\n/* AI Craft Pad original service-recipe previews and validation. MIT. No network or storage. */\n(function(root,factory){const api=factory();if(typeof module==='object'&&module.exports)module.exports=api;else root.AICPServiceRecipes=api;})(typeof window==='undefined'?this:window,function(){\n 'use strict';\n const text=(v,max=200)=>typeof v==='string'?v.trim().slice(0,max):'';\n const esc=s=>s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n function run(id,input){\n  if(!input||typeof input!=='object'||Array.isArray(input))throw Error('Use a JSON object.');\n  if(id==='form-to-hubspot'||id==='form-to-sheets'){\n   const email=text(input.email,254).toLowerCase(),event_id=text(input.event_id,80);\n   if(!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email))throw Error('A valid email shape is required.');\n   if(!/^[A-Za-z0-9_-]{1,80}$/.test(event_id))throw Error('event_id must contain 1–80 letters, numbers, underscores or hyphens.');\n   const properties={email,firstname:text(input.firstname,100),lastname:text(input.lastname,100)};\n   return {recipe:id,mode:'preview',event_id,properties,rows:[[event_id,email,properties.firstname,properties.lastname]],side_effects:'None in this preview. Live mode writes only after explicit configuration.'};\n  }\n  if(id==='gmail-to-sheets'){\n   if(!Array.isArray(input.messages)||input.messages.length>5)throw Error('Supply up to five message metadata records.');\n   const seen=new Set(),rows=[];\n   for(const m of input.messages){if(!m||!text(m.id,120))throw Error('Each message needs an id.');const key=text(m.id,120);if(seen.has(key))continue;seen.add(key);const headers=m.payload?.headers||[];if(!Array.isArray(headers))throw Error('payload.headers must be an array.');const header=n=>text(headers.find(h=>typeof h?.name==='string'&&h.name.toLowerCase()===n)?.value,250);rows.push([key,header('from'),header('subject'),header('date')]);}\n   return {recipe:id,mode:'preview',rows,side_effects:'No mailbox access or spreadsheet write in this preview.'};\n  }\n  if(id==='sheets-to-telegram'){\n   if(!Array.isArray(input.values)||input.values.length>100)throw Error('Supply up to 100 metric rows: name, current, previous.');\n   const rows=[],rejected=[];\n   input.values.forEach((r,i)=>{if(!Array.isArray(r)||!text(r[0],80)||typeof r[1]!=='number'||typeof r[2]!=='number'||![r[1],r[2],r[1]-r[2]].every(Number.isFinite)){rejected.push(i+1);return;}const percent=r[2]===0?null:Math.round((r[1]-r[2])/Math.abs(r[2])*10000)/100;if(percent!==null&&!Number.isFinite(percent)){rejected.push(i+1);return;}rows.push({metric:text(r[0],80),current:r[1],previous:r[2],percent});});\n   const message=rows.length?('Metric review\\n'+rows.map(r=>r.metric+': '+r.current+' vs '+r.previous+' ('+(r.percent===null?'n/a':r.percent+'%')+')').join('\\n')+'\\nRejected rows: '+rejected.length).slice(0,3900):'';\n   return {recipe:id,mode:'preview',rows,rejected,text:message,telegram_html:esc(message),side_effects:'No Google Sheets read or Telegram message in this preview.'};\n  }\n  if(id==='rss-to-telegram'){\n   if(!Array.isArray(input.entries)||input.entries.length>100)throw Error('Supply up to 100 parsed feed entries.');\n   const asof=Date.parse(input.as_of);if(!Number.isFinite(asof))throw Error('Supply an explicit ISO as_of time.');\n   const seen=new Set(),rows=[];\n   for(const e of input.entries){if(!e||typeof e!=='object')continue;const url=text(e.link,500),time=Date.parse(e.isoDate||e.pubDate||'');if(!/^https:\\/\\/[A-Za-z0-9.-]+(?::\\d+)?(?:[/?#][^\\s<>\\\\]*)?$/.test(url)||url.includes('@')||!Number.isFinite(time)||time>asof||time<asof-86400000||seen.has(url))continue;seen.add(url);rows.push({title:text(e.title,160)||'Untitled',url,date:new Date(time).toISOString()});}\n   rows.sort((a,b)=>b.date.localeCompare(a.date));const selected=rows.slice(0,5),message=selected.length?('Feed review — last 24 hours\\n'+selected.map(e=>e.title+'\\n'+e.url).join('\\n\\n')).slice(0,3900):'';\n   return {recipe:id,mode:'preview',rows:selected,text:message,telegram_html:esc(message),side_effects:'No feed request or Telegram message in this preview.'};\n  }\n  throw Error('Unknown service recipe.');\n }\n return {run};\n});\n\nreturn [{json:module.exports.run(\"gmail-to-sheets\",{messages:$input.all().map(i=>i.json)})}];"
      }
    },
    {
      "id": "read-ledger",
      "name": "Read ledger",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1980,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://sheets.googleapis.com/v4/spreadsheets/\"+encodeURIComponent($(\"Configuration\").first().json.config.spreadsheet_id)+\"/values/\"+encodeURIComponent($(\"Configuration\").first().json.config.range) }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "options": {
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": false,
      "onError": "stopWorkflow"
    },
    {
      "id": "append-plan",
      "name": "Append plan",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2200,
        0
      ],
      "parameters": {
        "jsCode": "if(typeof $json.range!==\"string\"||($json.values!==undefined&&!Array.isArray($json.values)))throw Error(\"Unexpected Sheets response\");const known=new Set(($json.values||[]).map(r=>String(r[0])));const rows=$(\"Validated input\").first().json.rows.filter(r=>!known.has(r[0]));return [{json:{rows,write:rows.length>0,status:rows.length?\"new-records\":\"already-recorded\"}}];"
      }
    },
    {
      "id": "new-records",
      "name": "New records",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2420,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.write === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "append-rows",
      "name": "Append rows",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2640,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ \"https://sheets.googleapis.com/v4/spreadsheets/\"+encodeURIComponent($(\"Configuration\").first().json.config.spreadsheet_id)+\"/values/\"+encodeURIComponent($(\"Configuration\").first().json.config.range)+\":append?valueInputOption=RAW&insertDataOption=INSERT_ROWS\" }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "options": {
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {majorDimension:\"ROWS\",values:$json.rows} }}"
      },
      "retryOnFail": false,
      "onError": "stopWorkflow"
    }
  ],
  "connections": {
    "Manual sample": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Live mode",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Live mode": {
      "main": [
        [
          {
            "node": "List labelled mail",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Local preview",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List labelled mail": {
      "main": [
        [
          {
            "node": "Message IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message IDs": {
      "main": [
        [
          {
            "node": "Read mail metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read mail metadata": {
      "main": [
        [
          {
            "node": "Validated input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validated input": {
      "main": [
        [
          {
            "node": "Read ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read ledger": {
      "main": [
        [
          {
            "node": "Append plan",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append plan": {
      "main": [
        [
          {
            "node": "New records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New records": {
      "main": [
        [
          {
            "node": "Append rows",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append rows": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "none",
    "saveDataSuccessExecution": "none",
    "saveManualExecutions": false
  },
  "pinData": {},
  "tags": []
}
