{
  "name": "HaloPSA: New Ticket \u2192 AI Summary (Template)",
  "nodes": [
    {
      "name": "Note: Webhook",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -24,
        48
      ],
      "parameters": {
        "content": "### \ud83d\udd14 Webhook (HaloPSA \u2192 n8n)\nUse **POST**. Replace `WEBHOOK_PATH` below, then paste the full **Production URL** back into HaloPSA webhook.\n- Path: something unique, e.g. `halopsa-new-ticket-ai`\n- HaloPSA payload must include `ticket`, `team` or `team_id` if you use the guard."
      },
      "typeVersion": 1
    },
    {
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        0,
        128
      ],
      "parameters": {
        "path": "WEBHOOK_PATH",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "name": "Note: Guard",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        200,
        48
      ],
      "parameters": {
        "content": "### \ud83d\udea7 Guard (optional)\nSkips tickets for a team you don\u2019t want to process.\n- Change `teamName` or `teamId` checks below (e.g. `sales` or `6`).\n- Remove this node if you don\u2019t need filtering."
      },
      "typeVersion": 1
    },
    {
      "name": "Guard",
      "type": "n8n-nodes-base.code",
      "position": [
        224,
        128
      ],
      "parameters": {
        "jsCode": "// Guard - Stop workflow if the ticket belongs to a filtered team\nconst body = $json.body || {};\nconst teamName = String(body.team ?? body.ticket?.team ?? '').toLowerCase();\nconst teamId = Number(body.team_id ?? body.ticket?.team_id ?? NaN);\nconst isFiltered = teamName === 'sales' || teamId === 6; // <- change these\nif (isFiltered) return []; // stop\nreturn $input.all();"
      },
      "typeVersion": 2
    },
    {
      "name": "Note: Extract",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        424,
        48
      ],
      "parameters": {
        "content": "### \ud83d\udce6 Extract Ticket\nPulls `id`, `summary`, `details` from webhook body.\nNo keys to change unless your webhook payload uses different field names."
      },
      "typeVersion": 1
    },
    {
      "name": "Extract Ticket",
      "type": "n8n-nodes-base.code",
      "position": [
        448,
        128
      ],
      "parameters": {
        "jsCode": "const ticket = $input.item.json.body?.ticket;\nif (!ticket) throw new Error('No ticket object found in webhook payload.');\nconst summaryCard = `\ud83c\udd95 New ticket created\\n\\n\ud83e\uddfe Ticket ID: ${ticket.id}\\n\ud83e\uddd1\u200d\ud83d\udcbb Summary: ${ticket.summary}\\n\ud83d\udcdd Description: ${ticket.details || 'No details provided.'}\\n`;\nreturn [{ json: { ticket_id: ticket.id, summary_card: summaryCard, details: ticket.details || '', subject: ticket.summary } }];"
      },
      "typeVersion": 2
    },
    {
      "name": "Note: Prompt",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        648,
        48
      ],
      "parameters": {
        "content": "### \ud83e\udde0 Build AI Prompt (Gemini / any LLM)\nEdit wording and the **tools/systems** your MSP uses.\nNo secrets here.\nOutputs a single `prompt` string."
      },
      "typeVersion": 1
    },
    {
      "name": "Build AI Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        672,
        128
      ],
      "parameters": {
        "jsCode": "const { subject, details, ticket_id } = $input.item.json;\nconst prompt = `You are a senior MSP engineer. Analyze the support ticket and return **strict JSON** with: summary, next_step, troubleshooting_suggestions (HTML list), system_actions (optional HTML list).\\n\\n### Systems we use\\n1. NinjaOne (RMM)\\n2. Microsoft 365 (tenant management)\\n3. CIPP (multi-tenant admin)\\n\\n### Ticket\\n- ID: ${ticket_id}\\n- Subject: ${subject}\\n- Details:\\n${details}\\n`;\nreturn [{ json: { prompt, ticket_id } }];"
      },
      "typeVersion": 2
    },
    {
      "name": "Note: AI Agent",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        872,
        48
      ],
      "parameters": {
        "content": "### \ud83e\udd16 AI Agent (LangChain)\nConnect your **LLM node** here.\n- If using Gemini: add a *Google Gemini Chat Model* node and connect as the **Language Model** input.\n- Or swap for OpenAI/other LLM nodes.\n- **Set credentials in the model node, not here.**"
      },
      "typeVersion": 1
    },
    {
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        896,
        128
      ],
      "parameters": {
        "text": "={{ $json.prompt }}",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "name": "Note: LLM Model",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        952,
        272
      ],
      "parameters": {
        "content": "### \ud83e\udde9 Gemini / LLM Model Node\n**Set your API credentials** here.\n- Example: Google Gemini Chat Model\n- No other changes required."
      },
      "typeVersion": 1
    },
    {
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        976,
        352
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "name": "Note: Parse",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1224,
        48
      ],
      "parameters": {
        "content": "### \ud83d\udcd1 Parse LLM Output (JSON)\nStrips ```json fences if present and parses to fields:\n- `summary`, `next_step`, `troubleshooting` (HTML), `ticket_id`."
      },
      "typeVersion": 1
    },
    {
      "name": "Parse AI JSON",
      "type": "n8n-nodes-base.code",
      "position": [
        1248,
        128
      ],
      "parameters": {
        "jsCode": "const raw = $json.output ?? '';\nlet clean = raw.trim();\nif (clean.startsWith('```')) {\n  clean = clean.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/```$/,'').trim();\n}\nlet parsed; try { parsed = JSON.parse(clean); } catch (e) { throw new Error('Failed to parse LLM JSON: ' + e.message); }\nreturn [{ json: { summary: parsed.summary, next_step: parsed.next_step, troubleshooting: parsed.troubleshooting_suggestions, ticket_id: $json.ticket_id } }];"
      },
      "typeVersion": 2
    },
    {
      "name": "Note: HTML Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1448,
        48
      ],
      "parameters": {
        "content": "### \ud83e\uddf1 Build HTML Note (branding)\nChange **logo URL**, colours, footer text.\nOutput: `note_html` + `ticket_id`."
      },
      "typeVersion": 1
    },
    {
      "name": "Build AI HTML Note",
      "type": "n8n-nodes-base.code",
      "position": [
        1472,
        128
      ],
      "parameters": {
        "jsCode": "const ticket_id = $items('Extract Ticket', 0, 0)[0]?.json?.ticket_id; if (!ticket_id) throw new Error('ticket_id missing');\nconst summary = String($json.summary||'').trim();\nconst next_step = String($json.next_step||'').trim();\nconst troubleshooting_html = String($json.troubleshooting||'').trim();\nconst LOGO_URL = 'https://YOUR_LOGO_URL/logo.png'; // <- change\nconst BRAND = 'Your MSP Brand';\nconst note_html = `\n<div style=\"font-family: Arial, sans-serif; font-size: 14px; color: #333; background: #fafafa; padding: 16px; border-radius: 8px; border: 1px solid #ddd; line-height: 1.6;\">\n  <div style=\"text-align: center; margin-bottom: 20px;\"><img src=\"${LOGO_URL}\" alt=\"${BRAND}\" style=\"max-width: 180px; height: auto;\" /></div>\n  <h2 style=\"color: #0055a5; margin-top: 0; border-bottom: 1px solid #ccc; padding-bottom: 6px;\">\ud83e\udde0 AI Ticket Summary</h2>\n  <p><strong>Ticket ID:</strong> ${ticket_id}</p>\n  <h3 style=\"margin-top: 24px;\">Summary</h3><div>${summary || 'n/a'}</div>\n  <h3 style=\"margin-top: 24px;\">Next Step</h3><div>${next_step || 'n/a'}</div>\n  <h3 style=\"margin-top: 24px;\">Troubleshooting Suggestions</h3><div>${troubleshooting_html || '<em>None</em>'}</div>\n  <hr style=\"margin: 30px 0; border: none; border-top: 1px solid #ccc;\" />\n  <p style=\"font-size: 12px; color: #666;\">This note was generated automatically by <strong>${BRAND} \u2013 AI Assistant</strong>.</p>\n</div>`;\nreturn [{ json: { ticket_id, note_html } }];"
      },
      "typeVersion": 2
    },
    {
      "name": "Note: Wrap",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1668,
        48
      ],
      "parameters": {
        "content": "### \ud83d\udce6 Wrap for HaloPSA\nPrepares the **Actions API** payload for a Private Note.\n- Change `is_visible_to_user` / `outcome` if needed."
      },
      "typeVersion": 1
    },
    {
      "name": "Wrap for Halo",
      "type": "n8n-nodes-base.code",
      "position": [
        1696,
        128
      ],
      "parameters": {
        "jsCode": "return [{ json: { payload: [{ ticket_id: $json.ticket_id, note_html: $json.note_html, is_visible_to_user: false, outcome: 'Private Note' }] } }];"
      },
      "typeVersion": 2
    },
    {
      "name": "Note: Halo HTTP",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1892,
        48
      ],
      "parameters": {
        "content": "### \ud83c\udf10 HTTP \u2192 HaloPSA\nPOST to your HaloPSA **Actions** endpoint.\n- Base URL: `https://YOUR_HALO_DOMAIN/api/actions`\n- Auth Header: set your API token or Basic auth.\n**Replace placeholders below** in URL & Headers."
      },
      "typeVersion": 1
    },
    {
      "name": "HaloPSA: Create Note",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1920,
        128
      ],
      "parameters": {
        "url": "https://YOUR_HALO_DOMAIN/api/actions",
        "method": "POST",
        "options": {
          "headers": {
            "Content-Type": "application/json",
            "Authorization": "Bearer YOUR_TOKEN_HERE"
          }
        },
        "jsonBody": "={{ $json.payload }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Guard": {
      "main": [
        [
          {
            "node": "Extract Ticket",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Guard",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Parse AI JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI JSON": {
      "main": [
        [
          {
            "node": "Build AI HTML Note",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wrap for Halo": {
      "main": [
        [
          {
            "node": "HaloPSA: Create Note",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Ticket": {
      "main": [
        [
          {
            "node": "Build AI Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI Prompt": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI HTML Note": {
      "main": [
        [
          {
            "node": "Wrap for Halo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  }
}