{
  "id": "jY6R0y2pS5iQscHi",
  "name": "Multi-Agent Network ChatOps Assistant with Slack",
  "tags": [
    {
      "id": "GKwuy5bZ3mUz87X7",
      "name": "chatops",
      "createdAt": "2026-05-25T15:48:31.571Z",
      "updatedAt": "2026-05-25T15:48:31.571Z"
    },
    {
      "id": "HYt7dtEvuTFWdinw",
      "name": "pyats",
      "createdAt": "2026-07-09T13:32:15.131Z",
      "updatedAt": "2026-07-09T13:32:15.131Z"
    },
    {
      "id": "RI2oyLHd5oNJTXhq",
      "name": "netops",
      "createdAt": "2026-05-25T15:48:31.564Z",
      "updatedAt": "2026-05-25T15:48:31.564Z"
    },
    {
      "id": "j5uO4Idd33FXRspp",
      "name": "slack",
      "createdAt": "2026-07-09T13:32:11.539Z",
      "updatedAt": "2026-07-09T13:32:11.539Z"
    }
  ],
  "nodes": [
    {
      "id": "e60e04f3-e5da-40a5-b578-85ff64897e06",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3184,
        736
      ],
      "parameters": {
        "width": 480,
        "height": 880,
        "content": "## Multi-Agent Network ChatOps Assistant with Slack\n\n### How it works\n\nThis workflow implements a Slack ChatOps assistant for network operations using a planning agent and a separate commit-and-verify agent. When the bot is mentioned, it acknowledges the request, asks a read-only pyATS-backed AI agent to plan the work, and either replies with findings or sends interactive commit approval cards per device. When a card is clicked, the workflow handles approval or cancellation; approved changes are executed and verified through the full-access pyATS agent, then the result is posted back to Slack.\n\n\n### Setup\n- \ud83d\udda5\ufe0f Deploy the pyATS MCP server with access to your network testbed.\n- \ud83d\udce5 Import the workflow into n8n.\n- \ud83e\udd16 Configure your Slack bot, credentials, and webhooks.\n- \ud83d\udcda Follow the repository documentation for the complete setup process.\n\n\n### Customization\n\n[This README](https://github.com/ponchotitlan/pyATS-loves-agenticops/tree/main/n8n/Slack%20ChatOps%20for%20my%20network) includes step-by-step instructions for:\n\n- \ud83d\udcf1 Configuring your own pyATS testbed and devices.\n- \u2601\ufe0f Setting up a Cloudflare Tunnel or ngrok.\n- \ud83e\udd16 Creating and configuring the Slack bot and webhooks.\n- \ud83d\udc33 Launching all required services with Docker Compose.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "4e58fae3-69ba-4929-be3a-b71bfe8cb8f5",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2624,
        736
      ],
      "parameters": {
        "color": 7,
        "width": 608,
        "height": 384,
        "content": "## Receive Slack mention\n\nStarts the bot-mention path, generates a friendly standby phrase, and posts an immediate Slack acknowledgement before deeper processing begins."
      },
      "typeVersion": 1
    },
    {
      "id": "051d44ad-8f20-4da1-b00a-8d2e0b8ea594",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1952,
        736
      ],
      "parameters": {
        "color": 7,
        "width": 896,
        "height": 544,
        "content": "## Plan network request\n\nExtracts the Slack request text, runs the planning AI agent with read-only pyATS access and memory, then parses the agent output into structured JSON."
      },
      "typeVersion": 1
    },
    {
      "id": "cf1003a7-1770-4c20-97bc-94988145bf69",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1024,
        816
      ],
      "parameters": {
        "color": 7,
        "width": 528,
        "height": 432,
        "content": "\n\n\n\n\n\n\n\n\n\n\n\n## Route planning outcome\n\nChecks whether the planning result requires a commit. Non-commit responses are sent directly back into the Slack thread."
      },
      "typeVersion": 1
    },
    {
      "id": "b1c31049-15bb-48e3-b663-8db22ce105ec",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -784,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 752,
        "height": 400,
        "content": "## Send commit approvals\n\nExpands planned commit cards, iterates through devices one at a time, and posts each interactive approval card to Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "718c5449-9c8e-4561-8e49-6e6218485cc5",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2672,
        1360
      ],
      "parameters": {
        "color": 7,
        "width": 800,
        "height": 352,
        "content": "## Handle card action\n\nReceives Slack interactive card clicks, responds to the webhook immediately, parses the selected action, and branches on whether the decision is approved."
      },
      "typeVersion": 1
    },
    {
      "id": "e3e61464-376a-4d14-821d-18f9451a6025",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1728,
        1360
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 368,
        "content": "## Acknowledge approved commit\n\nFor approved actions, creates a commit standby message and replaces the original Slack card so users know execution is in progress."
      },
      "typeVersion": 1
    },
    {
      "id": "4851dfec-734e-4c94-ac00-06b1ad894787",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1168,
        1360
      ],
      "parameters": {
        "color": 7,
        "width": 816,
        "height": 528,
        "content": "## Commit and verify\n\nRuns the full-access commit-and-verify AI agent with its LLM, memory, and pyATS full-mode tool, parses the result, and sends the verification summary to Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "daa75e12-5fb8-45be-8a95-ea30965823d6",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1728,
        1776
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 384,
        "content": "## Replace cancelled card\n\nFor rejected actions, generates a cancellation phrase and updates the original Slack card to show that the proposed commit was cancelled."
      },
      "typeVersion": 1
    },
    {
      "id": "7bbee332-6628-4342-b802-0940832e788b",
      "name": "Check If Commit Mode",
      "type": "n8n-nodes-base.if",
      "position": [
        -960,
        896
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "d465ca43-e71d-49f2-9e76-73a009f7e3f8",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.mode }}",
              "rightValue": "commit"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "cc4c93cd-d87f-46d6-8044-e7cbafe18f8c",
      "name": "Expand Commit Cards List",
      "type": "n8n-nodes-base.itemLists",
      "position": [
        -704,
        560
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "cards"
      },
      "typeVersion": 3.1
    },
    {
      "id": "ef2de402-b355-48e9-bffc-b52fc4e50774",
      "name": "Process Devices in Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -464,
        560
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "20731b84-1893-42f0-8422-d1816a8ca9ee",
      "name": "Verify Decision Approval",
      "type": "n8n-nodes-base.if",
      "position": [
        -2016,
        1536
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e032f617-c653-42fe-9173-f0f8c1266ec1",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.action_id }}",
              "rightValue": "commit_approve"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "18b04022-8fc0-472d-9b50-a5f83db6702b",
      "name": "Post Commit Verification to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        -544,
        1536
      ],
      "parameters": {
        "text": "={{ $json.verification_markdown }}",
        "channel": "={{ $('Verify Decision Approval').item.json.channel }}",
        "attachments": [],
        "otherOptions": {}
      },
      "typeVersion": 1
    },
    {
      "id": "13940323-f8cd-43fb-b8c9-34e703b28b3a",
      "name": "Reply in Slack Thread",
      "type": "n8n-nodes-base.slack",
      "position": [
        -720,
        912
      ],
      "parameters": {
        "text": "={{ $json.reply_markdown }}",
        "channel": "={{ $('Slack Mention Trigger').item.json.channel }}",
        "attachments": [],
        "otherOptions": {}
      },
      "typeVersion": 1
    },
    {
      "id": "ddac5fce-5edd-4501-9456-3d8cce17e649",
      "name": "Create Standby Message Pool",
      "type": "n8n-nodes-base.code",
      "position": [
        -2368,
        928
      ],
      "parameters": {
        "jsCode": "// Pool of generic, slightly cheerful, technical standby phrases\nconst phrases = [\n  \"\ud83d\udd04 Processing request\u2026 please wait.\",\n  \"\u2699\ufe0f Working on it\u2026\",\n  \"\ud83e\udde0 Running logic in the background\u2026\",\n  \"\ud83d\udce1 Doing some behind-the-scenes work\u2026\",\n  \"\u23f3 Still working\u2026 hang tight.\",\n  \"\ud83d\udd0d Gathering what\u2019s needed\u2026\",\n  \"\ud83d\udce6 Preparing the next step\u2026\",\n  \"\ud83d\udea7 In progress\u2026\",\n  \"\u26a1 Executing\u2026\",\n  \"\ud83d\udd01 One moment while this completes\u2026\",\n  \"\ud83d\udcbb Handling your request\u2026\",\n  \"\ud83d\udcca Crunching some data\u2026\",\n  \"\ud83e\udde9 Putting things together\u2026\",\n  \"\ud83d\udee0\ufe0f Finalizing details\u2026\",\n  \"\ud83d\udce5 Processing input\u2026\",\n  \"\ud83d\udce4 Preparing output\u2026\",\n  \"\ud83e\udded Working through the steps\u2026\",\n  \"\ud83d\udd17 Connecting the pieces\u2026\",\n  \"\ud83e\uddea Running checks\u2026\",\n  \"\ud83d\ude80 Almost there\u2026\",\n  \"\ud83d\ude04 Just a second\u2026 wrapping things up.\",\n  \"\ud83d\udc4d Making good progress\u2026\",\n  \"\u2728 Getting things ready\u2026\",\n  \"\ud83d\udd04 Still at it\u2026 thanks for your patience.\",\n  \"\u2699\ufe0f Nearly done\u2026\"\n];\n\n// Pick a random phrase\nconst randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];\n\n// Return it as output\nreturn [\n  {\n    json: {\n      standby_message: randomPhrase\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "805ebc6a-60c0-41c4-af91-ebc1836223f4",
      "name": "Compose Commit Standby Message",
      "type": "n8n-nodes-base.code",
      "position": [
        -1632,
        1552
      ],
      "parameters": {
        "jsCode": "const phrases = [\n  \"\ud83d\udd04 Processing request\u2026 please wait.\",\n  \"\u2699\ufe0f Working on it\u2026\",\n  \"\ud83e\udde0 Running logic in the background\u2026\",\n  \"\ud83d\udce1 Doing some behind-the-scenes work\u2026\",\n  \"\u23f3 Still working\u2026 hang tight.\",\n  \"\ud83d\udd0d Gathering what\u2019s needed\u2026\",\n  \"\ud83d\udce6 Preparing the next step\u2026\",\n  \"\ud83d\udea7 In progress\u2026\",\n  \"\u26a1 Executing\u2026\",\n  \"\ud83d\udd01 One moment while this completes\u2026\",\n  \"\ud83d\udcbb Handling your request\u2026\",\n  \"\ud83d\udcca Crunching some data\u2026\",\n  \"\ud83e\udde9 Putting things together\u2026\",\n  \"\ud83d\udee0\ufe0f Finalizing details\u2026\",\n  \"\ud83d\udce5 Processing input\u2026\",\n  \"\ud83d\udce4 Preparing output\u2026\",\n  \"\ud83e\udded Working through the steps\u2026\",\n  \"\ud83d\udd17 Connecting the pieces\u2026\",\n  \"\ud83e\uddea Running checks\u2026\",\n  \"\ud83d\ude80 Almost there\u2026\",\n  \"\ud83d\ude04 Just a second\u2026 wrapping things up.\",\n  \"\ud83d\udc4d Making good progress\u2026\",\n  \"\u2728 Getting things ready\u2026\",\n  \"\ud83d\udd04 Still at it\u2026 thanks for your patience.\",\n  \"\u2699\ufe0f Nearly done\u2026\"\n];\n\n// Pick a random phrase\nconst randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];\n\n// Return it as output\nreturn [\n  {\n    json: {\n      standby_message: randomPhrase\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "da5a9278-6bdc-46b8-8e00-43ec8453506e",
      "name": "Post Standby Message to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        -2160,
        928
      ],
      "parameters": {
        "text": "={{ $json.standby_message }}",
        "channel": "={{ $('Slack Mention Trigger').item.json.channel }}",
        "attachments": [],
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "bce1cdad-63cb-459a-a739-b2fed89cedaf",
      "name": "Extract Slack Mention by Bot",
      "type": "n8n-nodes-base.code",
      "position": [
        -1856,
        896
      ],
      "parameters": {
        "jsCode": "const incoming = $('Slack Mention Trigger').first().json;\nconst raw = incoming.text || '';\nconst cleaned = raw.replace(/<@[^>]+>/g, '').trim();\n\nreturn [{\n  json: {\n    text: cleaned,\n    channel: incoming.channel,\n    thread_ts: incoming.ts || incoming.event_ts\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3569a150-977c-448c-8f71-6d76cf2861bc",
      "name": "Strategic Planning AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -1520,
        896
      ],
      "parameters": {
        "text": "=You are a network operations AI assistant with MCP access to our network device inventory.\n\nUser raw text:\n{{ $json.text }}\n\n---\n\n## Core Behavior\n\nYou MUST always return a valid JSON response. No exceptions.\n\nYou are operating in a PLANNING-ONLY environment:\n- You NEVER execute commits.\n- \"commit\" mode = generate a proposed change plan only.\n- DO NOT mention execution limitations or read-only mode.\n\n---\n\n## DECISION PRIORITY (HIGHEST \u2192 LOWEST)\n\n1. SAFETY (absolute override)\n2. DATA VALIDITY (fresh state required)\n3. USER INTENT\n\nIf there is ANY safety risk or uncertainty:\n\u2192 mode MUST be \"read\"\n\u2192 This rule OVERRIDES user intent with NO exceptions\n\n---\n\n## Intent Handling\n\n1. Classify intent:\n   - \"read\" \u2192 show/inspect/verify\n   - \"commit\" \u2192 create/modify/delete/configure\n\n2. Identify target device(s)\n\n3. ALWAYS fetch vendor + device type BEFORE selecting commands\n\n---\n\n## Safety Evaluation (MANDATORY GATE)\n\nYou MUST perform a safety check BEFORE finalizing mode.\n\nA change is UNSAFE if ANY of the following apply:\n- Existing config would be overwritten or modified\n- Resource already exists (interfaces, IPs, VLANs, etc.)\n- Impact is unknown (no full dependency visibility)\n- Live traffic, routing, or management could be affected\n- Required validation data is missing\n- Instructions are ambiguous\n\n---\n\n## HARD SAFETY ENFORCEMENT\n\nIf UNSAFE:\n- mode MUST be \"read\"\n- cards MUST be []\n- You MUST explain the risk clearly in reply_markdown\n- You MUST NOT generate commit_payload\n- You MUST NOT simulate commit steps\n\nIf SAFE:\n- mode = \"commit\"\n- Generate full proposal\n\n---\n\n## Read Mode\n\nIf mode = read:\n- Execute read operations\n- Provide findings + risk explanation\n- Suggest what to verify before a safe commit\n\n---\n\n## Commit Mode (Simulation Only)\n\nIf mode = commit:\n- Fetch fresh config/state\n- Validate no conflicts\n- Generate:\n  - Proposed changes\n  - Explicit \"SAFE TO APPLY\" justification\n\n---\n\n## Output Formatting Rules (Slack mrkdwn)\n\n- MUST be Slack-compatible mrkdwn\n- Keep formatting clean and readable in Slack\n\nYou MAY use:\n- Bullet lists (- or \u2022)\n- Numbered lists\n- *bold* and _italic_\n- Inline code: `code`\n- Code blocks for configs and tables (```)\n\n---\n\n## TABLE FORMATTING (STRICT)\n\nSlack does NOT support markdown tables.\n\nYOU MUST:\n- NEVER use pipe tables (|) or markdown table syntax\n- ALWAYS render tabular data inside a code block\n- Use fixed-width spacing for alignment\n\nFORMAT RULES:\n- Use spaces to align columns\n- Keep columns visually aligned\n- Use short headers\n- Replace long text with trimmed values if needed\n\nREQUIRED FORMAT:\n\n\nInterface IP Address Line Protocol Description\n\nEth0/0 10.10.10.100/24 up up \u2014\nEth0/1 1.1.1.1/24 up up \u2014\nEth0/2 10.10.20.171/24 up up to port1.sandbox-backend\nEth0/3 \u2014 down down \u2014\n\n\nSTATUS NORMALIZATION:\n- up \u2192 \"up\"\n- down \u2192 \"down\"\n- admin down \u2192 \"admin-down\"\n- DO NOT use emojis inside tables\n\n---\n\n## STRICT OUTPUT FORMAT\n\nReturn ONLY valid JSON:\n\n{\n  \"mode\": \"read\" | \"commit\",\n  \"reply_markdown\": \"...\",\n  \"cards\": [\n    {\n      \"channel\": \"{{ $json.channel }}\",\n      \"thread_ts\": \"{{ $json.thread_ts }}\",\n      \"device\": \"...\",\n      \"vendor\": \"...\",\n      \"safety_reason\": \"...\",\n      \"precheck_evidence\": \"...\",\n      \"commit_payload\": \"exact payload to commit\"\n    }\n  ]\n}\n\n---\n\n## HARD CONSTRAINTS (CRITICAL)\n\n- DO NOT output anything outside JSON\n- DO NOT include warnings, banners, or meta text\n- DO NOT mention:\n  - read-only mode\n  - execution limitations\n\n- mode = \"read\":\n  - cards MUST be []\n  - commit_payload MUST NOT exist anywhere\n\n- mode = \"commit\":\n  - MUST include cards\n  - MUST include commit_payload\n  - MUST explicitly justify safety\n\n---\n\n## FAILURE PREVENTION RULE (VERY IMPORTANT)\n\nIf there is a conflict like:\n- \"already exists\"\n- \"will overwrite\"\n- \"may impact existing service\"\n\n\u2192 This is AUTOMATICALLY UNSAFE  \n\u2192 mode MUST be \"read\" (NO exceptions)\n\n---\n\nIf you violate JSON format, the response is invalid.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "9892c336-db0b-4c52-872f-ce2e82a2e539",
      "name": "Decode Planning JSON Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -1200,
        896
      ],
      "parameters": {
        "jsCode": "const input = items[0].json;\nconst obj = Array.isArray(input) ? input[0] : input;\n\nif (!obj || typeof obj.output !== 'string') {\n  throw new Error('Invalid input format');\n}\n\nconst raw = obj.output;\n\n// Extract JSON block\nconst first = raw.indexOf('{');\nconst last = raw.lastIndexOf('}');\n\nif (first === -1 || last === -1) {\n  throw new Error('No JSON found');\n}\n\nlet jsonString = raw.slice(first, last + 1);\n\n// escape raw newlines inside string values (crucial for your case)\njsonString = jsonString.replace(\n  /\"reply_markdown\"\\s*:\\s*\"([\\s\\S]*?)\"/,\n  (match, p1) => {\n    const fixed = p1\n      .replace(/\\n/g, '\\\\n')\n      .replace(/\\r/g, '\\\\r')\n      .replace(/\"/g, '\\\\\"');\n\n    return `\"reply_markdown\":\"${fixed}\"`;\n  }\n);\n\nlet parsed;\n\ntry {\n  parsed = JSON.parse(jsonString);\n} catch (e) {\n  throw new Error(\n    'JSON parse failed AFTER repair: ' + e.message + '\\n--- RAW ---\\n' + jsonString\n  );\n}\n\nreturn [{ json: parsed }];"
      },
      "typeVersion": 2
    },
    {
      "id": "aa8f0ef4-ac47-4533-a666-2631f5d6845f",
      "name": "Transmit Commit Card to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        -224,
        576
      ],
      "parameters": {
        "text": "=Commit approval required for {{ $json.device || 'unknown' }}",
        "channel": "={{ $json.channel }}",
        "blocksJson": "={{ JSON.stringify(\n[\n  {\n    \"type\": \"header\",\n    \"text\": {\n      \"type\": \"plain_text\",\n      \"text\": \"\ud83e\uddf0 Commit Approval Required\",\n      \"emoji\": true\n    }\n  },\n  {\n    \"type\": \"section\",\n    \"fields\": [\n      {\n        \"type\": \"mrkdwn\",\n        \"text\": \"*\ud83e\udd16 Device:*\\n\" + ($json.device || \"unknown\")\n      },\n      {\n        \"type\": \"mrkdwn\",\n        \"text\": \"*\ud83c\udff7\ufe0f Vendor:*\\n\" + ($json.vendor || \"unknown\")\n      }\n    ]\n  },\n  {\n    \"type\": \"section\",\n    \"text\": {\n      \"type\": \"mrkdwn\",\n      \"text\": \"*\ud83d\udccb Safety Comments*\\n\" + ($json.safety_reason || \"No safety reason provided.\")\n    }\n  },\n  {\n    \"type\": \"section\",\n    \"text\": {\n      \"type\": \"mrkdwn\",\n      \"text\": \"*\u270f\ufe0f Pre-check Evidence*\\n\" + ($json.precheck_evidence || \"No pre-check evidence provided.\")\n    }\n  },\n  {\n    \"type\": \"section\",\n    \"text\": {\n      \"type\": \"mrkdwn\",\n      \"text\": \"*\u2699\ufe0f Commit Payload*\\n```\\n\" + ($json.commit_payload || \"No commit payload provided.\") + \"\\n```\"\n    }\n  },\n  {\n    \"type\": \"actions\",\n    \"elements\": [\n      {\n        \"type\": \"button\",\n        \"text\": {\n          \"type\": \"plain_text\",\n          \"text\": \"\u2705 Please commit\",\n          \"emoji\": false\n        },\n        \"action_id\": \"commit_approve\",\n        \"value\": JSON.stringify({\n          \"channel\": $json.channel,\n          \"thread_ts\": $json.thread_ts,\n          \"device\": $json.device || \"unknown\",\n          \"vendor\": $json.vendor || \"unknown\",\n          \"commit_payload\": $json.commit_payload || \"\"\n        }),\n        \"style\": \"primary\"\n      },\n      {\n        \"type\": \"button\",\n        \"text\": {\n          \"type\": \"plain_text\",\n          \"text\": \"\u274c Cancel\",\n          \"emoji\": false\n        },\n        \"action_id\": \"commit_cancel\",\n        \"value\": JSON.stringify({\n          \"channel\": $json.channel,\n          \"thread_ts\": $json.thread_ts,\n          \"device\": $json.device || \"unknown\",\n          \"vendor\": $json.vendor || \"unknown\",\n          \"commit_payload\": $json.commit_payload || \"\"\n        }),\n        \"style\": \"danger\"\n      }\n    ]\n  }\n]) }}",
        "attachments": [],
        "otherOptions": {},
        "jsonParameters": true,
        "attachmentsJson": "[]"
      },
      "typeVersion": 1
    },
    {
      "id": "336856a6-a7a9-4606-8475-0fadf4117f71",
      "name": "Card Click Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -2624,
        1536
      ],
      "parameters": {
        "path": "ccb38736-1a6a-4f95-a198-4dd1d932f9db",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "0f868903-541e-4724-bc20-6ee40da10a6b",
      "name": "Analyze Slack Card Action",
      "type": "n8n-nodes-base.code",
      "position": [
        -2208,
        1536
      ],
      "parameters": {
        "jsCode": "const body = $input.first().json.body;\n\nif (!body || !body.payload) {\n  throw new Error('Missing Slack payload');\n}\n\nlet payload;\ntry {\n  payload = typeof body.payload === 'string' ? JSON.parse(body.payload) : body.payload;\n} catch (e) {\n  throw new Error('Failed to parse Slack payload: ' + e.message);\n}\n\nconst action = payload.actions?.[0];\nif (!action) {\n  throw new Error('Missing Slack action');\n}\n\nconst actionValue = action.value || '{}';\nlet parsedValue = {};\n\nif (typeof actionValue === 'string' && actionValue.startsWith('{')) {\n  try {\n    parsedValue = JSON.parse(actionValue);\n  } catch (e) {\n    console.warn('Could not parse action value as JSON:', actionValue);\n  }\n}\n\nreturn [{\n  json: {\n    action_id: action.action_id,\n    decision: action.action_id === 'commit_approve' ? 'ok' : 'cancel',\n    response_url: payload.response_url || '',\n    channel: parsedValue.channel || payload.channel?.id || payload.container?.channel_id || '',\n    thread_ts: parsedValue.thread_ts || payload.container?.thread_ts || payload.container?.message_ts || '',\n    device: parsedValue.device || 'unknown',\n    vendor: parsedValue.vendor || 'unknown',\n    commit_payload: parsedValue.commit_payload || ''\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "896829a4-b6b6-4bbb-a617-88b4ce3f5f00",
      "name": "Send Card Cancellation Request",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1488,
        1968
      ],
      "parameters": {
        "url": "={{ $('Verify Decision Approval').item.json.response_url }}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "replace_original",
              "value": "true"
            },
            {
              "name": "text",
              "value": "={{ $json.standby_message }}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e1e149d8-140f-48a3-b8be-a21f9a4477df",
      "name": "Request Card Standby Replacement",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1440,
        1552
      ],
      "parameters": {
        "url": "={{ $('Verify Decision Approval').item.json.response_url }}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "replace_original",
              "value": "true"
            },
            {
              "name": "text",
              "value": "={{ $json.standby_message }}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "16038809-0430-47dd-85df-45b22b7e94e5",
      "name": "Commit Verification AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -1056,
        1536
      ],
      "parameters": {
        "text": "=You are a Network Configuration with MCP access to our network devices.\n\nExecute the approved commit and verify it.\n\nInputs:\n- Device: {{ $('Verify Decision Approval').item.json.device }}\n- Vendor: {{ $('Verify Decision Approval').item.json.vendor }}\n- Commit payload: {{ $('Verify Decision Approval').item.json.commit_payload }}\n\nInstructions:\n- Apply write operation to the specified device using vendor-appropriate method.\n- Re-fetch post-commit state and verify intended result.\n- Return ONLY strict JSON:\n{\n  \"device\": \"...\",\n  \"vendor\": \"...\",\n  \"verification_markdown\": \"human-readable verification summary\"\n}\n\n- DON'T add any comments or else before or after the JSON.\n- Markdown must be compatible with Slack mrkdwn.\n- Do not use markdown tables.\n- If data is tabular, reformat it as bullet list, or section per item with Field: Value lines, or fenced code block for dense matrix data. Keep all original data, only change layout.\n- For diffs, use: Field: before -> after.\n- Keep lines short and readable.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "3f9b67d9-c0d9-4db4-ba7e-0e31ae98206c",
      "name": "Decode Commit JSON Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -752,
        1536
      ],
      "parameters": {
        "jsCode": "// Get input safely\nconst input = items[0].json;\n\n// If input itself is a string, parse it\nconst parsed = typeof input === 'string' ? JSON.parse(input) : input;\n\n// Normalize structure (array vs object)\nconst obj = Array.isArray(parsed) ? parsed[0] : parsed;\n\n// Validate existence\nif (!obj || !obj.output) {\n  throw new Error('Invalid input format: \"output\" field not found');\n}\n\n// Extract raw string\nconst rawOutput = obj.output;\n\n// Remove markdown fences\nconst cleaned = rawOutput\n  .replace(/^```json\\s*/, '')\n  .replace(/```$/, '')\n  .trim();\n\n// Parse inner JSON\nconst finalJson = JSON.parse(cleaned);\n\n// Return\nreturn [\n  {\n    json: finalJson\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "fedc940c-f2b0-4f72-86e6-c0ea3b1d0946",
      "name": "Read-only pyATS MCP Client",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        -1248,
        1120
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "https://your-mcp-endpoint.example/mcp",
        "includeTools": [
          "pyats_list_devices",
          "pyats_run_show_command",
          "pyats_show_running_config",
          "pyats_show_logging",
          "pyats_ping_from_network_device",
          "pyats_run_linux_command",
          "pyats_run_dynamic_test"
        ]
      },
      "typeVersion": 1.2
    },
    {
      "id": "d0970cf8-1c7e-43aa-8eb6-7c862b48333c",
      "name": "Full Access pyATS MCP Client",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        -784,
        1728
      ],
      "parameters": {
        "options": {},
        "endpointUrl": "https://your-mcp-endpoint.example/mcp"
      },
      "typeVersion": 1.2
    },
    {
      "id": "f7b9e336-d6b0-449b-9ec2-3a7f0dd781d4",
      "name": "Create Cancellation Phrase",
      "type": "n8n-nodes-base.code",
      "position": [
        -1680,
        1968
      ],
      "parameters": {
        "jsCode": "const phrases = [\n  \"\ud83d\uded1 Commit request cancelled.\",\n  \"\u2699\ufe0f Commit operation stopped.\",\n  \"\ud83e\udde0 Commit aborted.\",\n  \"\ud83d\udce1 Commit cancel signal executed.\",\n  \"\ud83d\udd0d Commit halted.\",\n  \"\ud83d\udea7 Commit cancelled.\",\n  \"\u26a1 Commit execution stopped.\",\n  \"\ud83d\udcbb Commit request terminated.\",\n  \"\ud83d\udcca Commit processing stopped.\",\n  \"\ud83e\udde9 Pending commit discarded.\",\n  \"\ud83d\udee0\ufe0f Commit cancellation finalized.\",\n  \"\ud83d\udce5 Commit input handling stopped.\",\n  \"\ud83d\udce4 Commit output cancelled.\",\n  \"\ud83e\udded Commit process exited.\",\n  \"\ud83d\udd17 Commit sequence disconnected.\",\n  \"\ud83e\uddea Commit validation stopped.\",\n  \"\ud83d\ude80 Commit aborted successfully.\",\n  \"\ud83d\ude04 Commit cancelled cleanly.\",\n  \"\ud83d\udc4d Commit cancellation complete.\",\n  \"\u2728 Commit cleared.\",\n  \"\ud83d\udd04 Commit state reset.\",\n  \"\u2699\ufe0f Commit fully stopped.\"\n];\n\n// Pick a random phrase\nconst randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];\n\n// Return it as output\nreturn [\n  {\n    json: {\n      standby_message: randomPhrase\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "b605795d-e481-4aa6-a2fb-b06a20a05941",
      "name": "Webhook Response Handler",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -2416,
        1536
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.1
    },
    {
      "id": "b076fe8c-66a4-4a7f-b83b-0eb8356e5fd2",
      "name": "Slack Mention Trigger",
      "type": "n8n-nodes-base.slackTrigger",
      "position": [
        -2576,
        928
      ],
      "parameters": {
        "options": {},
        "trigger": [
          "app_mention"
        ],
        "watchWorkspace": true
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b088a9b4-1340-4014-be2c-8c1c0f6b537c",
      "name": "Planning Memory Buffer",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        -1424,
        1120
      ],
      "parameters": {
        "sessionKey": "={{ $('Extract Slack Mention by Bot').item.json.channel }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.4
    },
    {
      "id": "7c8ec171-7221-45d3-a823-92825cd2ce33",
      "name": "Commit Memory Buffer",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        -944,
        1728
      ],
      "parameters": {
        "sessionKey": "={{ $('Analyze Slack Card Action').item.json.response_url }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.4
    },
    {
      "id": "02cd300a-aa9c-4fdb-8ecd-14fb4ddee108",
      "name": "Your Commit LLM",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        -1104,
        1728
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "4fdcb63c-1a45-4429-a9ee-e7b1c5fccbd0",
      "name": "Your Planning LLM",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        -1616,
        1120
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "63ea2b49-f731-41b3-b351-4d6fc1d01cbe",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        384
      ],
      "parameters": {
        "width": 336,
        "height": 400,
        "content": "![commit_card](https://raw.githubusercontent.com/ponchotitlan/pyATS-loves-agenticops/main/images/slack_04_config.png)"
      },
      "typeVersion": 1
    },
    {
      "id": "543f2c6b-0442-48d6-9d4a-b763eeab6ef9",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -464,
        816
      ],
      "parameters": {
        "width": 384,
        "height": 448,
        "content": "![query_example](https://raw.githubusercontent.com/ponchotitlan/pyATS-loves-agenticops/refs/heads/main/images/slack_03_interfaces.png)"
      },
      "typeVersion": 1
    },
    {
      "id": "a164c025-d6e8-431d-b4cb-22386d6ab4a1",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        1408
      ],
      "parameters": {
        "width": 416,
        "height": 480,
        "content": "![commit_done_validation](https://raw.githubusercontent.com/ponchotitlan/pyATS-loves-agenticops/main/images/slack_05_commit.png)"
      },
      "typeVersion": 1
    },
    {
      "id": "1294bb1f-e5b8-4aea-91eb-bfd4d43be4ae",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        816
      ],
      "parameters": {
        "width": 368,
        "height": 448,
        "content": "![commit_conflict](https://raw.githubusercontent.com/ponchotitlan/pyATS-loves-agenticops/main/images/slack_06_conflict.png)"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "90b3c53b-8572-4258-9362-5e86ca212363",
  "nodeGroups": [],
  "connections": {
    "Your Commit LLM": {
      "ai_languageModel": [
        [
          {
            "node": "Commit Verification AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Your Planning LLM": {
      "ai_languageModel": [
        [
          {
            "node": "Strategic Planning AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Check If Commit Mode": {
      "main": [
        [
          {
            "node": "Expand Commit Cards List",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reply in Slack Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Commit Memory Buffer": {
      "ai_memory": [
        [
          {
            "node": "Commit Verification AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Slack Mention Trigger": {
      "main": [
        [
          {
            "node": "Create Standby Message Pool",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Planning Memory Buffer": {
      "ai_memory": [
        [
          {
            "node": "Strategic Planning AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Decode Commit JSON Data": {
      "main": [
        [
          {
            "node": "Post Commit Verification to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand Commit Cards List": {
      "main": [
        [
          {
            "node": "Process Devices in Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Decision Approval": {
      "main": [
        [
          {
            "node": "Compose Commit Standby Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Cancellation Phrase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Response Handler": {
      "main": [
        [
          {
            "node": "Analyze Slack Card Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Slack Card Action": {
      "main": [
        [
          {
            "node": "Verify Decision Approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decode Planning JSON Data": {
      "main": [
        [
          {
            "node": "Check If Commit Mode",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Card Click Webhook Trigger": {
      "main": [
        [
          {
            "node": "Webhook Response Handler",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Cancellation Phrase": {
      "main": [
        [
          {
            "node": "Send Card Cancellation Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Devices in Batches": {
      "main": [
        [],
        [
          {
            "node": "Transmit Commit Card to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read-only pyATS MCP Client": {
      "ai_tool": [
        [
          {
            "node": "Strategic Planning AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Create Standby Message Pool": {
      "main": [
        [
          {
            "node": "Post Standby Message to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Strategic Planning AI Agent": {
      "main": [
        [
          {
            "node": "Decode Planning JSON Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Commit Verification AI Agent": {
      "main": [
        [
          {
            "node": "Decode Commit JSON Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Slack Mention by Bot": {
      "main": [
        [
          {
            "node": "Strategic Planning AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Full Access pyATS MCP Client": {
      "ai_tool": [
        [
          {
            "node": "Commit Verification AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Post Standby Message to Slack": {
      "main": [
        [
          {
            "node": "Extract Slack Mention by Bot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transmit Commit Card to Slack": {
      "main": [
        [
          {
            "node": "Process Devices in Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compose Commit Standby Message": {
      "main": [
        [
          {
            "node": "Request Card Standby Replacement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Card Standby Replacement": {
      "main": [
        [
          {
            "node": "Commit Verification AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}