{
  "name": "joinIssueCollector",
  "nodes": [
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.emailReadImap",
      "typeVersion": 2.1,
      "position": [
        -2416,
        368
      ],
      "id": "ecead752-afc0-4401-9436-448de8db7f74",
      "name": "joinemailrequest@gmail.com (IMAP)",
      "credentials": {
        "imap": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "=https://join-278b5-default-rtdb.europe-west1.firebasedatabase.app/requestLimits/global/{{ $json.dayKey }}.json",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -2000,
        368
      ],
      "id": "383b89ec-7987-46d8-a5f5-6d2f10611945",
      "name": "HTTP Request getDayKey"
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://join-278b5-default-rtdb.europe-west1.firebasedatabase.app/requestLimits/global/{{ $('CalculateDateKey').item.json.dayKey }}.json",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"count\": {{ $('InitializeEntry').item.json.count + 1 }},\n  \"limit\": {{ $('InitializeEntry').item.json.limit }},\n  \"dayKey\": \"{{ $('InitializeEntry').item.json.dayKey }}\",\n  \"updatedAt\": \"{{ new Date().toISOString() }}\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -1168,
        352
      ],
      "id": "587e8cee-864b-4db7-b150-f75adba0e24d",
      "name": "HTTP Request setLimit"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are a task extraction agent for incoming emails in a Kanban system.\n\n## YOUR ONLY JOB\nAnalyze the email below and return a single valid JSON object. Nothing else.\nNo explanations. No markdown. No wrapper keys. Just the raw JSON.\n\n## CRITICAL OUTPUT RULE\nYour response must start with { and end with }\nNEVER wrap the result in \"output\", \"result\", \"data\" or any other key.\nNEVER add text before or after the JSON.\n\n## TODAY'S DATE\n{{ $json.dayKey }}\nAll dates must be in the future. Format: dd/mm/yyyy\n\n## DECISION: IS THIS A TASK?\nSet \"isTask\": true if the email contains a clear work request or actionable item.\nSet \"isTask\": false for: spam, newsletters, ads, private messages, unclear content.\n\nIf \"isTask\" is false \u2192 leave all other fields empty.\n\n## JSON SCHEMA\n{\n  \"isTask\": false,\n  \"title\": \"\",\n  \"date\": \"\",\n  \"category\": \"\",\n  \"description\": \"\",\n  \"priority\": \"\",\n  \"subtasks\": [],\n  \"createdByName\": \"\",\n  \"createdBySource\": \"extern\"\n}\n\n## FIELD RULES\n- title: short and precise, derived from email content (required if isTask=true)\n- date: dd/mm/yyyy, must be in the future. If none given, set a reasonable future date (required if isTask=true)\n- category: ONLY \"technical\" or \"user-story\" (required if isTask=true)\n- priority: ONLY \"urgent\", \"medium\" or \"low\" \u2014 never anything else\n- description: brief summary of the task, \"\" if unclear\n- subtasks: array of strings, [] if none\n- createdByName: sender email address if recognizable, otherwise \"\"\n- createdBySource: always \"extern\"\n\n## INPUT\nSender: {{ $('joinemailrequest@gmail.com (IMAP)').item.json.from }}\nEmail: {{ $('joinemailrequest@gmail.com (IMAP)').item.json.textPlain }}\n\n## REMINDER\nRespond with ONLY the JSON object. First character: { \u2014 Last character: }",
        "hasOutputParser": true,
        "needsFallback": true,
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        -976,
        352
      ],
      "id": "d9076496-9d3b-44c7-ae43-3e2acda2368b",
      "name": "AI Agent",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "067b91d1-379b-44e7-8bb5-1c23c6a4532d",
              "leftValue": "={{ !!$json.output.title && !!$json.output.date && !!$json.output.category && $json.output.isTask }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -576,
        336
      ],
      "id": "420df979-10a7-4526-bc54-82f82b5f1f9e",
      "name": "If RequiredFieldsPresent"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "=/**\n * Builds a normalized task payload for Firebase persistence.\n *\n * The payload:\n * - generates a unique string ID from the current timestamp,\n * - maps required fields from `$json.task`,\n * - sanitizes `priority` and `category` against allowed values,\n * - normalizes `subtasks` into `{ text, completed }` objects,\n * - sets default workflow metadata (`status`, `createdBySource`).\n *\n * Intended for n8n expression mode in an Edit Fields / Set node.\n *\n * @returns \n *   id: string,\n *   title: string,\n *   description: string,\n *   date: string,\n *   priority: string,\n *   category: \"technical-task\" | \"user-story\",\n *   assigned: unknown[],\n *   subtasks: { text: string, completed: boolean }[],\n *   status: \"triage\",\n *   createdByName: string,\n *   createdBySource: \"extern\"\n * }} Normalized task object ready to be stored.\n */\n{{ {\n  id: String(Date.now()),\n  title: $json.output.title,\n  description: $json.output.description || \"\",\n  date: $json.output.date || \"\",\n  priority: [\"urgent\", \"medium\", \"low\"].includes($json.output.priority) ? $json.output.priority : \"medium\",\n  category: [\"technical-task\", \"user-story\"].includes($json.output.category) ? $json.output.category : \"user-story\",\n  assigned: [],\n  subtasks: Array.isArray($json.output.subtasks)\n    ? $json.output.subtasks.map(s => ({ text: s.title || String(s), completed: s.done ?? false }))\n    : [],\n  status: \"triage\",\n  createdByName: $json.output.createdByName || \"\",\n  createdBySource: $json.output.createdBySource || \"extern\"\n} }}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -368,
        320
      ],
      "id": "994eaa57-93e1-4eea-ab2d-fb99b7672890",
      "name": "BuildTaskPayload"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://join-278b5-default-rtdb.europe-west1.firebasedatabase.app/tasks.json",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -160,
        320
      ],
      "id": "70f9f3a9-acd7-40fe-aa72-480375222b49",
      "name": "HTTP Request CreateTaskInFirebase"
    },
    {
      "parameters": {
        "jsCode": "/**\n * Generates a day key in `YYYY-MM-DD` format using the `Europe/Berlin` time zone.\n *\n * This ensures daily logic (for example request limits) is based on Berlin time\n * instead of the server's local time zone.\n *\n * @returns {{ json: { dayKey: string } }[]} An n8n item array containing the `dayKey`.\n */\nconst now = new Date();\nconst key = new Intl.DateTimeFormat(\"de-DE\", {\n  timeZone: \"Europe/Berlin\",\n  year: \"numeric\", month: \"2-digit\", day: \"2-digit\"\n}).format(now).split(\".\").reverse().join(\"-\");\n\nreturn [{ json: { dayKey: key } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2208,
        368
      ],
      "id": "eaf913ba-3629-4797-8978-0419e5d07db5",
      "name": "CalculateDateKey"
    },
    {
      "parameters": {
        "jsCode": "/**\n * Normalizes the daily limit record for the current `dayKey`.\n *\n * If no record exists in the input, it returns a default initialization object\n * with `count = 0`, `limit = 10`, and `needsInit = true`.\n * If a record exists, it preserves the current count, applies a fallback limit of `10`,\n * and marks `needsInit = false`.\n *\n * @returns {{ json: { count: number, limit: number, dayKey: string, needsInit: boolean } }[]}\n * An n8n item array containing the normalized daily limit state.\n */\nconst dayKey = $('CalculateDateKey').first().json.dayKey;\nconst data = $input.item.json;\n\nif (!data || data.count === undefined) {\n  return [{ json: { count: 0, limit: 10, dayKey, needsInit: true } }];\n}\n\nreturn [{ json: { count: data.count, limit: data.limit ?? 10, dayKey, needsInit: false } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1792,
        368
      ],
      "id": "50031d4d-bf0a-4abf-9e85-16b4cb02583d",
      "name": "InitializeEntry"
    },
    {
      "parameters": {
        "operation": "getAll",
        "limit": 1,
        "filters": {
          "q": "={{ 'from:' + (($('joinemailrequest@gmail.com (IMAP)').item.json.from || '').match(/<([^>]+)>/)?.[1] || '') + ' subject:\"' + (($('joinemailrequest@gmail.com (IMAP)').item.json.subject || '').replace(/\"/g, '')) + '\"' }}"
        }
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -1584,
        368
      ],
      "id": "da52e2dc-e2fb-469f-a395-3079a3ab6a85",
      "name": "FilterCurrentMessage",
      "alwaysOutputData": true,
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "reply",
        "messageId": "={{ $('FilterCurrentMessage').first().json.id }}",
        "emailType": "text",
        "message": "=Hello, \n\nthank you for reaching out. \nUnfortunately, we are currently experiencing technical difficulties and were unable to process your request automatically. \n\nOur team has received the message and will address it immediately.We apologize for the inconvenience.  \n\nBest regards, \nJoin Team",
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -576,
        528
      ],
      "id": "5240334c-357b-43d6-b74a-5998cb5598a2",
      "name": "Send ErrorMessage",
      "executeOnce": true,
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "reply",
        "messageId": "={{ $('FilterCurrentMessage').first().json.id }}",
        "emailType": "text",
        "message": "=Hello,  \n\nthank you for reaching out to us. \nYour request has been successfully received and added to our Kanbanboard. Our team will review and process it as soon as possible.  \n\nHere is a summary of your submission:  \nTitle: {{ $('BuildTaskPayload').item.json.title }} \nCategory: {{ $('BuildTaskPayload').item.json.category }} \nPriority: {{ $('BuildTaskPayload').item.json.priority || 'Not specified' }} \nDue Date: {{ $('BuildTaskPayload').item.json.date }}  \n\nIf you have any questions, simply reply to this email.  \n\nBest regards, \nJoin Team",
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        48,
        320
      ],
      "id": "4622992f-5310-4b87-aa13-f0cb7cc9ee13",
      "name": "Send SuccessMessage",
      "executeOnce": true,
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "addLabels",
        "messageId": "={{ $('FilterCurrentMessage').first().json.id }}",
        "labelIds": [
          "Label_5076039637280646665"
        ]
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -960,
        704
      ],
      "id": "3c92afee-4e1d-43a2-9d3b-d7a62c6b1fef",
      "name": "MoveToInProgress (Limit)",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "removeLabels",
        "messageId": "={{ $('FilterCurrentMessage').first().json.id }}",
        "labelIds": [
          "INBOX"
        ]
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -752,
        704
      ],
      "id": "2fe307d1-6510-4815-919a-3ef1bf67b4a3",
      "name": "RemoveInboxLabel (Limit)",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "addLabels",
        "messageId": "={{ $('FilterCurrentMessage').first().json.id }}",
        "labelIds": [
          "Label_2"
        ]
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        256,
        320
      ],
      "id": "2a6e3a03-e1b4-4a4c-9f4c-587a5cb7790d",
      "name": "MoveToDone (Success)",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "removeLabels",
        "messageId": "={{ $('FilterCurrentMessage').first().json.id }}",
        "labelIds": [
          "INBOX"
        ]
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        464,
        320
      ],
      "id": "b0826b23-d24d-4b71-8167-8968800c9b9b",
      "name": "RemoveInboxLabel (Success)",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "reply",
        "messageId": "={{ $('FilterCurrentMessage').first().json.id }}",
        "emailType": "text",
        "message": "=Hello,\n\nthank you for reaching out.\nWe have reached our daily request limit for today, so we are currently unable to process your request automatically. However, our team has received your message and will handle it as soon as possible.\n\nWe apologize for the inconvenience.\n\nBest regards,\nJoin Team",
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -1168,
        704
      ],
      "id": "70729656-c4d6-48e7-ae4c-118cc133f99c",
      "name": "Send LimitMessage",
      "executeOnce": true,
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "addLabels",
        "messageId": "={{ $('FilterCurrentMessage').first().json.id }}",
        "labelIds": [
          "Label_5076039637280646665"
        ]
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -368,
        528
      ],
      "id": "0109ba75-70d7-42d8-934f-9ab4226ee1b9",
      "name": "MoveToInProgress (Error)",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "removeLabels",
        "messageId": "={{ $('FilterCurrentMessage').first().json.id }}",
        "labelIds": [
          "INBOX"
        ]
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -160,
        528
      ],
      "id": "f1fc352c-430a-4a12-92c2-10d12a05ee65",
      "name": "RemoveInboxLabel (Error)",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "extern-task-status-changed",
        "responseMode": "lastNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -2416,
        592
      ],
      "id": "96210c38-0560-472e-8f10-0fb6430cfbda",
      "name": "Webhook"
    },
    {
      "parameters": {
        "sendTo": "={{ $json.body.recipientEmail }}",
        "subject": "=Update on your request: {{ $json.body.title }}",
        "emailType": "text",
        "message": "=Hello,\n\nyour request \"{{$json.body.title}}\" has been moved from \"{{$json.body.fromStatus}}\" to \"{{$json.body.toStatus}}\".\n\nOur team will process it as soon as possible.\n\nBest regards,\nJoin Team",
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -2208,
        592
      ],
      "id": "0b2fdcf4-d0ca-4d40-8875-2bcf692df487",
      "name": "Send MoveMessage",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "cc246ae8-45ca-492a-a88f-056dcb772103",
              "leftValue": "={{ $('InitializeEntry').item.json.count }}",
              "rightValue": "={{ $('InitializeEntry').item.json.limit }}",
              "operator": {
                "type": "number",
                "operation": "lt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -1376,
        368
      ],
      "id": "d29b2b37-e06c-4b54-b898-d6ec51d0655e",
      "name": "If dailyLimitReached"
    },
    {
      "parameters": {
        "jsonSchemaExample": "{\n  \"isTask\": false,\n  \"title\": \"Beispiel Aufgabe\",\n  \"date\": \"2024-01-15\",\n  \"category\": \"Allgemein\",\n  \"description\": \"Kurze Beschreibung der Aufgabe\",\n  \"priority\": \"medium\",\n  \"subtasks\": [\n    {\n      \"title\": \"Teilaufgabe 1\",\n      \"done\": false\n    }\n  ],\n  \"createdByName\": \"Max Mustermann\",\n  \"createdBySource\": \"extern\"\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        -768,
        544
      ],
      "id": "f5beef9e-1ed7-4f23-825b-f8bbc6f33b14",
      "name": "Structured Output Parser"
    },
    {
      "parameters": {
        "modelName": "models/gemini-2.5-flash",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "typeVersion": 1.1,
      "position": [
        -1040,
        544
      ],
      "id": "769be1a9-0f81-4f51-8638-16a6f59f8783",
      "name": "Gemini-2.5 flash",
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "typeVersion": 1.1,
      "position": [
        -912,
        544
      ],
      "id": "39affef9-b19e-41be-9055-04e39baee7fb",
      "name": "Gemini-3 flash",
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "joinemailrequest@gmail.com (IMAP)": {
      "main": [
        [
          {
            "node": "CalculateDateKey",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request getDayKey": {
      "main": [
        [
          {
            "node": "InitializeEntry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request setLimit": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "If RequiredFieldsPresent",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send ErrorMessage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If RequiredFieldsPresent": {
      "main": [
        [
          {
            "node": "BuildTaskPayload",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "BuildTaskPayload": {
      "main": [
        [
          {
            "node": "HTTP Request CreateTaskInFirebase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request CreateTaskInFirebase": {
      "main": [
        [
          {
            "node": "Send SuccessMessage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CalculateDateKey": {
      "main": [
        [
          {
            "node": "HTTP Request getDayKey",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "InitializeEntry": {
      "main": [
        [
          {
            "node": "FilterCurrentMessage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FilterCurrentMessage": {
      "main": [
        [
          {
            "node": "If dailyLimitReached",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send ErrorMessage": {
      "main": [
        [
          {
            "node": "MoveToInProgress (Error)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MoveToInProgress (Limit)": {
      "main": [
        [
          {
            "node": "RemoveInboxLabel (Limit)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send SuccessMessage": {
      "main": [
        [
          {
            "node": "MoveToDone (Success)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MoveToDone (Success)": {
      "main": [
        [
          {
            "node": "RemoveInboxLabel (Success)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send LimitMessage": {
      "main": [
        [
          {
            "node": "MoveToInProgress (Limit)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MoveToInProgress (Error)": {
      "main": [
        [
          {
            "node": "RemoveInboxLabel (Error)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Send MoveMessage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If dailyLimitReached": {
      "main": [
        [
          {
            "node": "HTTP Request setLimit",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send LimitMessage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "AI Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Gemini-2.5 flash": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Gemini-3 flash": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 1
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "versionId": "29dfe0eb-b23b-4b2c-bfd2-b86170113995",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "gLK6ixVUq44X7Gtd",
  "tags": []
}