This workflow corresponds to n8n.io template #13943 — we link there as the canonical source.
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 →
{
"id": "UGhTOwk0sz0gm8PU",
"name": "Manage AI coding sessions from Matrix with YouTrack and GitLab",
"tags": [],
"nodes": [
{
"id": "7c31b18c",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
208,
-352
],
"parameters": {
"color": 4,
"width": 540,
"height": 520,
"content": "## Manage AI coding sessions from Matrix\nA chat-ops bridge between Matrix, Claude Code,\nYouTrack, and GitLab. Your team talks to an AI\ncoding assistant from a chat room \u2014 with issue\ntracking and CI/CD visibility built in.\n\n## How it works\n1. Polls a Matrix room every 30s for messages\n2. Routes `!commands` to the matching handler\n3. Forwards messages to Claude Code via SSH\n4. Posts Claude's response back to Matrix\n5. Syncs session state with YouTrack issues\n\n## Setup steps\n1. Import and edit the **Gateway Config** node\n2. Create n8n credentials: SSH + Matrix token\n3. Set `YOUTRACK_TOKEN` and `GITLAB_TOKEN`\n4. Create the SQLite database (see description)\n5. Activate the workflow"
},
"typeVersion": 1
},
{
"id": "4ceda743",
"name": "Sticky Note 4fa9",
"type": "n8n-nodes-base.stickyNote",
"position": [
176,
224
],
"parameters": {
"width": 260,
"height": 228,
"content": "### 1. Configuration\nUser-configurable variables."
},
"typeVersion": 1
},
{
"id": "277ed962",
"name": "Sticky Note 7417",
"type": "n8n-nodes-base.stickyNote",
"position": [
464,
224
],
"parameters": {
"width": 960,
"height": 228,
"content": "### 2. Matrix polling\nPolls Matrix /sync every 30 seconds, extracts new messages, filters empty batches."
},
"typeVersion": 1
},
{
"id": "874e52f1",
"name": "Sticky Note 782c",
"type": "n8n-nodes-base.stickyNote",
"position": [
1472,
224
],
"parameters": {
"width": 500,
"height": 420,
"content": "### 3. Command routing\nParses `!commands`, routes to handlers."
},
"typeVersion": 1
},
{
"id": "8489758d",
"name": "Sticky Note c9e3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2016,
-304
],
"parameters": {
"width": 1576,
"height": 374,
"content": "### 4. Message handler\nChecks lock, reads session, resumes Claude via SSH, posts response to Matrix."
},
"typeVersion": 1
},
{
"id": "2e04a109",
"name": "Sticky Note 6e7b",
"type": "n8n-nodes-base.stickyNote",
"position": [
2016,
128
],
"parameters": {
"width": 592,
"height": 1248,
"content": "### 5. Command handlers\n**!session** current, list, done, cancel, pause, resume\n**!issue** status, info, start, verify, done, comment\n**!pipeline** status, logs, retry\n**!system** status | **!help** reference"
},
"typeVersion": 1
},
{
"id": "7c36c474",
"name": "Sticky Note dbbd",
"type": "n8n-nodes-base.stickyNote",
"position": [
2656,
496
],
"parameters": {
"width": 760,
"height": 288,
"content": "### 6. Response and session end\nPosts output to Matrix. Archives session to SQLite on done."
},
"typeVersion": 1
},
{
"id": "gateway-config",
"name": "Gateway Config",
"type": "n8n-nodes-base.set",
"position": [
208,
304
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "ssh-host",
"name": "SSH_HOST",
"type": "string",
"value": "your-server-hostname"
},
{
"id": "e402df7c",
"name": "MATRIX_HOMESERVER",
"type": "string",
"value": "https://your-matrix-server.com"
},
{
"id": "7a2638f9",
"name": "MATRIX_ROOM_ID",
"type": "string",
"value": "!yourRoomId:your-matrix-server.com"
},
{
"id": "f020d2f8",
"name": "MATRIX_BOT_USER",
"type": "string",
"value": "@bot:your-matrix-server.com"
},
{
"id": "dc840315",
"name": "YOUTRACK_URL",
"type": "string",
"value": "https://your-youtrack-instance.com"
},
{
"id": "0e88ef8d",
"name": "GITLAB_URL",
"type": "string",
"value": "https://your-gitlab-instance.com"
},
{
"id": "0ef605c8",
"name": "CLAUDE_PROJECT_PATH",
"type": "string",
"value": "/home/user/your-project"
},
{
"id": "e0482d36",
"name": "CLAUDE_BINARY",
"type": "string",
"value": "/home/user/.local/bin/claude"
},
{
"id": "690b2fc6",
"name": "DB_PATH",
"type": "string",
"value": "/home/user/your-project/claude-context/gateway.db"
},
{
"id": "e8bcc6ff",
"name": "CONTEXT_DIR",
"type": "string",
"value": "/home/user/your-project/claude-context"
},
{
"id": "393a34b8",
"name": "ISSUE_PREFIX",
"type": "string",
"value": "PROJ"
},
{
"id": "8d185b9c",
"name": "COOLDOWN_TTL",
"type": "string",
"value": "30"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "poll-trigger",
"name": "Poll Every 30s",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
512,
304
],
"parameters": {
"rule": {
"interval": [
{
"field": "seconds"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "get-sync-token",
"name": "Get Sync Token",
"type": "n8n-nodes-base.code",
"position": [
704,
304
],
"parameters": {
"jsCode": "const staticData = $getWorkflowStaticData('global');\nconst sinceToken = staticData.matrixSinceToken || '';\nreturn [{ json: { sinceToken, ...$('Gateway Config').first().json } }];"
},
"typeVersion": 2
},
{
"id": "poll-matrix",
"name": "Poll Matrix Sync",
"type": "n8n-nodes-base.httpRequest",
"position": [
912,
304
],
"parameters": {
"url": "={{ $json.MATRIX_HOMESERVER }}/_matrix/client/v3/sync?timeout=0&filter={\"room\":{\"rooms\":[\"{{ $json.MATRIX_ROOM_ID }}\"],\"timeline\":{\"limit\":10}}}{{ $json.sinceToken ? '&since=' + $json.sinceToken : '' }}",
"options": {
"timeout": 15000
},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "extract-messages",
"name": "Extract Messages",
"type": "n8n-nodes-base.code",
"position": [
1104,
304
],
"parameters": {
"jsCode": "const config = $('Gateway Config').first().json;\nconst syncData = $input.first().json;\nconst staticData = $getWorkflowStaticData('global');\n\n// Save sync token for next poll\nif (syncData.next_batch) staticData.matrixSinceToken = syncData.next_batch;\n\n// Extract messages from timeline\nconst roomId = config.MATRIX_ROOM_ID;\nconst rooms = syncData.rooms?.join || {};\nconst room = rooms[roomId] || syncData.rooms?.join?.[Object.keys(syncData.rooms?.join || {})[0]];\nconst events = room?.timeline?.events || [];\n\nconst lastTs = staticData.lastProcessedTimestamp || 0;\nconst botUser = config.MATRIX_BOT_USER;\n\nconst messages = events\n .filter(e => e.type === 'm.room.message'\n && e.sender !== botUser\n && e.origin_server_ts > lastTs)\n .map(e => ({\n messageText: e.content?.body || '',\n sender: e.sender,\n timestamp: e.origin_server_ts\n }));\n\nif (messages.length > 0) {\n staticData.lastProcessedTimestamp = Math.max(...messages.map(m => m.timestamp));\n}\n\nreturn messages.length > 0\n ? [{ json: { ...messages[0], ...config } }]\n : [];"
},
"typeVersion": 2
},
{
"id": "has-messages",
"name": "Has Messages?",
"type": "n8n-nodes-base.if",
"position": [
1312,
304
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c8b1ae50",
"operator": {
"type": "string",
"operation": "isNotEmpty",
"singleValue": true
},
"leftValue": "={{ $json.messageText }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "detect-command",
"name": "Detect Command",
"type": "n8n-nodes-base.code",
"position": [
1504,
304
],
"parameters": {
"jsCode": "const config = $('Gateway Config').first().json;\nconst messageText = $input.first().json.messageText || '';\nconst trimmed = messageText.trim();\nconst lower = trimmed.toLowerCase();\nconst base64Message = Buffer.from(messageText).toString('base64');\n\n// Check for PREFIX: message format (e.g. PROJ-4: do something)\nconst prefixMatch = trimmed.match(/^([A-Z]+-\\d+):\\s*([\\s\\S]*)$/);\nif (prefixMatch) {\n const prefix = prefixMatch[1];\n const strippedText = prefixMatch[2].trim();\n if (!strippedText) {\n return [{ json: { ...config, command: 'empty', matrixBody: JSON.stringify({ msgtype: 'm.notice', body: 'Message body is empty. Usage: ' + prefix + ': <your message>' }) } }];\n }\n const base64Stripped = Buffer.from(strippedText).toString('base64');\n return [{ json: { ...config, command: 'message', sub: '', args: [], messageText: strippedText, base64Message: base64Stripped, prefix } }];\n}\n\nif (!lower.startsWith('!')) {\n if (!trimmed) return [{ json: { ...config, command: 'empty', matrixBody: JSON.stringify({ msgtype: 'm.notice', body: 'Message cannot be empty.' }) } }];\n return [{ json: { ...config, command: 'message', sub: '', args: [], messageText, base64Message, prefix: '' } }];\n}\n\nconst lowerParts = lower.slice(1).split(/\\s+/);\nconst origParts = trimmed.slice(1).split(/\\s+/);\nconst command = lowerParts[0] || '';\nconst sub = lowerParts[1] || '';\nconst args = origParts.slice(2);\n\n// Legacy aliases\nconst resolved = { done: 'session', cancel: 'session', status: 'session' }[command] || command;\nconst resolvedSub = { done: 'done', cancel: 'cancel', status: 'current' }[command] || sub;\n\nreturn [{ json: { ...config, command: resolved, sub: resolvedSub, args, messageText, base64Message, prefix: '' } }];"
},
"typeVersion": 2
},
{
"id": "command-router",
"name": "Command Router",
"type": "n8n-nodes-base.switch",
"position": [
1760,
304
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "message",
"conditions": {
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.command }}",
"rightValue": "message"
}
]
},
"renameOutput": true
},
{
"outputKey": "session",
"conditions": {
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.command }}",
"rightValue": "session"
}
]
},
"renameOutput": true
},
{
"outputKey": "help",
"conditions": {
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.command }}",
"rightValue": "help"
}
]
},
"renameOutput": true
},
{
"outputKey": "issue",
"conditions": {
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.command }}",
"rightValue": "issue"
}
]
},
"renameOutput": true
},
{
"outputKey": "pipeline",
"conditions": {
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.command }}",
"rightValue": "pipeline"
}
]
},
"renameOutput": true
},
{
"outputKey": "system",
"conditions": {
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.command }}",
"rightValue": "system"
}
]
},
"renameOutput": true
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"typeVersion": 3
},
{
"id": "check-lock",
"name": "Check Lock",
"type": "n8n-nodes-base.ssh",
"position": [
2064,
-112
],
"parameters": {
"command": "={{ 'GW=\"' + $json.CONTEXT_DIR + '\" && if [ -f $GW/gateway.lock ] && [ $(( $(date +%s) - $(stat -c %Y $GW/gateway.lock) )) -lt 600 ]; then echo LOCKED; else rm -f $GW/gateway.lock && echo FREE; fi' }}",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "is-locked",
"name": "Is Locked?",
"type": "n8n-nodes-base.if",
"position": [
2272,
-112
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "02be178d",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.stdout.trim() }}",
"rightValue": "LOCKED"
}
]
}
},
"typeVersion": 2
},
{
"id": "post-busy",
"name": "Post Busy Notice",
"type": "n8n-nodes-base.httpRequest",
"position": [
2608,
-240
],
"parameters": {
"url": "={{ $('Gateway Config').first().json.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/{{ $('Gateway Config').first().json.MATRIX_ROOM_ID }}/send/m.room.message/busy-{{ Date.now() }}",
"method": "PUT",
"options": {},
"jsonBody": "={{ JSON.stringify({ msgtype: 'm.notice', body: 'Claude is busy processing a previous message. Your message has been queued.' }) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "read-session",
"name": "Read Session & Acquire Lock",
"type": "n8n-nodes-base.ssh",
"position": [
2496,
-112
],
"parameters": {
"command": "={{ 'DB=\"' + $('Gateway Config').first().json.DB_PATH + '\" && GW=\"' + $('Gateway Config').first().json.CONTEXT_DIR + '\" && echo locked > $GW/gateway.lock && sqlite3 $DB \"SELECT json_object(\\'sessionId\\',session_id,\\'issueId\\',issue_id,\\'issueTitle\\',issue_title) FROM sessions WHERE is_current=1 LIMIT 1;\" 2>/dev/null' }}",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "resume-claude",
"name": "Resume Claude Session",
"type": "n8n-nodes-base.ssh",
"position": [
2720,
-112
],
"parameters": {
"command": "=CLAUDE_BIN=\"{{ $(\"Gateway Config\").first().json.CLAUDE_BINARY }}\"\nPROJECT=\"{{ $(\"Gateway Config\").first().json.CLAUDE_PROJECT_PATH }}\"\nSESSION_RAW=\"{{ $(\"Read Session & Acquire Lock\").first().json.stdout || '' }}\"\nMSG_B64=\"{{ $(\"Detect Command\").first().json.base64Message }}\"\n\nSESSION_RAW=$(echo \"$SESSION_RAW\" | tr -d '\\n')\n[ -z \"$SESSION_RAW\" ] && echo \"NO_SESSION\" && exit 0\n\nSID=$(echo \"$SESSION_RAW\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('sessionId',''))\" 2>/dev/null)\n[ -z \"$SID\" ] && echo \"NO_SESSION\" && exit 0\n\nunset CLAUDECODE\ncd \"$PROJECT\"\ntimeout 300 \"$CLAUDE_BIN\" -r \"$SID\" -p \"$(printf '%s' \"$MSG_B64\" | base64 -d)\" --output-format json --dangerously-skip-permissions 2>&1",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "parse-response",
"name": "Parse Claude Response",
"type": "n8n-nodes-base.code",
"position": [
2944,
-112
],
"parameters": {
"jsCode": "const config = $('Gateway Config').first().json;\nconst stdout = $input.first().json.stdout || '';\nconst stderr = $input.first().json.stderr || '';\nconst output = (stdout.trim() || stderr.trim());\n\nif (output === 'NO_SESSION') {\n const body = 'No active Claude session. Start one via YouTrack or !issue start <id>.';\n return [{ json: { matrixBody: JSON.stringify({ msgtype: 'm.notice', body }), ...config } }];\n}\n\nlet result;\ntry {\n const parsed = JSON.parse(output);\n result = parsed.result || output.substring(0, 4000);\n} catch(e) {\n result = output.substring(0, 4000) || 'No response from Claude.';\n}\n\nconst body = result;\nconst matrixBody = JSON.stringify({ msgtype: 'm.text', body });\nreturn [{ json: { matrixBody, ...config } }];"
},
"typeVersion": 2
},
{
"id": "post-response",
"name": "Post Response to Matrix",
"type": "n8n-nodes-base.httpRequest",
"position": [
3152,
-112
],
"parameters": {
"url": "={{ $json.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/{{ $json.MATRIX_ROOM_ID }}/send/m.room.message/resp-{{ Date.now() }}",
"method": "PUT",
"options": {},
"jsonBody": "={{ $json.matrixBody }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "release-lock",
"name": "Release Lock",
"type": "n8n-nodes-base.ssh",
"position": [
3376,
-112
],
"parameters": {
"command": "={{ 'rm -f \"' + $('Gateway Config').first().json.CONTEXT_DIR + '/gateway.lock\"' }}",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "handle-session",
"name": "Handle Session Command",
"type": "n8n-nodes-base.ssh",
"position": [
2064,
272
],
"parameters": {
"command": "=SUB=\"{{ $(\"Detect Command\").first().json.sub || 'current' }}\"\nRAW_ARG0=\"{{ $(\"Detect Command\").first().json.args[0] || '' }}\"\nDB=\"{{ $(\"Gateway Config\").first().json.DB_PATH }}\"\nGW=\"{{ $(\"Gateway Config\").first().json.CONTEXT_DIR }}\"\n\nARG0=$(echo \"$RAW_ARG0\" | tr '[:lower:]' '[:upper:]')\n\ncase \"$SUB\" in\n current)\n ROW=$(sqlite3 \"$DB\" \"SELECT json_object('issue_id',issue_id,'session_id',session_id,'started_at',started_at,'message_count',message_count,'paused',paused) FROM sessions WHERE is_current=1 LIMIT 1;\")\n echo \"SESSION:${ROW:-NO_SESSION}\"\n ;;\n list)\n sqlite3 \"$DB\" \"SELECT json_group_array(json_object('issue_id',issue_id,'started_at',started_at,'message_count',message_count,'paused',paused,'is_current',is_current)) FROM sessions ORDER BY is_current DESC;\" 2>/dev/null\n ;;\n done)\n if [ -n \"$ARG0\" ]; then W=\"issue_id='$ARG0'\"; else W=\"is_current=1\"; fi\n sqlite3 \"$DB\" \"SELECT json_object('session_id',session_id,'issue_id',issue_id,'issue_title',issue_title) FROM sessions WHERE $W LIMIT 1;\" 2>/dev/null\n ;;\n cancel)\n if [ -n \"$ARG0\" ]; then W=\"issue_id='$ARG0'\"; else W=\"is_current=1\"; fi\n ISSUE=$(sqlite3 \"$DB\" \"SELECT issue_id FROM sessions WHERE $W LIMIT 1;\")\n if [ -n \"$ISSUE\" ]; then\n pkill -f claude 2>/dev/null\n rm -f \"$GW/gateway.lock\"\n sqlite3 \"$DB\" \"DELETE FROM sessions WHERE issue_id='$ISSUE'; DELETE FROM queue WHERE issue_id='$ISSUE';\"\n echo \"CANCELLED:$ISSUE\"\n else\n echo \"NO_SESSION\"\n fi\n ;;\n pause)\n if [ -n \"$ARG0\" ]; then W=\"issue_id='$ARG0'\"; else W=\"is_current=1\"; fi\n ISSUE=$(sqlite3 \"$DB\" \"SELECT issue_id FROM sessions WHERE $W LIMIT 1;\")\n if [ -n \"$ISSUE\" ]; then\n sqlite3 \"$DB\" \"UPDATE sessions SET paused=1 WHERE issue_id='$ISSUE';\"\n echo \"PAUSED:$ISSUE\"\n else\n echo \"NO_SESSION\"\n fi\n ;;\n resume)\n if [ -n \"$ARG0\" ]; then W=\"issue_id='$ARG0'\"; else W=\"is_current=1\"; fi\n ISSUE=$(sqlite3 \"$DB\" \"SELECT issue_id FROM sessions WHERE $W LIMIT 1;\")\n if [ -n \"$ISSUE\" ]; then\n sqlite3 \"$DB\" \"UPDATE sessions SET paused=0 WHERE issue_id='$ISSUE';\"\n echo \"RESUMED:$ISSUE\"\n else\n echo \"NO_SESSION\"\n fi\n ;;\n *) echo \"UNKNOWN_SUB:$SUB\" ;;\nesac",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "format-session",
"name": "Format Session Response",
"type": "n8n-nodes-base.code",
"position": [
2272,
272
],
"parameters": {
"jsCode": "const config = $('Gateway Config').first().json;\nconst stdout = $input.first().json.stdout || '';\nconst sub = $('Detect Command').first().json.sub || 'current';\nconst output = stdout.trim();\nlet body = '';\n\nif (sub === 'current') {\n const raw = output.replace('SESSION:', '');\n if (!raw || raw === 'NO_SESSION') body = 'No active session.';\n else {\n try {\n const s = JSON.parse(raw);\n body = 'Current session:\\nIssue: ' + s.issue_id + '\\nMessages: ' + s.message_count + '\\nPaused: ' + (s.paused ? 'Yes' : 'No');\n } catch(e) { body = 'Error: ' + raw.substring(0, 200); }\n }\n} else if (sub === 'list') {\n try {\n const rows = JSON.parse(output || '[]');\n body = rows.length ? rows.map((s, i) => (i+1) + '. ' + s.issue_id + (s.is_current ? ' (current)' : '')).join('\\n') : 'No active sessions.';\n } catch(e) { body = 'Error: ' + output.substring(0, 200); }\n} else if (sub === 'done') {\n if (!output) body = 'No active session to end.';\n else {\n try { const s = JSON.parse(output); body = s.session_id ? 'Ending session for ' + s.issue_id + '...' : 'No active session.'; } catch(e) { body = 'No active session.'; }\n }\n} else if (sub === 'cancel') {\n body = output.startsWith('CANCELLED:') ? 'Session ' + output.split(':')[1] + ' cancelled.' : output === 'NO_SESSION' ? 'No session to cancel.' : output;\n} else if (sub === 'pause') {\n body = output.startsWith('PAUSED:') ? 'Session ' + output.split(':')[1] + ' paused.' : output === 'NO_SESSION' ? 'No session to pause.' : output;\n} else if (sub === 'resume') {\n body = output.startsWith('RESUMED:') ? 'Session ' + output.split(':')[1] + ' resumed.' : output === 'NO_SESSION' ? 'No session to resume.' : output;\n} else { body = 'Unknown: ' + sub; }\n\nconst matrixBody = JSON.stringify({ msgtype: 'm.notice', body });\nreturn [{ json: { matrixBody, ...config } }];"
},
"typeVersion": 2
},
{
"id": "handle-issue",
"name": "Handle Issue Command",
"type": "n8n-nodes-base.ssh",
"position": [
2064,
480
],
"parameters": {
"command": "=SUB=\"{{ $(\"Detect Command\").first().json.sub || 'status' }}\"\nRAW_ARG0=\"{{ $(\"Detect Command\").first().json.args[0] || '' }}\"\nYT_URL=\"{{ $(\"Gateway Config\").first().json.YOUTRACK_URL }}\"\n\nARG0=$(echo \"$RAW_ARG0\" | tr '[:lower:]' '[:upper:]')\n\ncase \"$SUB\" in\n status)\n curl -s --max-time 10 \\\n -H \"Authorization: Bearer $YT_TOKEN\" \\\n \"$YT_URL/api/issues?query=State:+%7BIn+Progress%7D&fields=idReadable,summary,customFields(name,value(name))\" 2>&1\n ;;\n info)\n [ -z \"$ARG0\" ] && echo \"USAGE: !issue info <id>\" && exit 0\n curl -s --max-time 10 \\\n -H \"Authorization: Bearer $YT_TOKEN\" \\\n \"$YT_URL/api/issues/$ARG0?fields=idReadable,summary,description,customFields(name,value(name))\" 2>&1\n ;;\n *) echo \"Available: !issue status, !issue info <id>\" ;;\nesac",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "format-issue",
"name": "Format Issue Response",
"type": "n8n-nodes-base.code",
"position": [
2272,
432
],
"parameters": {
"jsCode": "const config = $('Gateway Config').first().json;\nconst stdout = $input.first().json.stdout || '';\nconst output = stdout.trim();\nlet body = '';\n\ntry {\n const data = JSON.parse(output);\n if (Array.isArray(data)) {\n body = data.length === 0 ? 'No issues in progress.' : data.map(i => i.idReadable + ': ' + (i.summary || '\u2014')).join('\\n');\n } else if (data.idReadable) {\n body = data.idReadable + ': ' + (data.summary || '\u2014') + '\\n' + (data.description || 'No description').substring(0, 500);\n } else { body = output.substring(0, 1000); }\n} catch(e) { body = output || 'No response from YouTrack.'; }\n\nconst matrixBody = JSON.stringify({ msgtype: 'm.notice', body });\nreturn [{ json: { matrixBody, ...config } }];"
},
"typeVersion": 2
},
{
"id": "handle-pipeline",
"name": "Handle Pipeline Command",
"type": "n8n-nodes-base.ssh",
"position": [
2064,
640
],
"parameters": {
"command": "=SUB=\"{{ $(\"Detect Command\").first().json.sub || 'status' }}\"\nGL_URL=\"{{ $(\"Gateway Config\").first().json.GITLAB_URL }}\"\n\ncase \"$SUB\" in\n status)\n curl -s --max-time 10 \\\n -H \"PRIVATE-TOKEN: $GL_TOKEN\" \\\n \"$GL_URL/api/v4/projects?membership=true&simple=true\" | \\\n python3 -c \"import sys,json; projects=json.load(sys.stdin); [print(p['name'] + ': ' + p.get('default_branch','main')) for p in projects[:10]]\" 2>&1\n ;;\n *) echo \"Available: !pipeline status\" ;;\nesac",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "format-pipeline",
"name": "Format Pipeline Response",
"type": "n8n-nodes-base.code",
"position": [
2272,
592
],
"parameters": {
"jsCode": "const config = $('Gateway Config').first().json;\nconst output = ($input.first().json.stdout || '').trim();\nconst body = output || 'No pipeline data available.';\nconst matrixBody = JSON.stringify({ msgtype: 'm.notice', body });\nreturn [{ json: { matrixBody, ...config } }];"
},
"typeVersion": 2
},
{
"id": "handle-help",
"name": "Handle Help",
"type": "n8n-nodes-base.code",
"position": [
2064,
832
],
"parameters": {
"jsCode": "const config = $('Gateway Config').first().json;\nconst body = 'Claude Gateway Commands:\\n\\n'\n + '!session current \u2014 Show current session\\n'\n + '!session list \u2014 List all sessions\\n'\n + '!session done \u2014 End current session\\n'\n + '!session cancel \u2014 Cancel session (no summary)\\n'\n + '!session pause \u2014 Pause message delivery\\n'\n + '!session resume \u2014 Resume paused session\\n\\n'\n + '!issue status \u2014 In-progress issues\\n'\n + '!issue info <id> \u2014 Issue details\\n\\n'\n + '!pipeline status \u2014 Pipeline overview\\n\\n'\n + '!system status \u2014 Server load & memory\\n\\n'\n + 'Prefix routing: PROJ-4: your message';\nconst matrixBody = JSON.stringify({ msgtype: 'm.notice', body });\nreturn [{ json: { matrixBody, ...config } }];"
},
"typeVersion": 2
},
{
"id": "handle-system",
"name": "Handle System Command",
"type": "n8n-nodes-base.ssh",
"position": [
2064,
976
],
"parameters": {
"command": "LOAD=$(uptime | sed 's/.*load average: //') && MEM=$(free -h | awk '/Mem:/{print $3\"/\"$2}') && PROCS=$(pgrep -c -f claude 2>/dev/null || echo 0) && echo \"Load: $LOAD\" && echo \"Memory: $MEM\" && echo \"Claude processes: $PROCS\"",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "format-system",
"name": "Format System Response",
"type": "n8n-nodes-base.code",
"position": [
2272,
912
],
"parameters": {
"jsCode": "const config = $('Gateway Config').first().json;\nconst output = ($input.first().json.stdout || '').trim();\nconst body = output || 'No system data.';\nconst matrixBody = JSON.stringify({ msgtype: 'm.notice', body });\nreturn [{ json: { matrixBody, ...config } }];"
},
"typeVersion": 2
},
{
"id": "handle-unknown",
"name": "Handle Unknown Command",
"type": "n8n-nodes-base.code",
"position": [
2064,
1168
],
"parameters": {
"jsCode": "const config = $('Gateway Config').first().json;\nif ($json.matrixBody) return [{ json: { matrixBody: $json.matrixBody, ...config } }];\nconst body = 'Unknown command: !' + ($json.command || 'unknown') + '\\nType !help to see available commands.';\nconst matrixBody = JSON.stringify({ msgtype: 'm.notice', body });\nreturn [{ json: { matrixBody, ...config } }];"
},
"typeVersion": 2
},
{
"id": "post-cmd-response",
"name": "Post Command Response",
"type": "n8n-nodes-base.httpRequest",
"position": [
2496,
592
],
"parameters": {
"url": "={{ $json.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/{{ $json.MATRIX_ROOM_ID }}/send/m.room.message/cmd-{{ Date.now() }}",
"method": "PUT",
"options": {},
"jsonBody": "={{ $json.matrixBody }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "session-end-check",
"name": "Is Session Done?",
"type": "n8n-nodes-base.if",
"position": [
2720,
592
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c70feebe",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ $('Handle Session Command').first().json.stdout || '' }}",
"rightValue": "session_id"
}
]
}
},
"typeVersion": 2
},
{
"id": "cleanup-session",
"name": "Clean Up & End Session",
"type": "n8n-nodes-base.ssh",
"position": [
2944,
592
],
"parameters": {
"command": "=DB=\"{{ $(\"Gateway Config\").first().json.DB_PATH }}\"\nGW=\"{{ $(\"Gateway Config\").first().json.CONTEXT_DIR }}\"\nPROJECT=\"{{ $(\"Gateway Config\").first().json.CLAUDE_PROJECT_PATH }}\"\nCLAUDE_BIN=\"{{ $(\"Gateway Config\").first().json.CLAUDE_BINARY }}\"\nSESSION_OUT=\"{{ $(\"Handle Session Command\").first().json.stdout || '' }}\"\n\nSESSION_OUT=$(echo \"$SESSION_OUT\" | tr -d '\\n')\n[ -z \"$SESSION_OUT\" ] && echo \"NO_SESSION\" && exit 0\n\nSID=$(echo \"$SESSION_OUT\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('session_id',''))\" 2>/dev/null)\nISSUE=$(echo \"$SESSION_OUT\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('issue_id',''))\" 2>/dev/null)\n[ -z \"$SID\" ] && echo \"NO_SESSION\" && exit 0\n\n# Get summary from Claude\nunset CLAUDECODE\ncd \"$PROJECT\"\ntimeout 60 \"$CLAUDE_BIN\" -r \"$SID\" -p \"Summarize what you worked on in 2-3 sentences\" --output-format json --dangerously-skip-permissions 2>&1\n\n# Archive and clean up\nsqlite3 \"$DB\" \"INSERT INTO session_log (issue_id,issue_title,session_id,started_at,message_count,outcome) SELECT issue_id,issue_title,session_id,started_at,message_count,'done' FROM sessions WHERE issue_id='$ISSUE'; DELETE FROM sessions WHERE issue_id='$ISSUE'; DELETE FROM queue WHERE issue_id='$ISSUE';\"\nrm -f \"$GW/gateway.lock\"\ntouch \"$GW/gateway.cooldown.$ISSUE\"\necho \"SESSION_ENDED:$ISSUE\"",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "post-session-ended",
"name": "Post Session Ended",
"type": "n8n-nodes-base.httpRequest",
"position": [
3152,
592
],
"parameters": {
"url": "={{ $('Gateway Config').first().json.MATRIX_HOMESERVER }}/_matrix/client/v3/rooms/{{ $('Gateway Config').first().json.MATRIX_ROOM_ID }}/send/m.room.message/end-{{ Date.now() }}",
"method": "PUT",
"options": {},
"jsonBody": "={{ JSON.stringify({ msgtype: 'm.notice', body: 'Session ended. Summary posted to YouTrack.' }) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "cred-sticky",
"name": "Sticky Note Credentials",
"type": "n8n-nodes-base.stickyNote",
"position": [
208,
800
],
"parameters": {
"color": 6,
"width": 520,
"height": 508,
"content": "### Credentials Setup\n\n**Do NOT store API tokens in workflow nodes.**\nUse n8n's credential system instead:\n\n1. **Matrix Bot Token** \u2014 HTTP Header Auth\n `Authorization: Bearer <token>`\n\n2. **SSH Private Key** \u2014 SSH credential\n for your Claude Code server\n\n**API tokens for YouTrack & GitLab:**\nSSH command nodes reference `$YT_TOKEN` and\n`$GL_TOKEN` environment variables. Set these\nin `~/.bashrc` on the remote server:\n```\nexport YT_TOKEN=perm-YOUR-TOKEN\nexport GL_TOKEN=glpat-YOUR-TOKEN\n```\n\nThis keeps tokens out of the workflow JSON\nand uses the server's environment instead."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false,
"executionOrder": "v1",
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "none"
},
"versionId": "929c4f9a-8b21-4f30-97d1-7b2f6fc1fdd4",
"connections": {
"Check Lock": {
"main": [
[
{
"node": "Is Locked?",
"type": "main",
"index": 0
}
]
]
},
"Is Locked?": {
"main": [
[
{
"node": "Post Busy Notice",
"type": "main",
"index": 0
}
],
[
{
"node": "Read Session & Acquire Lock",
"type": "main",
"index": 0
}
]
]
},
"Handle Help": {
"main": [
[
{
"node": "Post Command Response",
"type": "main",
"index": 0
}
]
]
},
"Has Messages?": {
"main": [
[
{
"node": "Detect Command",
"type": "main",
"index": 0
}
]
]
},
"Command Router": {
"main": [
[
{
"node": "Check Lock",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Session Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Help",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Issue Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Pipeline Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle System Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Unknown Command",
"type": "main",
"index": 0
}
]
]
},
"Detect Command": {
"main": [
[
{
"node": "Command Router",
"type": "main",
"index": 0
}
]
]
},
"Get Sync Token": {
"main": [
[
{
"node": "Poll Matrix Sync",
"type": "main",
"index": 0
}
]
]
},
"Poll Every 30s": {
"main": [
[
{
"node": "Get Sync Token",
"type": "main",
"index": 0
}
]
]
},
"Extract Messages": {
"main": [
[
{
"node": "Has Messages?",
"type": "main",
"index": 0
}
]
]
},
"Is Session Done?": {
"main": [
[
{
"node": "Clean Up & End Session",
"type": "main",
"index": 0
}
]
]
},
"Poll Matrix Sync": {
"main": [
[
{
"node": "Extract Messages",
"type": "main",
"index": 0
}
]
]
},
"Handle Issue Command": {
"main": [
[
{
"node": "Format Issue Response",
"type": "main",
"index": 0
}
]
]
},
"Format Issue Response": {
"main": [
[
{
"node": "Post Command Response",
"type": "main",
"index": 0
}
]
]
},
"Handle System Command": {
"main": [
[
{
"node": "Format System Response",
"type": "main",
"index": 0
}
]
]
},
"Parse Claude Response": {
"main": [
[
{
"node": "Post Response to Matrix",
"type": "main",
"index": 0
}
]
]
},
"Post Command Response": {
"main": [
[
{
"node": "Is Session Done?",
"type": "main",
"index": 0
}
]
]
},
"Resume Claude Session": {
"main": [
[
{
"node": "Parse Claude Response",
"type": "main",
"index": 0
}
]
]
},
"Clean Up & End Session": {
"main": [
[
{
"node": "Post Session Ended",
"type": "main",
"index": 0
}
]
]
},
"Format System Response": {
"main": [
[
{
"node": "Post Command Response",
"type": "main",
"index": 0
}
]
]
},
"Handle Session Command": {
"main": [
[
{
"node": "Format Session Response",
"type": "main",
"index": 0
}
]
]
},
"Handle Unknown Command": {
"main": [
[
{
"node": "Post Command Response",
"type": "main",
"index": 0
}
]
]
},
"Format Session Response": {
"main": [
[
{
"node": "Post Command Response",
"type": "main",
"index": 0
}
]
]
},
"Handle Pipeline Command": {
"main": [
[
{
"node": "Format Pipeline Response",
"type": "main",
"index": 0
}
]
]
},
"Post Response to Matrix": {
"main": [
[
{
"node": "Release Lock",
"type": "main",
"index": 0
}
]
]
},
"Format Pipeline Response": {
"main": [
[
{
"node": "Post Command Response",
"type": "main",
"index": 0
}
]
]
},
"Read Session & Acquire Lock": {
"main": [
[
{
"node": "Resume Claude Session",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpHeaderAuthsshPrivateKey
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Development teams using Claude Code who want a chat-ops interface for project management. Instead of SSH-ing into a server to run Claude, your whole team interacts with it through a Matrix chat room — with issue tracking and CI/CD pipeline visibility built in. Polls a Matrix…
Source: https://n8n.io/workflows/13943/ — 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.
Automates daily CVE-driven scanning against bug bounty scopes. It fetches bug-bounty domains, pulls newly published Project Discovery templates, converts them to Nuclei rules, runs targeted scans, and
YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.
Instead of providing a routine check, it focuses on significant movements by: Sending a Slack alert only if a query crosses a defined movement threshold. Emailing a structured report with the Top 25 i
Looking for a way to track GitHub bounty issues automatically and get notified in real time? This GitHub Bounty Tracker workflow monitors repositories for issues labeled 💎 Bounty, logs them in Google
This workflow automatically sends a beautifully designed HTML newsletter every Sunday at 8 AM, featuring products currently on sale from your Algolia-powered e-commerce store.