{"updatedAt":"2026-04-24T17:43:30.065Z","createdAt":"2026-04-24T17:43:30.065Z","id":"wPrSsTe17UEMfpPy","name":"Approvals — Request Handler (GAL-109)","description":null,"active":true,"isArchived":false,"nodes":[{"id":"webhook-trigger","name":"Webhook Trigger","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[240,300],"parameters":{"httpMethod":"POST","path":"approval-request","responseMode":"responseNode","options":{}},"webhookId":"bbb0bad8-f710-4aec-90c0-ff1e7eb51a1f"},{"id":"normalize","name":"Normalize Inputs","type":"n8n-nodes-base.set","typeVersion":3.4,"position":[440,300],"parameters":{"mode":"manual","duplicateItem":false,"assignments":{"assignments":[{"id":"8ae53f5b-ca99-4900-ad67-21f361698a63","name":"idempotency_key","value":"={{ $json.body.idempotency_key }}","type":"string"},{"id":"370bf71a-802b-46f8-9283-7ffc5a484d6d","name":"agent","value":"={{ $json.body.agent }}","type":"string"},{"id":"2a52b5fc-488b-4ec2-a197-ae6a18d5064d","name":"gate_id","value":"={{ $json.body.gate_id }}","type":"string"},{"id":"9eecf776-29ef-4c01-b7b4-f4e8295c026d","name":"description","value":"={{ $json.body.description }}","type":"string"},{"id":"b4a07b2a-fb57-4ccf-a140-88ec59a9aefa","name":"detail","value":"={{ $json.body.detail || '' }}","type":"string"},{"id":"b647e37f-ca92-47fe-9839-6ae4f5ba6958","name":"risk_level","value":"={{ $json.body.risk_level || 'medium' }}","type":"string"},{"id":"e8568afb-5be7-44fb-88f4-9a1714afcf2d","name":"session_id","value":"={{ $json.body.session_id || '' }}","type":"string"},{"id":"b38902d8-3d71-4e7d-a88e-8c35ee5ea626","name":"dry_run","value":"={{ $json.query && $json.query.dry_run === 'true' }}","type":"boolean"},{"id":"ae475c92-d1c5-4616-ad70-07ebbbb6afb2","name":"timeout_minutes","value":"={{ ({\"deploy_billing_auth_payments\": 10, \"db_migration\": 10, \"dns_change\": 10, \"rls_policy_change\": 10, \"free_tier_exceed\": 15, \"new_third_party_service\": 15, \"regulated_content\": 30, \"incident_disclosure\": 5, \"partner_commitment\": 30, \"destructive_op\": 5})[$json.body.gate_id] || 10 }}","type":"number"}]},"options":{}}},{"id":"validate","name":"Validate Payload","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[640,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"93388492-00fd-4470-8c11-74a6305a0980","leftValue":"={{ $json.idempotency_key }}","rightValue":"","operator":{"type":"string","operation":"notEmpty"}},{"id":"b2a6e562-4196-4247-af9b-639d78784626","leftValue":"={{ $json.agent }}","rightValue":"","operator":{"type":"string","operation":"notEmpty"}},{"id":"710b9a67-b2fa-43f0-9bc0-4e7fa617caea","leftValue":"={{ $json.gate_id }}","rightValue":"","operator":{"type":"string","operation":"notEmpty"}},{"id":"a33ff56d-1dc0-4927-a570-702948f7e1aa","leftValue":"={{ $json.description }}","rightValue":"","operator":{"type":"string","operation":"notEmpty"}}],"combinator":"and"}}},{"id":"respond-400","name":"Respond: 400 Bad Request","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[840,460],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({error: \"missing required field(s): idempotency_key, agent, gate_id, description\"}) }}","options":{"responseCode":400}}},{"id":"check-idempotency","name":"Check Idempotency","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[840,300],"parameters":{"method":"GET","url":"=https://jiidzeympaalzljyqvjq.supabase.co/rest/v1/approval_requests?idempotency_key=eq.{{ $json.idempotency_key }}&select=*","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"apikey","value":"={{ $env.SB_LSA_PRO_SR }}"},{"name":"Authorization","value":"=Bearer {{ $env.SB_LSA_PRO_SR }}"},{"name":"Accept-Profile","value":"ops"}]},"options":{"response":{"response":{"neverError":true,"responseFormat":"json"}}}},"alwaysOutputData":true},{"id":"wrap-idempotency","name":"Wrap Idempotency Result","type":"n8n-nodes-base.code","typeVersion":2,"position":[940,300],"parameters":{"mode":"runOnceForAllItems","language":"javaScript","jsCode":"// n8n's HTTP Request splits JSON arrays into items; each item's json is a single row.\n// With alwaysOutputData=true, a truly empty response still emits 1 item (possibly empty json).\n// Filter by idempotency_key presence to distinguish real rows from the phantom item.\nconst items = $input.all();\nconst rows = items.map(i => i.json).filter(r => r && typeof r === 'object' && r.idempotency_key);\nreturn [{ json: { rows, row_count: rows.length, first_row: rows[0] || null } }];\n"}},{"id":"route-existing","name":"Existing?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1040,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"id":"51e6f017-f042-4ef9-948f-da3c0c0416f4","leftValue":"={{ $json.row_count }}","rightValue":0,"operator":{"type":"number","operation":"gt"}}],"combinator":"and"}}},{"id":"route-pending-or-resolved","name":"Pending or Resolved?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1240,200],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"f36bf5cb-ca14-4211-b836-d52c0e24dc32","leftValue":"={{ $json.first_row.status }}","rightValue":"pending","operator":{"type":"string","operation":"equals"}}],"combinator":"and"}}},{"id":"respond-409","name":"Respond: 409 Pending Dup","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[1440,100],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({error: \"duplicate pending request\", idempotency_key: $json.first_row.idempotency_key, status: \"pending\"}) }}","options":{"responseCode":409}}},{"id":"respond-cached","name":"Respond: Cached Outcome","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[1440,300],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({gate_id: $json.first_row.gate_id, approved: $json.first_row.status === \"approved\", approver: $json.first_row.approver, timestamp: $json.first_row.resolved_at, status: $json.first_row.status, dry_run: $json.first_row.is_dry_run, cached: true}) }}","options":{"responseCode":200}}},{"id":"insert-row","name":"Insert Pending Row","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1240,500],"parameters":{"method":"POST","url":"=https://jiidzeympaalzljyqvjq.supabase.co/rest/v1/approval_requests","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"apikey","value":"={{ $env.SB_LSA_PRO_SR }}"},{"name":"Authorization","value":"=Bearer {{ $env.SB_LSA_PRO_SR }}"},{"name":"Content-Profile","value":"ops"},{"name":"Content-Type","value":"application/json"},{"name":"Prefer","value":"return=representation"}]},"sendBody":true,"bodyParameters":{"parameters":[{"name":"idempotency_key","value":"={{ $('Normalize Inputs').item.json.idempotency_key }}"},{"name":"agent","value":"={{ $('Normalize Inputs').item.json.agent }}"},{"name":"gate_id","value":"={{ $('Normalize Inputs').item.json.gate_id }}"},{"name":"description","value":"={{ $('Normalize Inputs').item.json.description }}"},{"name":"detail","value":"={{ $('Normalize Inputs').item.json.detail }}"},{"name":"risk_level","value":"={{ $('Normalize Inputs').item.json.risk_level }}"},{"name":"session_id","value":"={{ $('Normalize Inputs').item.json.session_id }}"},{"name":"is_dry_run","value":"={{ $('Normalize Inputs').item.json.dry_run }}"},{"name":"status","value":"pending"}]},"options":{}}},{"id":"send-telegram","name":"Send Telegram Approval","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1440,500],"parameters":{"method":"POST","url":"=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN_OPS }}/sendMessage","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({\n  chat_id: $env.TELEGRAM_CHAT_ID,\n  text: `\\ud83d\\udd12 Human Gate — ${$('Normalize Inputs').item.json.gate_id}\\n\\nAgent: ${$('Normalize Inputs').item.json.agent}\\nAction: ${$('Normalize Inputs').item.json.description}\\n\\n${$('Normalize Inputs').item.json.detail}\\n\\nRisk: ${$('Normalize Inputs').item.json.risk_level}\\n\\n\\u23f0 Auto-rejects in ${$('Normalize Inputs').item.json.timeout_minutes}m.${$('Normalize Inputs').item.json.dry_run ? '\\n\\n(DRY-RUN — not actually blocking)' : ''}`,\n  reply_markup: {\n    inline_keyboard: [[\n      {text: '\\u2705 Approve', callback_data: `approve:${$('Insert Pending Row').item.json.id}`},\n      {text: '\\u274c Reject', callback_data: `reject:${$('Insert Pending Row').item.json.id}`}\n    ]]\n  }\n}) }}","options":{}}},{"id":"is-dry-run","name":"Is Dry Run?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1640,500],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"6553778d-cd9b-486e-b6ec-9b8fa74dd438","leftValue":"={{ $('Normalize Inputs').item.json.dry_run }}","rightValue":true,"operator":{"type":"boolean","operation":"true"}}],"combinator":"and"}}},{"id":"respond-dry-run","name":"Respond: Dry Run","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[1840,400],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({gate_id: $('Normalize Inputs').item.json.gate_id, approved: false, status: \"pending\", dry_run: true, message: \"Dry run — not blocking\"}) }}","options":{"responseCode":200}}},{"id":"poll-wait","name":"Poll Wait 3s","type":"n8n-nodes-base.wait","typeVersion":1.1,"position":[1840,600],"parameters":{"amount":3,"unit":"seconds"},"webhookId":"bcf9c687-135c-49f7-8406-605e102699c4"},{"id":"check-status","name":"Check Status","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2040,600],"parameters":{"method":"GET","url":"=https://jiidzeympaalzljyqvjq.supabase.co/rest/v1/approval_requests?idempotency_key=eq.{{ $('Normalize Inputs').item.json.idempotency_key }}&select=*","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"apikey","value":"={{ $env.SB_LSA_PRO_SR }}"},{"name":"Authorization","value":"=Bearer {{ $env.SB_LSA_PRO_SR }}"},{"name":"Accept-Profile","value":"ops"}]},"options":{"response":{"response":{"neverError":true,"responseFormat":"json"}}}}},{"id":"is-resolved","name":"Is Resolved?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[2240,600],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"5b66f49e-b067-42ad-a117-7dbc3e6dd6d0","leftValue":"={{ $json.status }}","rightValue":"pending","operator":{"type":"string","operation":"notEquals"}}],"combinator":"and"}}},{"id":"check-timeout","name":"Timeout Reached?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[2240,800],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"id":"31825745-63a9-4ce4-919d-d48b5afb6c8c","leftValue":"={{ (new Date() - new Date($('Check Status').item.json.requested_at)) / 60000 }}","rightValue":"={{ $('Normalize Inputs').item.json.timeout_minutes }}","operator":{"type":"number","operation":"gte"}}],"combinator":"and"}}},{"id":"update-timeout","name":"Update: Timeout","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2440,900],"parameters":{"method":"PATCH","url":"=https://jiidzeympaalzljyqvjq.supabase.co/rest/v1/approval_requests?idempotency_key=eq.{{ $('Normalize Inputs').item.json.idempotency_key }}&status=eq.pending","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"apikey","value":"={{ $env.SB_LSA_PRO_SR }}"},{"name":"Authorization","value":"=Bearer {{ $env.SB_LSA_PRO_SR }}"},{"name":"Content-Profile","value":"ops"},{"name":"Content-Type","value":"application/json"},{"name":"Prefer","value":"return=representation"}]},"sendBody":true,"bodyParameters":{"parameters":[{"name":"status","value":"timeout"},{"name":"resolved_at","value":"={{ new Date().toISOString() }}"}]},"options":{}}},{"id":"send-timeout-msg","name":"Send Timeout Notice","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2640,900],"parameters":{"method":"POST","url":"=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN_OPS }}/sendMessage","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $env.TELEGRAM_CHAT_ID, text: `\\u23f0 Auto-rejected: ${$('Normalize Inputs').item.json.description} (no response in ${$('Normalize Inputs').item.json.timeout_minutes}m)` + ($('Normalize Inputs').item.json.gate_id === 'incident_disclosure' ? '\\n\\n\\ud83d\\udea8 INCIDENT DISCLOSURE — immediate visibility required' : '')}) }}","options":{}}},{"id":"respond-timeout","name":"Respond: Timeout","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[2840,900],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({gate_id: $('Normalize Inputs').item.json.gate_id, approved: false, status: \"timeout\", dry_run: false, message: \"Auto-rejected on timeout\"}) }}","options":{"responseCode":200}}},{"id":"respond-resolved","name":"Respond: Resolved","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[2440,500],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({gate_id: $('Check Status').item.json.gate_id, approved: $('Check Status').item.json.status === \"approved\", approver: $('Check Status').item.json.approver, timestamp: $('Check Status').item.json.resolved_at, status: $('Check Status').item.json.status, dry_run: $('Check Status').item.json.is_dry_run}) }}","options":{"responseCode":200}}}],"connections":{"Webhook Trigger":{"main":[[{"node":"Normalize Inputs","type":"main","index":0}]]},"Normalize Inputs":{"main":[[{"node":"Validate Payload","type":"main","index":0}]]},"Validate Payload":{"main":[[{"node":"Check Idempotency","type":"main","index":0}],[{"node":"Respond: 400 Bad Request","type":"main","index":0}]]},"Check Idempotency":{"main":[[{"node":"Wrap Idempotency Result","type":"main","index":0}]]},"Wrap Idempotency Result":{"main":[[{"node":"Existing?","type":"main","index":0}]]},"Existing?":{"main":[[{"node":"Pending or Resolved?","type":"main","index":0}],[{"node":"Insert Pending Row","type":"main","index":0}]]},"Pending or Resolved?":{"main":[[{"node":"Respond: 409 Pending Dup","type":"main","index":0}],[{"node":"Respond: Cached Outcome","type":"main","index":0}]]},"Insert Pending Row":{"main":[[{"node":"Send Telegram Approval","type":"main","index":0}]]},"Send Telegram Approval":{"main":[[{"node":"Is Dry Run?","type":"main","index":0}]]},"Is Dry Run?":{"main":[[{"node":"Respond: Dry Run","type":"main","index":0}],[{"node":"Poll Wait 3s","type":"main","index":0}]]},"Poll Wait 3s":{"main":[[{"node":"Check Status","type":"main","index":0}]]},"Check Status":{"main":[[{"node":"Is Resolved?","type":"main","index":0}]]},"Is Resolved?":{"main":[[{"node":"Respond: Resolved","type":"main","index":0}],[{"node":"Timeout Reached?","type":"main","index":0}]]},"Timeout Reached?":{"main":[[{"node":"Update: Timeout","type":"main","index":0}],[{"node":"Poll Wait 3s","type":"main","index":0}]]},"Update: Timeout":{"main":[[{"node":"Send Timeout Notice","type":"main","index":0}]]},"Send Timeout Notice":{"main":[[{"node":"Respond: Timeout","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveExecutionProgress":true,"saveManualExecutions":true,"callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"a2c4e521-29c1-48a3-be51-75b6383600de","activeVersionId":"a2c4e521-29c1-48a3-be51-75b6383600de","versionCounter":3,"triggerCount":1,"shared":[{"updatedAt":"2026-04-24T17:43:30.066Z","createdAt":"2026-04-24T17:43:30.066Z","role":"workflow:owner","workflowId":"wPrSsTe17UEMfpPy","projectId":"WMTxW1hBsgAEIsg6","project":{"updatedAt":"2026-02-16T08:56:00.695Z","createdAt":"2026-02-16T07:50:49.259Z","id":"WMTxW1hBsgAEIsg6","name":"Joao Galhardo <jgalhardo.pt@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"0487392a-ff85-4881-beb2-1ef86afbe010"}}],"tags":[],"activeVersion":{"updatedAt":"2026-04-24T17:43:30.068Z","createdAt":"2026-04-24T17:43:30.068Z","versionId":"a2c4e521-29c1-48a3-be51-75b6383600de","workflowId":"wPrSsTe17UEMfpPy","nodes":[{"id":"webhook-trigger","name":"Webhook Trigger","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[240,300],"parameters":{"httpMethod":"POST","path":"approval-request","responseMode":"responseNode","options":{}},"webhookId":"bbb0bad8-f710-4aec-90c0-ff1e7eb51a1f"},{"id":"normalize","name":"Normalize Inputs","type":"n8n-nodes-base.set","typeVersion":3.4,"position":[440,300],"parameters":{"mode":"manual","duplicateItem":false,"assignments":{"assignments":[{"id":"8ae53f5b-ca99-4900-ad67-21f361698a63","name":"idempotency_key","value":"={{ $json.body.idempotency_key }}","type":"string"},{"id":"370bf71a-802b-46f8-9283-7ffc5a484d6d","name":"agent","value":"={{ $json.body.agent }}","type":"string"},{"id":"2a52b5fc-488b-4ec2-a197-ae6a18d5064d","name":"gate_id","value":"={{ $json.body.gate_id }}","type":"string"},{"id":"9eecf776-29ef-4c01-b7b4-f4e8295c026d","name":"description","value":"={{ $json.body.description }}","type":"string"},{"id":"b4a07b2a-fb57-4ccf-a140-88ec59a9aefa","name":"detail","value":"={{ $json.body.detail || '' }}","type":"string"},{"id":"b647e37f-ca92-47fe-9839-6ae4f5ba6958","name":"risk_level","value":"={{ $json.body.risk_level || 'medium' }}","type":"string"},{"id":"e8568afb-5be7-44fb-88f4-9a1714afcf2d","name":"session_id","value":"={{ $json.body.session_id || '' }}","type":"string"},{"id":"b38902d8-3d71-4e7d-a88e-8c35ee5ea626","name":"dry_run","value":"={{ $json.query && $json.query.dry_run === 'true' }}","type":"boolean"},{"id":"ae475c92-d1c5-4616-ad70-07ebbbb6afb2","name":"timeout_minutes","value":"={{ ({\"deploy_billing_auth_payments\": 10, \"db_migration\": 10, \"dns_change\": 10, \"rls_policy_change\": 10, \"free_tier_exceed\": 15, \"new_third_party_service\": 15, \"regulated_content\": 30, \"incident_disclosure\": 5, \"partner_commitment\": 30, \"destructive_op\": 5})[$json.body.gate_id] || 10 }}","type":"number"}]},"options":{}}},{"id":"validate","name":"Validate Payload","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[640,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"93388492-00fd-4470-8c11-74a6305a0980","leftValue":"={{ $json.idempotency_key }}","rightValue":"","operator":{"type":"string","operation":"notEmpty"}},{"id":"b2a6e562-4196-4247-af9b-639d78784626","leftValue":"={{ $json.agent }}","rightValue":"","operator":{"type":"string","operation":"notEmpty"}},{"id":"710b9a67-b2fa-43f0-9bc0-4e7fa617caea","leftValue":"={{ $json.gate_id }}","rightValue":"","operator":{"type":"string","operation":"notEmpty"}},{"id":"a33ff56d-1dc0-4927-a570-702948f7e1aa","leftValue":"={{ $json.description }}","rightValue":"","operator":{"type":"string","operation":"notEmpty"}}],"combinator":"and"}}},{"id":"respond-400","name":"Respond: 400 Bad Request","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[840,460],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({error: \"missing required field(s): idempotency_key, agent, gate_id, description\"}) }}","options":{"responseCode":400}}},{"id":"check-idempotency","name":"Check Idempotency","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[840,300],"parameters":{"method":"GET","url":"=https://jiidzeympaalzljyqvjq.supabase.co/rest/v1/approval_requests?idempotency_key=eq.{{ $json.idempotency_key }}&select=*","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"apikey","value":"={{ $env.SB_LSA_PRO_SR }}"},{"name":"Authorization","value":"=Bearer {{ $env.SB_LSA_PRO_SR }}"},{"name":"Accept-Profile","value":"ops"}]},"options":{"response":{"response":{"neverError":true,"responseFormat":"json"}}}},"alwaysOutputData":true},{"id":"wrap-idempotency","name":"Wrap Idempotency Result","type":"n8n-nodes-base.code","typeVersion":2,"position":[940,300],"parameters":{"mode":"runOnceForAllItems","language":"javaScript","jsCode":"// n8n's HTTP Request splits JSON arrays into items; each item's json is a single row.\n// With alwaysOutputData=true, a truly empty response still emits 1 item (possibly empty json).\n// Filter by idempotency_key presence to distinguish real rows from the phantom item.\nconst items = $input.all();\nconst rows = items.map(i => i.json).filter(r => r && typeof r === 'object' && r.idempotency_key);\nreturn [{ json: { rows, row_count: rows.length, first_row: rows[0] || null } }];\n"}},{"id":"route-existing","name":"Existing?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1040,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"id":"51e6f017-f042-4ef9-948f-da3c0c0416f4","leftValue":"={{ $json.row_count }}","rightValue":0,"operator":{"type":"number","operation":"gt"}}],"combinator":"and"}}},{"id":"route-pending-or-resolved","name":"Pending or Resolved?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1240,200],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"f36bf5cb-ca14-4211-b836-d52c0e24dc32","leftValue":"={{ $json.first_row.status }}","rightValue":"pending","operator":{"type":"string","operation":"equals"}}],"combinator":"and"}}},{"id":"respond-409","name":"Respond: 409 Pending Dup","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[1440,100],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({error: \"duplicate pending request\", idempotency_key: $json.first_row.idempotency_key, status: \"pending\"}) }}","options":{"responseCode":409}}},{"id":"respond-cached","name":"Respond: Cached Outcome","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[1440,300],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({gate_id: $json.first_row.gate_id, approved: $json.first_row.status === \"approved\", approver: $json.first_row.approver, timestamp: $json.first_row.resolved_at, status: $json.first_row.status, dry_run: $json.first_row.is_dry_run, cached: true}) }}","options":{"responseCode":200}}},{"id":"insert-row","name":"Insert Pending Row","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1240,500],"parameters":{"method":"POST","url":"=https://jiidzeympaalzljyqvjq.supabase.co/rest/v1/approval_requests","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"apikey","value":"={{ $env.SB_LSA_PRO_SR }}"},{"name":"Authorization","value":"=Bearer {{ $env.SB_LSA_PRO_SR }}"},{"name":"Content-Profile","value":"ops"},{"name":"Content-Type","value":"application/json"},{"name":"Prefer","value":"return=representation"}]},"sendBody":true,"bodyParameters":{"parameters":[{"name":"idempotency_key","value":"={{ $('Normalize Inputs').item.json.idempotency_key }}"},{"name":"agent","value":"={{ $('Normalize Inputs').item.json.agent }}"},{"name":"gate_id","value":"={{ $('Normalize Inputs').item.json.gate_id }}"},{"name":"description","value":"={{ $('Normalize Inputs').item.json.description }}"},{"name":"detail","value":"={{ $('Normalize Inputs').item.json.detail }}"},{"name":"risk_level","value":"={{ $('Normalize Inputs').item.json.risk_level }}"},{"name":"session_id","value":"={{ $('Normalize Inputs').item.json.session_id }}"},{"name":"is_dry_run","value":"={{ $('Normalize Inputs').item.json.dry_run }}"},{"name":"status","value":"pending"}]},"options":{}}},{"id":"send-telegram","name":"Send Telegram Approval","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1440,500],"parameters":{"method":"POST","url":"=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN_OPS }}/sendMessage","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({\n  chat_id: $env.TELEGRAM_CHAT_ID,\n  text: `\\ud83d\\udd12 Human Gate — ${$('Normalize Inputs').item.json.gate_id}\\n\\nAgent: ${$('Normalize Inputs').item.json.agent}\\nAction: ${$('Normalize Inputs').item.json.description}\\n\\n${$('Normalize Inputs').item.json.detail}\\n\\nRisk: ${$('Normalize Inputs').item.json.risk_level}\\n\\n\\u23f0 Auto-rejects in ${$('Normalize Inputs').item.json.timeout_minutes}m.${$('Normalize Inputs').item.json.dry_run ? '\\n\\n(DRY-RUN — not actually blocking)' : ''}`,\n  reply_markup: {\n    inline_keyboard: [[\n      {text: '\\u2705 Approve', callback_data: `approve:${$('Insert Pending Row').item.json.id}`},\n      {text: '\\u274c Reject', callback_data: `reject:${$('Insert Pending Row').item.json.id}`}\n    ]]\n  }\n}) }}","options":{}}},{"id":"is-dry-run","name":"Is Dry Run?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1640,500],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"6553778d-cd9b-486e-b6ec-9b8fa74dd438","leftValue":"={{ $('Normalize Inputs').item.json.dry_run }}","rightValue":true,"operator":{"type":"boolean","operation":"true"}}],"combinator":"and"}}},{"id":"respond-dry-run","name":"Respond: Dry Run","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[1840,400],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({gate_id: $('Normalize Inputs').item.json.gate_id, approved: false, status: \"pending\", dry_run: true, message: \"Dry run — not blocking\"}) }}","options":{"responseCode":200}}},{"id":"poll-wait","name":"Poll Wait 3s","type":"n8n-nodes-base.wait","typeVersion":1.1,"position":[1840,600],"parameters":{"amount":3,"unit":"seconds"},"webhookId":"bcf9c687-135c-49f7-8406-605e102699c4"},{"id":"check-status","name":"Check Status","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2040,600],"parameters":{"method":"GET","url":"=https://jiidzeympaalzljyqvjq.supabase.co/rest/v1/approval_requests?idempotency_key=eq.{{ $('Normalize Inputs').item.json.idempotency_key }}&select=*","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"apikey","value":"={{ $env.SB_LSA_PRO_SR }}"},{"name":"Authorization","value":"=Bearer {{ $env.SB_LSA_PRO_SR }}"},{"name":"Accept-Profile","value":"ops"}]},"options":{"response":{"response":{"neverError":true,"responseFormat":"json"}}}}},{"id":"is-resolved","name":"Is Resolved?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[2240,600],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"5b66f49e-b067-42ad-a117-7dbc3e6dd6d0","leftValue":"={{ $json.status }}","rightValue":"pending","operator":{"type":"string","operation":"notEquals"}}],"combinator":"and"}}},{"id":"check-timeout","name":"Timeout Reached?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[2240,800],"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"id":"31825745-63a9-4ce4-919d-d48b5afb6c8c","leftValue":"={{ (new Date() - new Date($('Check Status').item.json.requested_at)) / 60000 }}","rightValue":"={{ $('Normalize Inputs').item.json.timeout_minutes }}","operator":{"type":"number","operation":"gte"}}],"combinator":"and"}}},{"id":"update-timeout","name":"Update: Timeout","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2440,900],"parameters":{"method":"PATCH","url":"=https://jiidzeympaalzljyqvjq.supabase.co/rest/v1/approval_requests?idempotency_key=eq.{{ $('Normalize Inputs').item.json.idempotency_key }}&status=eq.pending","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"apikey","value":"={{ $env.SB_LSA_PRO_SR }}"},{"name":"Authorization","value":"=Bearer {{ $env.SB_LSA_PRO_SR }}"},{"name":"Content-Profile","value":"ops"},{"name":"Content-Type","value":"application/json"},{"name":"Prefer","value":"return=representation"}]},"sendBody":true,"bodyParameters":{"parameters":[{"name":"status","value":"timeout"},{"name":"resolved_at","value":"={{ new Date().toISOString() }}"}]},"options":{}}},{"id":"send-timeout-msg","name":"Send Timeout Notice","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2640,900],"parameters":{"method":"POST","url":"=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN_OPS }}/sendMessage","authentication":"none","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({chat_id: $env.TELEGRAM_CHAT_ID, text: `\\u23f0 Auto-rejected: ${$('Normalize Inputs').item.json.description} (no response in ${$('Normalize Inputs').item.json.timeout_minutes}m)` + ($('Normalize Inputs').item.json.gate_id === 'incident_disclosure' ? '\\n\\n\\ud83d\\udea8 INCIDENT DISCLOSURE — immediate visibility required' : '')}) }}","options":{}}},{"id":"respond-timeout","name":"Respond: Timeout","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[2840,900],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({gate_id: $('Normalize Inputs').item.json.gate_id, approved: false, status: \"timeout\", dry_run: false, message: \"Auto-rejected on timeout\"}) }}","options":{"responseCode":200}}},{"id":"respond-resolved","name":"Respond: Resolved","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[2440,500],"parameters":{"respondWith":"json","responseBody":"={{ JSON.stringify({gate_id: $('Check Status').item.json.gate_id, approved: $('Check Status').item.json.status === \"approved\", approver: $('Check Status').item.json.approver, timestamp: $('Check Status').item.json.resolved_at, status: $('Check Status').item.json.status, dry_run: $('Check Status').item.json.is_dry_run}) }}","options":{"responseCode":200}}}],"connections":{"Webhook Trigger":{"main":[[{"node":"Normalize Inputs","type":"main","index":0}]]},"Normalize Inputs":{"main":[[{"node":"Validate Payload","type":"main","index":0}]]},"Validate Payload":{"main":[[{"node":"Check Idempotency","type":"main","index":0}],[{"node":"Respond: 400 Bad Request","type":"main","index":0}]]},"Check Idempotency":{"main":[[{"node":"Wrap Idempotency Result","type":"main","index":0}]]},"Wrap Idempotency Result":{"main":[[{"node":"Existing?","type":"main","index":0}]]},"Existing?":{"main":[[{"node":"Pending or Resolved?","type":"main","index":0}],[{"node":"Insert Pending Row","type":"main","index":0}]]},"Pending or Resolved?":{"main":[[{"node":"Respond: 409 Pending Dup","type":"main","index":0}],[{"node":"Respond: Cached Outcome","type":"main","index":0}]]},"Insert Pending Row":{"main":[[{"node":"Send Telegram Approval","type":"main","index":0}]]},"Send Telegram Approval":{"main":[[{"node":"Is Dry Run?","type":"main","index":0}]]},"Is Dry Run?":{"main":[[{"node":"Respond: Dry Run","type":"main","index":0}],[{"node":"Poll Wait 3s","type":"main","index":0}]]},"Poll Wait 3s":{"main":[[{"node":"Check Status","type":"main","index":0}]]},"Check Status":{"main":[[{"node":"Is Resolved?","type":"main","index":0}]]},"Is Resolved?":{"main":[[{"node":"Respond: Resolved","type":"main","index":0}],[{"node":"Timeout Reached?","type":"main","index":0}]]},"Timeout Reached?":{"main":[[{"node":"Update: Timeout","type":"main","index":0}],[{"node":"Poll Wait 3s","type":"main","index":0}]]},"Update: Timeout":{"main":[[{"node":"Send Timeout Notice","type":"main","index":0}]]},"Send Timeout Notice":{"main":[[{"node":"Respond: Timeout","type":"main","index":0}]]}},"authors":"Joao Galhardo","name":null,"description":null,"autosaved":false,"workflowPublishHistory":[{"createdAt":"2026-04-24T17:43:30.186Z","id":104,"workflowId":"wPrSsTe17UEMfpPy","versionId":"a2c4e521-29c1-48a3-be51-75b6383600de","event":"activated","userId":"0487392a-ff85-4881-beb2-1ef86afbe010"}]}}