{
  "name": "Shiny Gmail Daily Orchestrator V4.0 Lean",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * *"
            }
          ]
        }
      },
      "id": "b9ee593d-c820-40d0-ae6b-212c03a02003",
      "name": "Daily Schedule 6AM Manila",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const crypto = require('crypto');\nfunction intEnv(k,d,min,max){const n=Number($env[k]);return Number.isFinite(n)?Math.min(Math.max(Math.trunc(n),min),max):d;}\nconst account=$env.GMAIL_ACCOUNT||'primary';\nconst required=['GEMINI_API_KEY','TELEGRAM_CHAT_ID'];\nconst missing=required.filter(k=>!$env[k]);\nreturn [{json:{workflowName:'Shiny Gmail Daily Orchestrator V4.0 Lean',runId:crypto.randomUUID(),account,startedAt:new Date().toISOString(),envOk:missing.length===0,missingEnv:missing,batchSize:intEnv('GMAIL_BATCH_SIZE',50,1,500),waitSeconds:intEnv('WAIT_BETWEEN_EMAILS',5,0,300),categories:['priority','finances','accounts-subscriptions','account-security','unimportant','needs-review'],processedLabel:'processed'}}];"
      },
      "id": "bb73e54d-1381-4f77-a369-80d41f208227",
      "name": "Build Context And Validate Env",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.envOk }}",
              "value2": true
            }
          ]
        }
      },
      "id": "50d9a32c-ad6a-4679-b890-95229e9c1741",
      "name": "Env OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const c=$('Build Context And Validate Env').first().json;return [{json:{chatId:$env.TELEGRAM_CHAT_ID,text:`\ud83d\udeab <b>Shiny Gmail V4.0 blocked</b>\n\nMissing env var(s): <code>${(c.missingEnv||[]).join(', ')}</code>\nNo emails were touched.`}}];"
      },
      "id": "57c4ea99-dadc-4a79-a880-0ffc1298bbf4",
      "name": "Build Env Error Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        160
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{\"WITH expired AS (UPDATE gmail_workflow_runs SET status='timed_out', error_message='Stale run auto-cleaned by next daily run.', finished_at=NOW(), updated_at=NOW() WHERE account='\" + $('Build Context And Validate Env').first().json.account.replace(/'/g,\"''\") + \"' AND status='running' AND started_at <= NOW() - INTERVAL '30 minutes' RETURNING run_id), active AS (SELECT run_id, started_at FROM gmail_workflow_runs WHERE account='\" + $('Build Context And Validate Env').first().json.account.replace(/'/g,\"''\") + \"' AND status='running' AND started_at > NOW() - INTERVAL '30 minutes' ORDER BY started_at DESC LIMIT 1), last_done AS (SELECT finished_at, completed_count FROM gmail_workflow_runs WHERE account='\" + $('Build Context And Validate Env').first().json.account.replace(/'/g,\"''\") + \"' AND status LIKE 'completed%' AND finished_at IS NOT NULL ORDER BY finished_at DESC LIMIT 1) SELECT EXISTS(SELECT 1 FROM active) AS active_run, (SELECT COUNT(*) FROM expired) AS expired_count, (SELECT run_id FROM active) AS active_run_id, (SELECT finished_at FROM last_done) AS last_completed_at, (SELECT completed_count FROM last_done) AS last_completed_count;\"}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "8074ef0c-ffe2-4d0a-b992-744552f277ef",
      "name": "Simple Run Guard Query",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        660,
        -80
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const c=$('Build Context And Validate Env').first().json;const row=$input.first()?.json||{};const dbError=!!row.error;return [{json:{...c,activeRun:dbError?true:Boolean(row.active_run),activeRunId:row.active_run_id||null,expiredStaleRuns:Number(row.expired_count||0),lastCompletedAt:row.last_completed_at||null,lastCompletedCount:row.last_completed_count??null,guardDbError:dbError,guardErrorMessage:row.error?.message||null}}];"
      },
      "id": "cf76a126-4000-4776-8497-afcc75e2c19f",
      "name": "Evaluate Run Guard",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        -80
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.activeRun }}",
              "value2": true
            }
          ]
        }
      },
      "id": "3e882314-83b2-40ef-b119-b8048dc7725d",
      "name": "Active Run?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        1100,
        -80
      ]
    },
    {
      "parameters": {
        "jsCode": "const j=$input.first().json;const reason=j.guardDbError?'Run guard database check failed; fail-safe skip activated.':`Another run is already active${j.activeRunId?` (${j.activeRunId})`:''}.`;let text=`\u23ed\ufe0f <b>Shiny Gmail V4.0 skipped</b>\n\n${reason}\n\nNo emails were touched.`;if(j.expiredStaleRuns)text+=`\n\n\u26a0\ufe0f Auto-cleaned ${Number(j.expiredStaleRuns)} stale run(s) from a previous crash before skipping.`;return [{json:{chatId:$env.TELEGRAM_CHAT_ID,text}}];"
      },
      "id": "7f6cb461-3db4-4db4-a064-4984f88f80d5",
      "name": "Build Skipped Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        60
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{\"INSERT INTO gmail_workflow_runs (run_id, account, status, started_at, created_at, updated_at) VALUES ('\" + $json.runId.replace(/'/g,\"''\") + \"','\" + $json.account.replace(/'/g,\"''\") + \"','running', NOW(), NOW(), NOW()) RETURNING run_id, account, status, started_at;\"}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "30a4531d-1e0c-4cbf-b92a-ffb26d49ce2e",
      "name": "Mark Run Started",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1320,
        -180
      ],
      "onError": "continueRegularOutput",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "resource": "label",
        "operation": "getAll",
        "returnAll": true
      },
      "id": "f37ee71a-d7f5-4a1a-a031-2a4d2f74fce9",
      "name": "Get Gmail Labels",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1540,
        -180
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{ \"SELECT sender_pattern, match_type, rule_type, priority, enabled, created_at FROM gmail_sender_rules WHERE account='\" + $('Build Context And Validate Env').first().json.account.replace(/'/g, \"''\") + \"' AND enabled = true ORDER BY priority ASC, CASE WHEN match_type='exact' THEN 0 ELSE 1 END, created_at DESC;\" }}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "243af4ae-c5e8-4170-b7db-0c713b34619a",
      "name": "Get Sender Rules",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1760,
        -180
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "getAll",
        "returnAll": false,
        "limit": "={{ $('Build Context And Validate Env').first().json.batchSize }}",
        "filters": {
          "q": "is:inbox -label:processed"
        }
      },
      "id": "b0aa08ed-d604-4203-abd0-cb0c65bcf941",
      "name": "Fetch Inbox Emails",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1980,
        -180
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const context=$('Build Context And Validate Env').first().json;\nconst guard=(()=>{try{return $('Evaluate Run Guard').first().json}catch{return {}}})();\nfunction all(name){try{return $(name).all().map(i=>i.json)}catch{return []}}\nfunction errorCode(err){const msg=String(err?.message||err||'');const m=msg.match(/status\\s*code\\s*(\\d{3})/i)||msg.match(/\\b(401|403|429|5\\d\\d)\\b/);return Number(err?.httpCode||err?.statusCode||err?.code||(m?m[1]:0));}\nfunction gmailHint(err,what){const c=errorCode(err);if(c===401)return `${what}: Gmail credential expired. Re-authorize in n8n credentials.`;if(c===403)return `${what}: Gmail API access forbidden. Check OAuth scopes.`;if(c===429)return `${what}: Gmail API rate limit hit. Will retry on next run.`;if(c>=500)return `${what}: Gmail server error. Transient \u2014 will retry on next run.`;return `${what}: Unexpected Gmail error (${c||'unknown'}).`;}\nfunction empty(status,msg,extra={}){return [{json:{noWork:true,runId:context.runId,account:context.account,startedAt:context.startedAt,status,totalDiscovered:0,completed:0,failed:0,needsReview:0,batchSaturated:false,categoryCounts:{},sourceCounts:{},confidenceBands:{high:0,medium:0,review:0,failed:0},inlineDigest:[],reviewDetails:[],failedDetails:[],warningDetails:[],warningCount:0,reviewReasonCounts:{},ruleSuggestions:[],ruleWarning:null,lastCompletedAt:guard.lastCompletedAt||null,lastCompletedCount:guard.lastCompletedCount??null,errorMessage:msg,...extra}}]}\ntry{\n const runStarted=$('Mark Run Started').first()?.json||{};\n function extractEmail(raw){const m=String(raw||'').match(/[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}/i);return m?m[0].toLowerCase():'';}\n function getHeader(email,name){const h=(email.payload?.headers||email.headers||[]).find(x=>String(x.name||'').toLowerCase()===name.toLowerCase());return h?.value||'';}\n function domainOf(sender){return String(sender||'').split('@').pop().toLowerCase()}\n function parseDateMs(s){const ms=new Date(s||0).getTime();return Number.isFinite(ms)?ms:0;}\n function ruleCategory(rule){const t=String(rule.rule_type||'').toLowerCase();if(t==='whitelist')return'priority';if(t==='blacklist')return'unimportant';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'finances';if(['review','force_review'].includes(t))return'needs-review';return null;}\n function ruleToSource(rule){const t=String(rule.rule_type||'').toLowerCase();if(t==='whitelist')return'whitelist';if(t==='blacklist')return'blacklist';if(['receipt','force_receipts','finance','force_finance'].includes(t))return'force_finance';if(['review','force_review'].includes(t))return'force_review';return rule.rule_type||'rule_matched';}\n function ruleSort(a,b){const p=Number(a.priority??100)-Number(b.priority??100);if(p!==0)return p;const mt=(a.match_type==='exact'?0:1)-(b.match_type==='exact'?0:1);if(mt!==0)return mt;return String(b.created_at||'').localeCompare(String(a.created_at||''));}\n function findRule(sender,rules){const d=domainOf(sender);for(const r of rules.filter(x=>x.enabled!==false&&x.enabled!=='false').sort(ruleSort)){const p=String(r.sender_pattern||'').toLowerCase().trim();const mt=String(r.match_type||'exact').toLowerCase();if(!p)continue;if(mt==='exact'&&sender===p)return r;if(mt==='domain'&&(d===p||d.endsWith('.'+p)))return r;}return null;}\n function rank(item){const c=item.json.ruleCategory;if(c==='priority')return 0;if(c==='finances')return 1;if(c==='needs-review')return 2;if(c==='unimportant')return 3;if(item.json.needsGemini)return 4;return 5;}\n if(runStarted.error||!runStarted.run_id)return empty('failed',`Run record could not be created. Gmail label/email fetch may have occurred, but no Gmail labels/archive mutations were performed.${runStarted.error?.message?' Reason: '+String(runStarted.error.message).slice(0,300):''}`,{runStartError:true});\n const labelRows=all('Get Gmail Labels');const labelErr=labelRows.find(r=>r.error);if(labelErr)return empty('failed',`${gmailHint(labelErr.error,'Could not load Gmail labels')} No Gmail mutations were performed.`,{labelError:true});\n const fetchRows=all('Fetch Inbox Emails');const fetchErr=fetchRows.find(r=>r.error);if(fetchErr)return empty('failed',`${gmailHint(fetchErr.error,'Could not fetch inbox')} No Gmail mutations were performed.`,{gmailFetchError:true});\n const labelMap={};for(const l of labelRows){const name=l.name||l.Name||l.labelName||'';const id=l.id||l.labelId||l.Id||'';if(name&&id)labelMap[name]=id;}\n const missing=[...context.categories,context.processedLabel].filter(x=>!labelMap[x]);if(missing.length)return empty('failed',`Missing Gmail label(s): ${missing.join(', ')}. Create them at: https://mail.google.com/mail/u/0/#settings/labels`,{labelError:true});\n const ruleRows=all('Get Sender Rules');const ruleDbError=ruleRows.some(r=>r.error);const rules=ruleDbError?[]:ruleRows;const ruleWarning=ruleDbError?'Sender rules unavailable; processed using AI/rule-free path.':null;\n const seen=new Set(),out=[];\n for(const email of fetchRows){const messageId=email.id||email.messageId||email.gmail_message_id;if(!messageId||seen.has(messageId))continue;seen.add(messageId);const fromRaw=email.From||email.from||getHeader(email,'from');const toRaw=email.To||email.to||getHeader(email,'to')||'';const ccRaw=email.Cc||email.cc||getHeader(email,'cc')||'';const sender=extractEmail(fromRaw);const subject=email.Subject||email.subject||getHeader(email,'subject')||'(no subject)';const subjectClean=String(subject||'').trim();const snippet=String(email.snippet||'').slice(0,300);const barrenMetadata=!sender&&(!subjectClean||subjectClean==='(no subject)')&&!snippet.trim();const matched=sender?findRule(sender,rules):null;const category=matched?ruleCategory(matched):(barrenMetadata?'needs-review':null);const source=matched?ruleToSource(matched):(barrenMetadata?'metadata_empty':null);const date=email.Date||email.date||getHeader(email,'date')||'';out.push({json:{runId:context.runId,account:context.account,startedAt:context.startedAt,totalDiscovered:seen.size,messageId,threadId:email.threadId||email.thread_id||null,sender,fromRaw:String(fromRaw||'').trim(),toRaw:String(toRaw).slice(0,200),ccRaw:String(ccRaw).slice(0,150),subject:String(subject).slice(0,200),date,snippet,labelMap,processedLabelId:labelMap[context.processedLabel],ruleCategory:category,ruleSource:source,confidence:barrenMetadata?0:undefined,reasoning:barrenMetadata?'No usable sender, subject, or snippet metadata was available.':undefined,needsGemini:!category,ruleMode:ruleDbError?'ai_only_rules_unavailable':'rules_enabled',ruleWarning,lastCompletedAt:guard.lastCompletedAt||null,lastCompletedCount:guard.lastCompletedCount??null}})}\n if(!out.length)return empty('completed',null,{ruleWarning});out.sort((a,b)=>{const r=rank(a)-rank(b);if(r!==0)return r;if(a.json.needsGemini&&b.json.needsGemini){const ta=a.json.threadId||'',tb=b.json.threadId||'';if(ta!==tb)return ta<tb?-1:1;}return parseDateMs(a.json.date)-parseDateMs(b.json.date);});const total=out.length;const batchSaturated=total>=context.batchSize;for(const item of out){item.json.totalDiscovered=total;item.json.batchSaturated=batchSaturated;}return out;\n}catch(e){return empty('failed',`Normalization failed before Gmail mutations. ${String(e.message||e).slice(0,300)}`,{normalizationError:true});}"
      },
      "id": "f173d740-0fd3-4b38-bf0d-03b641d73956",
      "name": "Normalize Emails And Apply Rules",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2200,
        -180
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ Boolean($json.noWork) }}",
              "value2": true
            }
          ]
        }
      },
      "id": "ec0554cd-e643-4e9c-8de9-3c5fcad40143",
      "name": "No Work?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        2420,
        -180
      ]
    },
    {
      "parameters": {
        "jsCode": "const j=$input.first().json;return [{json:{runId:j.runId,account:j.account,startedAt:j.startedAt,finishedAt:new Date().toISOString(),status:j.status||'completed',totalDiscovered:j.totalDiscovered||0,completed:j.completed||0,failed:j.failed||0,needsReview:j.needsReview||0,batchSaturated:!!j.batchSaturated,categoryCounts:j.categoryCounts||{},sourceCounts:j.sourceCounts||{},confidenceBands:j.confidenceBands||{high:0,medium:0,review:0,failed:0},inlineDigest:j.inlineDigest||[],reviewDetails:j.reviewDetails||[],failedDetails:j.failedDetails||[],warningDetails:j.warningDetails||[],warningCount:j.warningCount||0,reviewReasonCounts:j.reviewReasonCounts||{},ruleSuggestions:j.ruleSuggestions||[],ruleWarning:j.ruleWarning||null,lastCompletedAt:j.lastCompletedAt||null,lastCompletedCount:j.lastCompletedCount??null,errorMessage:j.errorMessage||null,labelError:!!j.labelError,runStartError:!!j.runStartError,gmailFetchError:!!j.gmailFetchError,normalizationError:!!j.normalizationError}}];"
      },
      "id": "b1db2b00-32a4-4480-904f-566e4962d3a2",
      "name": "Build No Work Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2640,
        -40
      ]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "dcff46fb-5981-41db-bb17-ffe519dcf64c",
      "name": "Split Emails",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        2640,
        -300
      ]
    },
    {
      "parameters": {
        "workflowId": "REPLACE_WITH_WORKFLOW_B_ID",
        "options": {}
      },
      "id": "7275fe04-8556-43f4-9482-28733b6a66db",
      "name": "Call Single Email Processor",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.1,
      "position": [
        2860,
        -420
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const input=$input.first()?.json||{};const parent=(()=>{try{return $('Split Emails').first().json}catch{return {}}})();function isAiSource(s){return ['ai','ai_low_confidence','ai_failed'].includes(String(s||''));}function waitMultiplierFrom(obj){const err=String(obj.errorMessage||obj.error?.message||'').toLowerCase();return(err.includes('429')||err.includes('503')||err.includes('unavailable')||err.includes('resource_exhausted')||err.includes('rate'))?3:1;}if(input.error)return[{json:{status:'failed',messageId:parent.messageId||input.messageId||null,sender:parent.sender||null,subject:parent.subject||null,category:'needs-review',source:'child_execution_failed',confidence:0,reasoning:'Child workflow execution failed.',errorMessage:String(input.error.message||input.error||'Unknown child error').slice(0,500),failureStage:'child_execution_failed',totalDiscovered:parent.totalDiscovered||0,batchSaturated:!!parent.batchSaturated,ruleWarning:parent.ruleWarning||null,usedGeminiBudget:Boolean(parent.needsGemini),dynamicWaitMultiplier:waitMultiplierFrom(input)}}];const usedGeminiBudget=Boolean(parent.needsGemini||isAiSource(input.source));return[{json:{...input,totalDiscovered:parent.totalDiscovered||input.totalDiscovered||0,batchSaturated:!!(parent.batchSaturated||input.batchSaturated),ruleWarning:parent.ruleWarning||input.ruleWarning||null,usedGeminiBudget,dynamicWaitMultiplier:waitMultiplierFrom(input)}}];"
      },
      "id": "4ca3c517-0cc1-4972-8316-14333c1e0968",
      "name": "Normalize Child Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        -420
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ Boolean($json.usedGeminiBudget) }}",
              "value2": true
            }
          ]
        }
      },
      "id": "b1bb743c-109d-4bee-bd14-255c48cfc88b",
      "name": "Used Gemini?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        3300,
        -420
      ]
    },
    {
      "parameters": {
        "amount": "={{ $('Build Context And Validate Env').first().json.waitSeconds * ($json.dynamicWaitMultiplier || 1) }}",
        "unit": "seconds"
      },
      "id": "a96f425e-6037-412e-ba77-e16b3e748bc7",
      "name": "Wait Between Emails",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        3520,
        -500
      ]
    },
    {
      "parameters": {
        "jsCode": "function countBy(arr,key){const o={};for(const r of arr){const v=r[key]||'unknown';o[v]=(o[v]||0)+1;}return o;}function reviewHint(r){if(!r.sender)return'missing sender';if(r.source==='metadata_empty')return'missing metadata';if(r.source==='ledger_reuse')return'ledger-inherited';if(r.source==='ai_low_confidence')return'low AI confidence';if(r.source==='ai_failed')return'AI unavailable';if(r.source==='force_review')return'operator rule';if(r.source==='thread_reuse')return'thread-inherited';return'ambiguous metadata';}\nconst context=$('Build Context And Validate Env').first().json;const guard=(()=>{try{return $('Evaluate Run Guard').first().json}catch{return {}}})();const results=$input.all().map(i=>i.json||{}).filter(r=>r.messageId||r.status);const completedItems=[],failedItems=[],reviewItems=[],warningItems=[];let ledgerWarningCount=0,ruleWarning=null,maxDiscovered=results.length,batchSaturated=results.some(r=>r.batchSaturated);const confidenceBands={high:0,medium:0,review:0,failed:0};\nfor(const r of results){maxDiscovered=Math.max(maxDiscovered,Number(r.totalDiscovered||0));const status=String(r.status||'');const done=status.startsWith('completed');if(done)completedItems.push(r);else failedItems.push(r);if(status==='completed_with_warnings')warningItems.push(r);if(done&&r.category==='needs-review')reviewItems.push(r);if(r.ledgerWarning)ledgerWarningCount++;if(!ruleWarning&&r.ruleWarning)ruleWarning=r.ruleWarning;if(['ai','ai_low_confidence','ai_failed'].includes(r.source)){const c=Number(r.confidence||0);if(r.source==='ai_failed')confidenceBands.failed++;else if(r.category==='needs-review')confidenceBands.review++;else if(c>=0.9)confidenceBands.high++;else if(c>=0.8)confidenceBands.medium++;}}\nconst inlineDigest=completedItems.slice(0,7).map(r=>({messageId:r.messageId||null,sender:r.sender||'unknown',subject:String(r.subject||'(no subject)').slice(0,80),category:r.category||null,source:r.source||null,confidence:r.confidence??0}));\nconst failedDetails=failedItems.slice(0,5).map(r=>({messageId:r.messageId||null,sender:r.sender||'unknown',subject:String(r.subject||'(no subject)').slice(0,80),category:r.category||null,source:r.source||null,failureStage:r.failureStage||null,errorMessage:r.errorMessage||null}));const reviewDetails=reviewItems.slice(0,5).map(r=>({messageId:r.messageId||null,sender:r.sender||'unknown',subject:String(r.subject||'(no subject)').slice(0,80),source:r.source||'unknown',confidence:r.confidence??0,hint:reviewHint(r),reasoning:r.reasoning||'No reasoning.'}));const reviewReasonCounts={},runReviewSenders={};for(const r of reviewItems){const hint=reviewHint(r);reviewReasonCounts[hint]=(reviewReasonCounts[hint]||0)+1;if(r.sender)runReviewSenders[r.sender]=(runReviewSenders[r.sender]||0)+1;}const ruleSuggestions=Object.entries(runReviewSenders).filter(([s,c])=>c>=2).sort((a,b)=>b[1]-a[1]).slice(0,3).map(([sender,count])=>({sender,count,hint:`${count}\u00d7 needs-review \u2192 /review ${sender}, /receipt ${sender}, or /whitelist ${sender}`}));const warningDetails=warningItems.slice(0,3).map(r=>({messageId:r.messageId||null,sender:r.sender||'unknown',subject:String(r.subject||'(no subject)').slice(0,80),errorMessage:r.errorMessage||'Completed with warning.'}));return[{json:{runId:context.runId,account:context.account,startedAt:context.startedAt,finishedAt:new Date().toISOString(),status:failedItems.length?'completed_with_failures':(warningItems.length?'completed_with_warnings':'completed'),totalDiscovered:maxDiscovered,completed:completedItems.length,failed:failedItems.length,needsReview:reviewItems.length,batchSaturated,categoryCounts:countBy(completedItems,'category'),sourceCounts:countBy(completedItems,'source'),confidenceBands,ledgerWarningCount,warningCount:warningItems.length,reviewReasonCounts,ruleSuggestions,ruleWarning,lastCompletedAt:guard.lastCompletedAt||null,lastCompletedCount:guard.lastCompletedCount??null,inlineDigest,reviewDetails,failedDetails,warningDetails}}];"
      },
      "id": "54389f25-d735-40c1-bf94-62c304661f78",
      "name": "Build Final Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2860,
        -180
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{\"UPDATE gmail_workflow_runs SET status='\"+String($json.status||'completed').replace(/'/g,\"''\")+\"', total_discovered=\"+Number($json.totalDiscovered||0)+\", completed_count=\"+Number($json.completed||0)+\", failed_count=\"+Number($json.failed||0)+\", review_count=\"+Number($json.needsReview||0)+\", error_message=\"+($json.errorMessage?\"'\"+String($json.errorMessage).slice(0,500).replace(/'/g,\"''\")+\"'\":\"NULL\")+\", finished_at=NOW(), updated_at=NOW() WHERE run_id='\"+String($json.runId).replace(/'/g,\"''\")+\"' RETURNING status, finished_at;\"}}",
        "options": {
          "connectionTimeout": 10,
          "delayClosingIdleConnection": 5
        }
      },
      "id": "fe1efb20-ddfc-43a7-9ac0-ac7dcc30bae8",
      "name": "Mark Run Finished",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        3080,
        -40
      ],
      "onError": "continueRegularOutput",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "function getNode(name){try{return $(name).first().json}catch{return null}}function h(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}function clampTelegram(text,max=3900){if(text.length<=max)return text;let cut=text.slice(0,max-180);const lastLt=cut.lastIndexOf('<'),lastGt=cut.lastIndexOf('>');if(lastLt>lastGt)cut=cut.slice(0,lastLt);for(const tag of ['b','i','code','pre','a','s','u']){const opens=(cut.match(new RegExp(`<${tag}[\\\\s>]`,'gi'))||[]).length;const closes=(cut.match(new RegExp(`</${tag}>`,'gi'))||[]).length;for(let i=0;i<opens-closes;i++)cut+=`</${tag}>`;}return cut+'\\n\\n\u2026truncated. Use /recent or DB for full details.';}function fmtTime(ts){try{return new Date(ts).toLocaleString('en-PH',{timeZone:'Asia/Manila',hour12:true})}catch{return String(ts||'unknown')}}function relativeTime(dateStr){const ms=Date.now()-new Date(dateStr||0).getTime();if(!dateStr||!Number.isFinite(ms))return'unknown';const min=Math.floor(ms/60000);if(min<1)return'just now';if(min<60)return`${min}m ago`;const hr=Math.floor(min/60);if(hr<24)return`${hr}h ago`;const d=Math.floor(hr/24);if(d<7)return`${d}d ago`;return`${Math.floor(d/7)}w ago`;}function duration(a,b){const ms=new Date(b).getTime()-new Date(a).getTime();if(!Number.isFinite(ms)||ms<0)return'unknown';const s=Math.round(ms/1000),m=Math.floor(s/60),r=s%60;if(m>=60){const h=Math.floor(m/60),rm=m%60;return `${h}h ${rm}m ${r}s`;}return m?`${m}m ${r}s`:`${r}s`}function emojiCat(c){const e={'priority':'\ud83d\udd34','finances':'\ud83d\udcb0','accounts-subscriptions':'\ud83d\udd14','account-security':'\ud83d\udd10','unimportant':'\ud83d\uddd1','needs-review':'\ud83d\udd0e'};return `${e[c]||''} ${c||'unknown'}`.trim();}function countsLine(obj){const order=['priority','finances','accounts-subscriptions','account-security','unimportant','needs-review'];const parts=order.filter(k=>obj?.[k]).map(k=>`${emojiCat(k)}: ${obj[k]}`);return parts.length?parts.join(' | '):'none'}function sourceLine(src){const order=['whitelist','blacklist','force_finance','force_review','thread_reuse','ledger_reuse','metadata_empty','ai','ai_low_confidence','ai_failed'];const seen=new Set(),parts=[];for(const k of order){if(src?.[k]){parts.push(`${k}: ${src[k]}`);seen.add(k);}}for(const [k,v] of Object.entries(src||{})){if(!seen.has(k))parts.push(`${k}: ${v}`);}return parts.length?parts.join(' | '):'none'}function link(id){return id?`https://mail.google.com/mail/u/0/#all/${encodeURIComponent(id)}`:null}\nconst summary=getNode('Build Final Summary')||getNode('Build No Work Summary')||$input.first().json||{};const db=$input.first().json||{};const guard=getNode('Evaluate Run Guard')||{};const dbError=!!db.error;\nif(!summary.errorMessage&&!summary.labelError&&!summary.runStartError&&!summary.gmailFetchError&&!summary.normalizationError&&(summary.totalDiscovered||0)===0&&!summary.ruleWarning&&!guard.expiredStaleRuns){let condensed=`\u2705 \ud83e\uddf9 No new emails \u00b7 ${h(duration(summary.startedAt,summary.finishedAt||new Date().toISOString()))}`;if(summary.lastCompletedAt){condensed+=` \u00b7 Last activity: ${h(relativeTime(summary.lastCompletedAt))}`;if(summary.lastCompletedCount!=null)condensed+=` (${summary.lastCompletedCount})`;const age=Date.now()-new Date(summary.lastCompletedAt).getTime();if(Number.isFinite(age)&&age>48*3600*1000)condensed+=`\\n\u26a0\ufe0f No completed run in 48+ hours \u2014 verify Gmail credentials.`;}return[{json:{chatId:$env.TELEGRAM_CHAT_ID,text:condensed}}];}\nconst icon=summary.status==='failed'||dbError?'\u26a0\ufe0f':summary.failed?'\u26a0\ufe0f':'\u2705';let verdict='\u2705 Clean run \u2014 no action needed.';if(summary.failed)verdict='\u26a0\ufe0f Processing failures need log review.';else if(summary.warningCount)verdict='\u26a0\ufe0f Processed with warnings; check linked emails.';else if(summary.needsReview)verdict=`\ud83d\udd0e Review ${summary.needsReview} uncertain email(s).`;else if(summary.ruleWarning)verdict='\ud83d\udeab Automation degraded \u2014 sender rules unavailable.';let text=`${icon} <b>\ud83e\uddf9 Shiny Gmail Daily Run</b>\\n<b>Verdict:</b> ${h(verdict)}\\n\\n`;\ntext+=`<b>Status:</b> ${h(summary.status||'completed')}\\n<b>Started:</b> ${h(fmtTime(summary.startedAt))}\\n<b>Duration:</b> ${h(duration(summary.startedAt,summary.finishedAt||new Date().toISOString()))}\\n`;if(summary.lastCompletedAt){text+=`<b>Last completed run:</b> ${h(relativeTime(summary.lastCompletedAt))}${summary.lastCompletedCount!=null?` (${summary.lastCompletedCount} email(s))`:''}\\n`;}text+=`\\n`;\ntext+=`<b>Emails found:</b> ${summary.totalDiscovered||0}\\n`;if(summary.batchSaturated)text+=`\u26a0\ufe0f Batch full (${summary.totalDiscovered}/${$('Build Context And Validate Env').first().json.batchSize}) \u2014 more emails may be waiting in inbox.\\n`;text+=`<b>Completed:</b> ${summary.completed||0}\\n<b>Failures:</b> ${summary.failed||0}\\n<b>Needs-review:</b> ${summary.needsReview||0}\\n\\n`;\ntext+=`<b>Categories:</b> ${h(countsLine(summary.categoryCounts||{}))}\\n`;const src=summary.sourceCounts||{};text+=`<b>Sources:</b> ${h(sourceLine(src))}\\n`;const totalSrc=Object.values(src).reduce((s,v)=>s+Number(v||0),0);const aiCount=Number(src.ai||0)+Number(src.ai_low_confidence||0)+Number(src.ai_failed||0);if(totalSrc>0){const rules=Number(src.whitelist||0)+Number(src.blacklist||0)+Number(src.force_finance||0)+Number(src.force_review||0);const thread=Number(src.thread_reuse||0);const ledger=Number(src.ledger_reuse||0);const explicitBypass=rules+thread+ledger;const otherNonAi=Math.max(0,totalSrc-aiCount-explicitBypass);text+=`\u26a1 AI bypass: ${Math.round((explicitBypass/totalSrc)*100)}% (${explicitBypass}/${totalSrc}) \u2014 rules: ${rules}, thread: ${thread}, ledger: ${ledger}`;if(otherNonAi)text+=` \u00b7 other non-AI: ${otherNonAi}`;text+=`\\n`;}\nif((summary.completed||0)<=7&&Array.isArray(summary.inlineDigest)&&summary.inlineDigest.length){text+=`\\n<b>Processed emails</b>\\n`+summary.inlineDigest.map((r,i)=>{const l=link(r.messageId);const subj=h(String(r.subject||'(no subject)').slice(0,60));const srcTxt=r.source==='ai'?`ai\u00b7${h(r.confidence)}`:h(r.source||'');return`${i+1}. ${emojiCat(r.category)} \u2014 ${h(r.sender||'unknown')}\\n${l?`<a href=\"${l}\">${subj}</a>`:subj} [${srcTxt}]`;}).join('\\n');}\nconst bands=summary.confidenceBands||{};if((bands.high||bands.medium||bands.review||bands.failed)||summary.ruleWarning||summary.ledgerWarningCount||summary.warningCount||guard.expiredStaleRuns||summary.needsReview||summary.batchSaturated){text+=`\\n\\n<b>Diagnostics</b>\\n`;if(summary.batchSaturated)text+=`\u26a0\ufe0f Batch saturated: increase GMAIL_BATCH_SIZE or trigger another run.\\n`;if(bands.high||bands.medium||bands.review||bands.failed)text+=`AI confidence: high ${Number(bands.high||0)} | medium ${Number(bands.medium||0)} | review ${Number(bands.review||0)} | failed ${Number(bands.failed||0)}\\n`;if(summary.needsReview&&summary.reviewReasonCounts)text+=`\ud83d\udd0e Review causes: `+Object.entries(summary.reviewReasonCounts).map(([k,v])=>`${h(k)}: ${v}`).join(' | ')+`\\n`;if(summary.ruleWarning)text+=`\u26a0\ufe0f Rules degraded: ${h(summary.ruleWarning)}\\n`;if(summary.ledgerWarningCount)text+=`\u26a0\ufe0f Ledger warnings: ${Number(summary.ledgerWarningCount)}\\n`;if(summary.warningCount)text+=`\u26a0\ufe0f Completed with warnings: ${Number(summary.warningCount)}\\n`;if(guard.expiredStaleRuns)text+=`\u26a0\ufe0f Auto-cleaned ${Number(guard.expiredStaleRuns)} stale run(s).\\n`;}\nif(Array.isArray(summary.warningDetails)&&summary.warningDetails.length)text+=`\\n<b>Warning digest</b>\\n`+summary.warningDetails.map((w,i)=>{const l=link(w.messageId);return`${i+1}. ${h(w.sender)}\\n${l?`<a href=\"${l}\">\"${h(w.subject)}\"</a>`:`\"${h(w.subject)}\"`}\\n${h(w.errorMessage)}`}).join('\\n\\n');\nif(Array.isArray(summary.failedDetails)&&summary.failedDetails.length)text+=`\\n\\n<b>Failure details</b>\\n`+summary.failedDetails.map((f,i)=>{const l=link(f.messageId);return`${i+1}. ${h(f.sender||'unknown')}\\n${l?`<a href=\"${l}\">\"${h(f.subject||'(no subject)')}\"</a>`:`\"${h(f.subject||'(no subject)')}\"`}\\n${h(f.failureStage||f.source||'failed')} \u2014 ${h(f.errorMessage||'No error message.')}`}).join('\\n\\n');\nif(Array.isArray(summary.reviewDetails)&&summary.reviewDetails.length)text+=`\\n\\n<b>Review digest</b>\\n`+summary.reviewDetails.map((r,i)=>{const l=link(r.messageId);return`${i+1}. ${h(r.sender)}\\n${l?`<a href=\"${l}\">\"${h(r.subject)}\"</a>`:`\"${h(r.subject)}\"`}\\n${h(r.source)} \u00b7 ${h(r.confidence)} \u00b7 ${h(r.hint)} \u2014 ${h(r.reasoning)}`}).join('\\n\\n');\nif(Array.isArray(summary.ruleSuggestions)&&summary.ruleSuggestions.length)text+=`\\n\\n<b>Rule suggestions this run</b>\\n`+summary.ruleSuggestions.map(s=>`\u2022 ${h(s.sender)} (${s.count}\u00d7 \u2192 /review ${h(s.sender)}, /receipt ${h(s.sender)}, or /whitelist ${h(s.sender)})`).join('\\n');const actions=[];if((summary.needsReview||0)>0)actions.push('\u2022 /recent needs-review \u2014 inspect uncertain emails');if((summary.failed||0)>0)actions.push('\u2022 /recent failed \u2014 inspect processing failures');if((summary.warningCount||0)>0)actions.push('\u2022 /recent warnings \u2014 inspect completed warnings');if(summary.batchSaturated)actions.push('\u2022 Increase GMAIL_BATCH_SIZE or run again');if((summary.completed||0)>0)actions.push('\u2022 /stats \u2014 find repeated review senders');if(actions.length)text+=`\\n\\n<b>Next actions</b>\\n`+actions.join('\\n');if(summary.errorMessage)text+=`\\n\\n<b>Note:</b> ${h(summary.errorMessage)}`;if(dbError)text+=`\\n\\n\u26a0\ufe0f Run finished logically, but DB final update failed.`;return[{json:{chatId:$env.TELEGRAM_CHAT_ID,text:clampTelegram(text)}}];"
      },
      "id": "6422c7c1-0229-4053-923d-fb6901d8e48e",
      "name": "Format Telegram Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3300,
        -40
      ]
    },
    {
      "parameters": {
        "operation": "sendMessage",
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.text }}",
        "additionalFields": {
          "parse_mode": "HTML",
          "disable_web_page_preview": true
        }
      },
      "id": "5fbdd418-1de1-451c-9989-27d2bcd13d11",
      "name": "Send Telegram Summary",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        3520,
        -40
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000
    },
    {
      "parameters": {
        "operation": "sendMessage",
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.text }}",
        "additionalFields": {
          "parse_mode": "HTML",
          "disable_web_page_preview": true
        }
      },
      "id": "68b58b3d-0b7d-4a8e-b466-a9d45b0e8720",
      "name": "Send Immediate Telegram Notice",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        880,
        160
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000
    }
  ],
  "connections": {
    "Daily Schedule 6AM Manila": {
      "main": [
        [
          {
            "node": "Build Context And Validate Env",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Context And Validate Env": {
      "main": [
        [
          {
            "node": "Env OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Env OK?": {
      "main": [
        [
          {
            "node": "Simple Run Guard Query",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Env Error Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Env Error Summary": {
      "main": [
        [
          {
            "node": "Send Immediate Telegram Notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simple Run Guard Query": {
      "main": [
        [
          {
            "node": "Evaluate Run Guard",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate Run Guard": {
      "main": [
        [
          {
            "node": "Active Run?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Active Run?": {
      "main": [
        [
          {
            "node": "Build Skipped Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Run Started",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Skipped Summary": {
      "main": [
        [
          {
            "node": "Send Immediate Telegram Notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Run Started": {
      "main": [
        [
          {
            "node": "Get Gmail Labels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Gmail Labels": {
      "main": [
        [
          {
            "node": "Get Sender Rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Sender Rules": {
      "main": [
        [
          {
            "node": "Fetch Inbox Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Inbox Emails": {
      "main": [
        [
          {
            "node": "Normalize Emails And Apply Rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Emails And Apply Rules": {
      "main": [
        [
          {
            "node": "No Work?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No Work?": {
      "main": [
        [
          {
            "node": "Build No Work Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Split Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build No Work Summary": {
      "main": [
        [
          {
            "node": "Mark Run Finished",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Emails": {
      "main": [
        [
          {
            "node": "Call Single Email Processor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Final Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Single Email Processor": {
      "main": [
        [
          {
            "node": "Normalize Child Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Child Result": {
      "main": [
        [
          {
            "node": "Used Gemini?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Used Gemini?": {
      "main": [
        [
          {
            "node": "Wait Between Emails",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Split Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait Between Emails": {
      "main": [
        [
          {
            "node": "Split Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Final Summary": {
      "main": [
        [
          {
            "node": "Mark Run Finished",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Run Finished": {
      "main": [
        [
          {
            "node": "Format Telegram Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Telegram Summary": {
      "main": [
        [
          {
            "node": "Send Telegram Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "timezone": "Asia/Manila",
    "saveExecutionProgress": true,
    "executionTimeout": 1800
  },
  "staticData": null,
  "tags": [],
  "triggerCount": 1,
  "versionId": "v4.0-lean-generated"
}