This workflow corresponds to n8n.io template #7557 — we link there as the canonical source.
This workflow follows the Agent → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"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
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
What it is
Source: https://n8n.io/workflows/7557/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
⏺ 🚀 How it works
CLINICAINTEGRAL_secretary. Uses postgres, mcpClientTool, googleDriveTool, toolWorkflow. Webhook trigger; 89 nodes.
secretaria. Uses postgres, n8n-nodes-evolution-api, openAi, httpRequest. Webhook trigger; 71 nodes.
LineOA. Uses httpRequest, agent, lmChatGoogleGemini, outputParserStructured. Webhook trigger; 69 nodes.
Resume Screening & Behavioral Interviews with Gemini, Elevenlabs, & Notion ATS copy. Uses outputParserStructured, chainLlm, googleDrive, stickyNote. Webhook trigger; 67 nodes.