{
  "name": "Discord-Summary-Agent - PUBLIC TEMPLATE",
  "nodes": [
    {
      "parameters": {
        "authentication": "oAuth2",
        "select": "user",
        "user": {
          "__rl": true,
          "value": "YOUR_SLACK_USER_OR_CHANNEL_ID",
          "mode": "id",
          "cachedResultName": "your_slack_destination"
        },
        "text": "={{ $json.message.content }}",
        "otherOptions": {
          "includeLinkToWorkflow": false,
          "mrkdwn": true,
          "unfurl_links": false,
          "unfurl_media": false
        }
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        272,
        192
      ],
      "id": "71bf588f-dd3f-4f6f-8457-74bfeb170642",
      "name": "Slack"
    },
    {
      "parameters": {
        "operation": "get",
        "guildId": {
          "__rl": true,
          "value": "YOUR_DISCORD_GUILD_ID",
          "mode": "id",
          "cachedResultName": "Your Discord Server",
          "cachedResultUrl": "https://discord.com/channels/YOUR_DISCORD_GUILD_ID"
        },
        "channelId": {
          "__rl": true,
          "value": "YOUR_DISCORD_CHANNEL_ID",
          "mode": "id",
          "cachedResultName": "your-channel-name",
          "cachedResultUrl": "https://discord.com/channels/YOUR_DISCORD_GUILD_ID/YOUR_DISCORD_CHANNEL_ID"
        }
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [
        -304,
        -144
      ],
      "id": "ed9f4703-8fa3-4fe8-8d3d-f296cd32bd1f",
      "name": "Get Channel"
    },
    {
      "parameters": {
        "amount": 2
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        -160,
        -144
      ],
      "id": "2afbe050-ff1c-4bed-923c-c37765608dfa",
      "name": "Wait"
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "getAll",
        "guildId": {
          "__rl": true,
          "value": "YOUR_DISCORD_GUILD_ID",
          "mode": "id",
          "cachedResultName": "Your Discord Server",
          "cachedResultUrl": "https://discord.com/channels/YOUR_DISCORD_GUILD_ID"
        },
        "channelId": {
          "__rl": true,
          "value": "YOUR_DISCORD_CHANNEL_ID",
          "mode": "id",
          "cachedResultName": "your-channel-name",
          "cachedResultUrl": "https://discord.com/channels/YOUR_DISCORD_GUILD_ID/YOUR_DISCORD_CHANNEL_ID"
        },
        "options": {
          "simplify": true
        }
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [
        -400,
        96
      ],
      "id": "d00d6edc-594a-4b38-9a6e-01dc10ccb3b6",
      "name": "Get Messages"
    },
    {
      "parameters": {
        "instructions": "filter to only include messages from the prior 48 hours",
        "codeGeneratedForPrompt": "filter to only include messages from the prior 48 hours",
        "jsCode": "const messages = $input.all();\nconst currentTime = new Date();\nconst twoDaysAgo = currentTime.setDate(currentTime.getDate() - 2);\n\nconst recentMessages = messages.filter((message) => {\n  const messageTime = new Date(message?.json?.timestamp);\n  return messageTime >= twoDaysAgo;\n});\n\nreturn recentMessages;\n"
      },
      "type": "n8n-nodes-base.aiTransform",
      "typeVersion": 1,
      "position": [
        -272,
        96
      ],
      "id": "a33adea7-b337-4ddc-8664-85ed9fb31c75",
      "name": "AI Transform"
    },
    {
      "parameters": {
        "jsCode": "// Get all input items from the AI Transform node\nconst messages = $input.all();\n\nconsole.log('=== COMBINING DISCORD MESSAGES ===');\nconsole.log('Total messages received from AI Transform:', messages.length);\n\n// Extract the JSON data from each message item\nconst messageData = messages.map(item => item.json);\n\n// Calculate some summary statistics\nconst now = new Date();\nlet oldestDate = null;\nlet newestDate = null;\n\nmessageData.forEach(msg => {\n  if (msg.timestamp) {\n    const msgDate = new Date(msg.timestamp);\n    if (!oldestDate || msgDate < oldestDate) oldestDate = msgDate;\n    if (!newestDate || msgDate > newestDate) newestDate = msgDate;\n  }\n});\n\n// Calculate time range\nconst timeRangeHours = oldestDate && newestDate ? \n  (newestDate - oldestDate) / (1000 * 60 * 60) : 0;\n\n// Get unique authors\nconst uniqueAuthors = [...new Set(messageData.map(msg => msg.author?.global_name || msg.author?.username))];\n\nconsole.log('Date range:', oldestDate?.toISOString(), 'to', newestDate?.toISOString());\nconsole.log('Time span:', timeRangeHours.toFixed(1), 'hours');\nconsole.log('Unique authors:', uniqueAuthors.length);\n\n// Return a single item containing all the messages and metadata\nreturn [{\n  json: {\n    messages: messageData,\n    messageCount: messageData.length,\n    timeRange: \"filtered messages\",\n    summary: {\n      totalMessages: messageData.length,\n      uniqueAuthors: uniqueAuthors.length,\n      authorNames: uniqueAuthors,\n      timeSpanHours: timeRangeHours,\n      oldestMessage: oldestDate ? oldestDate.toISOString() : null,\n      newestMessage: newestDate ? newestDate.toISOString() : null,\n      generatedAt: now.toISOString()\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -144,
        96
      ],
      "id": "4634d307-32cc-4525-84d8-724b07d6bc01",
      "name": "Code"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "4accdf36-0f8f-4fe4-97a1-99ed159f3e84",
              "leftValue": "={{ Array.isArray($json.incidents) ? $json.incidents.length : 0 }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        432,
        0
      ],
      "id": "026522cd-0b96-4499-b5ed-17212437de39",
      "name": "If (Incidents?)"
    },
    {
      "parameters": {
        "sendTo": "={{ $json.to }}",
        "subject": "={{ $json.subject }}",
        "message": "={{$json.text}}",
        "options": {
          "appendAttribution": false
        }
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        688,
        -144
      ],
      "id": "cdce4251-ecc0-47b6-8b15-5a3f8b9cecc9",
      "name": "Send a message"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "897ef1ab-591a-4645-8cb9-16032a92c2d9",
              "name": "to",
              "value": "user@yourcompany.com",
              "type": "string"
            },
            {
              "id": "a9b28744-2df5-44cc-b8d6-d736776d2152",
              "name": "subject",
              "value": "={{ $json.emailSubject }}",
              "type": "string"
            },
            {
              "id": "31f31962-b71e-4a86-8ab8-ba5636b8bd71",
              "name": "text",
              "value": "={{ $json.emailText }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        560,
        -144
      ],
      "id": "1fe50480-b2f9-4f8f-92c6-36fb60c0498a",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "jsCode": "const item = items[0].json;\n\n// If the classifier already returned a parsed object with incidents, take it.\nif (Array.isArray(item.incidents)) {\n  const channel =\n    item.channel ||\n    (item.summary && item.summary.channelName) ||\n    '#your-channel-name';\n\n  const oldest =\n    (item.summary && item.summary.oldestMessage) ||\n    item['summary.oldestMessage'] ||\n    '';\n\n  const newest =\n    (item.summary && item.summary.newestMessage) ||\n    item['summary.newestMessage'] ||\n    '';\n\n  const incidents = item.incidents;\n  const count = incidents.length;\n  const plural = count === 1 ? '' : 's';\n  const emailSubject = `Discord mod escalation: ${count} incident${plural} in ${channel}`;\n\n  let emailText = 'Moderator incident report\\n\\n';\n  if (count === 0) {\n    emailText += 'No incidents detected in the window.\\n';\n  } else {\n    for (let i = 0; i < incidents.length; i++) {\n      const it = incidents[i] || {};\n      emailText +=\n        `${i + 1}) ${(it.severity || 'unknown').toUpperCase()} | ${it.action || 'other'}\\n` +\n        `Time: ${it.timestamp || 'unknown'}\\n` +\n        `Moderator: ${it.moderator || 'unknown'}\\n` +\n        `Member: ${it.member || 'unknown'}\\n` +\n        `Summary: ${it.summary || 'n/a'}`;\n      if (i < incidents.length - 1) emailText += '\\n\\n';\n    }\n  }\n  emailText += `\\n\\nChannel: ${channel}\\nWindow: ${oldest} to ${newest}`;\n\n  return [{ json: { channel, summary: { oldestMessage: oldest, newestMessage: newest }, incidents, emailSubject, emailText } }];\n}\n\n// Otherwise, extract text from common LLM shapes and parse.\nlet content = '';\nif (typeof item.raw_content === 'string' && item.raw_content.trim() !== '') {\n  content = item.raw_content;\n} else if (item?.output) {\n  // LangChain OpenAI node usually uses `output`\n  content = item.output;\n} else if (item?.choices?.[0]?.message?.content) {\n  content = item.choices[0].message.content;\n} else if (item?.data?.[0]?.text) {\n  content = item.data[0].text;\n} else if (typeof item.text === 'string') {\n  content = item.text;\n} else if (typeof item.content === 'string') {\n  content = item.content;\n}\n\n// Parse the JSON string\nlet incidents = [];\nif (typeof content === 'string') {\n  try {\n    const cleaned = content.replace(/^```(?:json)?\\s*|\\s*```$/g, '');\n    const obj = JSON.parse(cleaned);\n    if (obj && Array.isArray(obj.incidents)) incidents = obj.incidents;\n  } catch (e) {\n    // leave incidents as []\n  }\n}\n\n// Carry forward window fields if present\nconst channel =\n  item.channel ||\n  (item.summary && item.summary.channelName) ||\n  '#your-channel-name';\n\nconst oldest =\n  (item.summary && item.summary.oldestMessage) ||\n  item['summary.oldestMessage'] ||\n  '';\n\nconst newest =\n  (item.summary && item.summary.newestMessage) ||\n  item['summary.newestMessage'] ||\n  '';\n\nconst count = Array.isArray(incidents) ? incidents.length : 0;\nconst plural = count === 1 ? '' : 's';\nconst emailSubject = `Discord mod escalation: ${count} incident${plural} in ${channel}`;\n\nlet emailText = 'Moderator incident report\\n\\n';\nif (count === 0) {\n  emailText += 'No incidents detected in the window.\\n';\n} else {\n  for (let i = 0; i < incidents.length; i++) {\n    const it = incidents[i] || {};\n    emailText +=\n      `${i + 1}) ${(it.severity || 'unknown').toUpperCase()} | ${it.action || 'other'}\\n` +\n      `Time: ${it.timestamp || 'unknown'}\\n` +\n      `Moderator: ${it.moderator || 'unknown'}\\n` +\n      `Member: ${it.member || 'unknown'}\\n` +\n      `Summary: ${it.summary || 'n/a'}`;\n    if (i < incidents.length - 1) emailText += '\\n\\n';\n  }\n}\nemailText += `\\n\\nChannel: ${channel}\\nWindow: ${oldest} to ${newest}`;\n\nreturn [{ json: { channel, summary: { oldestMessage: oldest, newestMessage: newest }, incidents, emailSubject, emailText } }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        272,
        0
      ],
      "id": "a10ab184-7922-41a0-9125-8bbce38a77a5",
      "name": "Parse Incidents JSON"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                3,
                5
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -464,
        -144
      ],
      "id": "bb8111bd-3150-4d8c-adc1-d344078afb8b",
      "name": "Every MWF at 8am"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5.4-mini",
          "mode": "list",
          "cachedResultName": "GPT-5.4-MINI"
        },
        "messages": {
          "values": [
            {
              "content": "=# You analyze a Discord channel’s last 48 hours of messages and return STRICT JSON only.\n\n## Rules:\n- Return valid JSON only. No prose. No code fences.\n- Top-level object must include: { \"incidents\": [...] }\n- \"incidents\" is an array of 0+ objects. Empty array if none.\n- Each incident object:\n  {\n    \"timestamp\": \"ISO 8601 or readable time\",\n    \"moderator\": \"username or id\",\n    \"member\": \"username or id\",\n    \"action\": \"warn|remind_rules|mute|kick|ban|lock_thread|other\",\n    \"severity\": \"low|medium|high\",\n    \"summary\": \"1-2 sentence description of what happened and why it required moderation\"\n  }\n\n## Criteria (what counts as an incident):\n- A moderator explicitly reminds rules, locks a thread, deletes a message for rules, times out a member, or kicks/bans.\n- Disputes where a mod intervention occurs, even if resolved quickly.\n\n\n## Moderator usernames\nThis is the list of moderator usernames: Chicken, Hailey, Kevlar, Kuno, Omi, [YOURTEAM] Justin S., YourCompany Team\n\n## Example output with incidents:\n{\n  \"incidents\": [\n    {\n      \"timestamp\": \"2025-09-28T23:05:12Z\",\n      \"moderator\": \"mod_aurora\",\n      \"member\": \"user_ryx\",\n      \"action\": \"remind_rules\",\n      \"severity\": \"low\",\n      \"summary\": \"Moderator reminded user about anti-spoiler policy after posting untagged spoilers.\"\n    }\n  ]\n}\n\n## Example output with no incidents:\n{ \"incidents\": [] }\n\n## Severity rubric:\n- low: gentle reminder, minor off-topic nudge, first warning.\n- medium: repeated issues, heated argument requiring firm mod action, deletions.\n- high: mutes/timeouts/kicks/bans, harassment/hate speech, safety concerns."
            },
            {
              "content": "=Analyze ONLY this data and return STRICT JSON as instructed. Do not wrap in code fences.\n\n{{ JSON.stringify($json, null, 2) }}\n"
            }
          ]
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        0,
        0
      ],
      "id": "6ea9c268-93c1-4537-ada1-0ae6af29336b",
      "name": "Mod Incident Classifier (GPT 5.4 mini)"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5.4-mini",
          "mode": "list",
          "cachedResultName": "GPT-5.4-MINI"
        },
        "messages": {
          "values": [
            {
              "content": "=# Discord Channel Summary Generator\n\nYou are a Discord channel summary generator. You will receive JSON data containing Discord messages that have been pre-filtered to the last 48 hours from Your Discord Server server's #your-channel-name channel.\n\n## Your Task:\nAnalyze ONLY the messages provided in the input JSON and create a summary. Do not use any external knowledge or make assumptions beyond what's in the data.\n\n## Expected Input Format:\n```json\n{\n  \"messages\": [array of message objects],\n  \"messageCount\": number,\n  \"summary\": {metadata object}\n}\n```\n\n## Instructions:\n1. Read through ALL messages in the \"messages\" array\n2. Count actual participants by looking at author.global_name or author.username\n3. Identify discussion topics from the actual message content\n4. Note timestamps to understand activity patterns\n5. Reference specific messages and usernames in your summary\n\n## Output Format:\nCreate a Slack-formatted summary with:\n\n*📊 48-Hour Activity Overview*\n- Total messages: [actual count]\n- Most Active participants: [actual usernames, up to a maximum of 5 most active participants based on their message count, with message count in parentheses]\n\n*🔥 Main Discussion Topics*\n- [Extract from actual message content to summarize 3-5 main discussion topics]\n- [Quote relevant messages if helpful]\n\n*💬 Key Conversations* \n- [Moderator input or intervention (Chicken, Hailey, Kevlar, Kuno, Omi, [YOURTEAM] Justin S., YourCompany Team)]\n- [messages or topics that elicited a lot of responses or more debate]\n\n*⭐ Overall Summary*\n- [brief high level summary of the 48-hour snapshot]\n\n## Critical Rules:\n- Use ONLY the provided message data\n- Include actual message content/quotes sparingly, only when the topic is inflammatory or eliciting a moderator response\n- If no messages about a topic, don't mention that topic\n- Be specific and factual based on the input data\n- the output (total message length) MUST BE NO MORE than 300 words\n\n## Other Instructions:\n\nDo not end the message with any offers to follow up or provide more information or encouragement for the team -- just end the message after the overall summary.\n\nDo not address the Slack message to anyone or say \"hi team\" or similar, simply start with the bolded message title \"48 HOURS #SAGA-COMMON-ROOM SUMMARY\" (use Discord Markdown to put it in italics)",
              "role": "system"
            },
            {
              "content": "==Please analyze this Discord channel data and create a single summary:\n\nCOMPLETE DATA STRUCTURE:\n{{ JSON.stringify($json, null, 2) }}\n\nMESSAGES TO ANALYZE:\n{{ $json.messages.map(msg => `\nTIMESTAMP: ${msg.timestamp}\nAUTHOR: ${msg.author.global_name || msg.author.username}\nCONTENT: ${msg.content}\n---`).join('') }}\n\nSUMMARY STATS:\n- Total Messages: {{ $json.messageCount }}\n- Unique Authors: {{ $json.summary.uniqueAuthors }}\n- Time Range: {{ $json.summary.oldestMessage }} to {{ $json.summary.newestMessage }}\n\nPlease create a summary based ONLY on the {{ $json.messageCount }} messages listed above."
            }
          ]
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        0,
        192
      ],
      "id": "17d9bd67-b716-4990-8634-a08088fc57b5",
      "name": "GPT 5.4 mini"
    }
  ],
  "connections": {
    "Get Channel": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "Get Messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Messages": {
      "main": [
        [
          {
            "node": "AI Transform",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Transform": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code": {
      "main": [
        [
          {
            "node": "GPT 5.4 mini",
            "type": "main",
            "index": 0
          },
          {
            "node": "Mod Incident Classifier (GPT 5.4 mini)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If (Incidents?)": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Incidents JSON": {
      "main": [
        [
          {
            "node": "If (Incidents?)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every MWF at 8am": {
      "main": [
        [
          {
            "node": "Get Channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mod Incident Classifier (GPT 5.4 mini)": {
      "main": [
        [
          {
            "node": "Parse Incidents JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT 5.4 mini": {
      "main": [
        [
          {
            "node": "Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner",
    "errorWorkflow": "wr04ZVaAacD0frHP",
    "timeSavedMode": "fixed",
    "availableInMCP": false
  },
  "tags": []
}
