AutomationFlowsSlack & Telegram › Execute Command

Execute Command

Execute_Command. Uses executeWorkflowTrigger, postgres, discord, httpRequest. Event-driven trigger; 47 nodes.

Event trigger★★★★★ complexity47 nodesExecute Workflow TriggerPostgresDiscordHTTP Request
Slack & Telegram Trigger: Event Nodes: 47 Complexity: ★★★★★ Added:

This workflow follows the Discord → 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 →

Download .json
{
  "updatedAt": "2025-12-24T10:26:39.506Z",
  "createdAt": "2025-12-23T09:30:26.164Z",
  "id": "ZwuxCuNFykFxgD3e",
  "name": "Execute_Command",
  "description": null,
  "active": false,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -1584,
        9616
      ],
      "id": "de6befd4-4882-4e2c-97b1-62f920df6d66",
      "name": "WhenExecutedByAnotherWorkflow"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "command",
              "name": "ctx.command.name",
              "value": "={{ $json.ctx.event.clean_text.trim().split(/\\s+/)[0] }}",
              "type": "string"
            },
            {
              "id": "args",
              "name": "ctx.command.args",
              "value": "={{ $json.ctx.event.clean_text.trim().split(/\\s+/).slice(1) }}",
              "type": "array"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1360,
        9616
      ],
      "id": "c204a8a7-abe4-45a8-ac75-2d842f69bbd3",
      "name": "ParseCommandAndArgs"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "get",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "33cb86c7-ccaf-4e61-bc6a-e8ed32676451"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "get"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "set",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "2150078e-a4bb-4872-bfdb-36245afa5701"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "set"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "recent",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "a807c42d-404f-419d-8129-d7c71ed6051c"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "recent"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "delete",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "d8a3f1e9-2c4b-4d7e-9a1f-3e5c7b9d1a2b"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "delete"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "stats",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "fe31fc91-b7dc-4554-97bf-8024d8f6af25"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "stats"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "status",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "5094be05-8bc0-4702-a6da-ecbd5913c93f"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "status"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "ping",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "9e2a0d1b-6254-4b67-a743-3e5b2367a01c"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "ping"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "generate",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "generate-cmd-id"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "generate"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "pulse",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "pulse-cmd-id"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "pulse"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "help",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "c42b06ed-ca87-489d-a4d8-d2e861db2239"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "help"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.ctx.command.name }}",
                    "rightValue": "modules",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "modules-cmd-id"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "modules"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        -1136,
        9504
      ],
      "id": "82eda145-e9cb-43da-a242-e7bf654ca38e",
      "name": "Switch"
    },
    {
      "parameters": {
        "jsCode": "// Handle ping command\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\n\nreturn {\n  ctx: {\n    ...ctx,\n    response: {\n      content: '\ud83c\udfd3 pong'\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        10000
      ],
      "id": "a07e31ae-ae03-4838-b91e-13e3c2f52c86",
      "name": "HandlePing"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO config (key, value, updated_at)\nVALUES ($1, $2, NOW())\nON CONFLICT (key) DO UPDATE\nSET value = EXCLUDED.value, updated_at = NOW()\nRETURNING key, value",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        -464,
        9040
      ],
      "id": "2296ca6d-1789-4986-a245-f2f16e88a92f",
      "name": "QuerySetConfig",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  (SELECT COUNT(*) FROM projections WHERE projection_type = 'activity' AND status IN ('auto_confirmed', 'confirmed') AND (data->>'timestamp')::timestamptz >= CURRENT_DATE) as activities_today,\n  (SELECT COUNT(*) FROM projections WHERE projection_type = 'note' AND status IN ('auto_confirmed', 'confirmed') AND (data->>'timestamp')::timestamptz >= DATE_TRUNC('week', CURRENT_DATE)) as notes_this_week,\n  (SELECT COUNT(*) FROM events WHERE event_type = 'discord_message') as total_events,\n  (SELECT MAX(received_at) FROM events) as last_activity",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        -464,
        9616
      ],
      "id": "c1663df9-a24b-4805-aed0-9946a2e5a9cc",
      "name": "QueryStats",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH activities_data AS (\n  SELECT \n    'activity' as type,\n    p.id,\n    (p.data->>'timestamp')::timestamptz as timestamp,\n    p.data->>'category' as category,\n    p.data->>'description' as text,\n    p.data->>'message_url' as message_url\n  FROM projections p\n  WHERE p.projection_type = 'activity'\n    AND p.status IN ('auto_confirmed', 'confirmed')\n    AND ($1::text = '' OR $1::text IN ('activities', 'activity'))\n  ORDER BY (p.data->>'timestamp')::timestamptz DESC\n  LIMIT CASE WHEN $1::text = '' THEN 5 ELSE $2::int END\n),\nnotes_data AS (\n  SELECT \n    'note' as type,\n    p.id,\n    (p.data->>'timestamp')::timestamptz as timestamp,\n    p.data->>'category' as category,\n    p.data->>'text' as text,\n    p.data->>'message_url' as message_url\n  FROM projections p\n  WHERE p.projection_type = 'note'\n    AND p.status IN ('auto_confirmed', 'confirmed')\n    AND ($1::text = '' OR $1::text IN ('notes', 'note'))\n  ORDER BY (p.data->>'timestamp')::timestamptz DESC\n  LIMIT CASE WHEN $1::text = '' THEN 5 ELSE $2::int END\n),\ntodos_data AS (\n  SELECT \n    'todo' as type,\n    p.id,\n    (p.data->>'timestamp')::timestamptz as timestamp,\n    COALESCE(p.data->>'status', 'pending') as category,\n    p.data->>'text' as text,\n    p.data->>'message_url' as message_url\n  FROM projections p\n  WHERE p.projection_type = 'todo'\n    AND p.status IN ('auto_confirmed', 'confirmed')\n    AND ($1::text = '' OR $1::text IN ('todos', 'todo'))\n  ORDER BY (p.data->>'timestamp')::timestamptz DESC\n  LIMIT CASE WHEN $1::text = '' THEN 5 ELSE $2::int END\n)\nSELECT * FROM activities_data\nUNION ALL\nSELECT * FROM notes_data\nUNION ALL\nSELECT * FROM todos_data\nORDER BY timestamp DESC\nLIMIT $2::int;",
        "options": {
          "queryReplacement": "={{ $json.ctx.validation.normalized_type }},{{ $json.ctx.validation.normalized_limit }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        -464,
        9232
      ],
      "id": "cdd4aab7-7649-4dd9-9574-3003796f3d3b",
      "name": "QueryRecentEvents",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH user_events AS (\n  SELECT \n    payload->>'author_login' as user_login,\n    MAX(received_at) as last_observation_at\n  FROM events\n  WHERE payload->>'author_login' IS NOT NULL\n  GROUP BY payload->>'author_login'\n  LIMIT 1\n)\nSELECT \n  ue.user_login,\n  ue.last_observation_at,\n  p.data->>'category' as last_activity_category,\n  EXTRACT(EPOCH FROM (NOW() - ue.last_observation_at))/60 as minutes_since_activity\nFROM user_events ue\nLEFT JOIN projections p ON p.projection_type = 'activity' \n  AND p.data->>'timestamp' = (\n    SELECT MAX(data->>'timestamp') \n    FROM projections \n    WHERE projection_type = 'activity'\n  )",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        -464,
        9808
      ],
      "id": "ace9000d-af6f-4eb7-90ac-6169a08148e9",
      "name": "QueryUserStatus",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Handle help command\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\n\nconst helpText = `\ud83d\udcda **Kairon Help**\n\n**Message Tags**\nTag your messages to classify them:\n\n\\`!! <message>\\` or \\`act <message>\\` - Log an activity\n\\`.. <message>\\` or \\`note <message>\\` - Capture a note\n\\`++ <message>\\` or \\`chat <message>\\` - Start a conversation thread\n\\`-- <message>\\` or \\`save <message>\\` - Save thread insights (coming soon)\n\\`:: <command>\\` or \\`cmd <command>\\` - Execute a command\n\\`$$ <message>\\` or \\`todo <message>\\` - Create a todo (coming soon)\n\n**Semantic Tagging**\nNo tag? Kairon uses AI to classify your message:\n- First-person actions \u2192 !! (activity)\n- Observations/notes \u2192 .. (note)\n- Questions/requests \u2192 ++ (conversation)\n\n**Commands**\n\n**Configuration:**\n\\`::get <key>\\` - Get a config value\n\\`::set <key> <value>\\` - Set a config value\n\n**Prompt Modules:**\n\\`::modules\\` - List all prompt modules\n\\`::get module <name>\\` - View module details\n\\`::set module <name> on\\` - Enable module\n\\`::set module <name> off\\` - Disable module\n\\`::set module <name> <content>\\` - Update content\n\n**Information:**\n\\`::stats\\` - Show activity statistics\n\\`::recent [type] [N]\\` - Show recent items\n\\`::status\\` - Show your current state\n\n**Data Management:**\n\\`::delete activity <number>\\` - Delete activity by index\n\\`::delete note <number>\\` - Delete note by index\n\n**Generation:**\n\\`::generate summary\\` - Generate daily summary now\n\\`::pulse\\` - Send a pulse message now\n\n**System:**\n\\`::ping\\` - Test if system is working\n\\`::help\\` - Show this message\n\n**Config Keys:**\n- \\`north_star\\` - Your guiding principle\n- \\`summary_time\\` - Daily summary time (HH:MM)\n- \\`timezone\\` - Your timezone (e.g. \\`pacific\\`, \\`vancouver\\`)\n- \\`verbose\\` - Always show projection details (true/false)\n- \\`next_pulse\\` - Next proactive pulse timestamp\n\n**Examples:**\n\\`!! working on the router\\`\n\\`.. John loves dark roast coffee\\`\n\\`::set timezone vancouver\\`\n\\`::modules\\`\n\\`::get module base_persona\\`\n\\`::set module base_persona off\\``;\n\nreturn {\n  ctx: {\n    ...ctx,\n    response: {\n      content: helpText\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        10192
      ],
      "id": "08ba42dd-9ee4-40fe-bb1a-3f6bf264f200",
      "name": "HandleHelp"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Format get command response - merge DB result into ctx\n// Fetches timezone and config from ctx.db (populated by Execute_Queries)\nconst ctx = $json.ctx;\nconst result = ctx.db?.config?.row;\n\n// Check if query returned data\nif (!result || !result.key || !result.value) {\n  const key = ctx.validation.normalized_key;\n  return {\n    json: {\n        ctx: {\n          ...ctx,\n          response: {\n            content: `\u274c Config key \\`${key}\\` not found.\\n\\nUse \\`::set ${key} <value>\\` to set it first.\\n\\nAvailable keys: north_star, summary_time, timezone, verbose, next_pulse`\n          }\n        }\n    }\n  };\n}\n\n// Format the value nicely\nlet displayValue = result.value;\n\n// Special formatting for timestamps\nif (result.key === 'next_pulse' || result.key === 'summary_time') {\n  try {\n    const timestamp = new Date(result.value);\n    if (!isNaN(timestamp.getTime())) {\n      const now = new Date();\n      const diffMs = timestamp - now;\n      \n      // Fetch timezone from ctx.db (populated by Execute_Queries)\n      const timezone = ctx.db?.timezone?.row?.value || 'America/Los_Angeles'; // fallback\n      \n      // Extract friendly timezone name (e.g., \"Vancouver\" from \"America/Vancouver\")\n      const tzName = timezone.includes('/') ? timezone.split('/')[1].replace(/_/g, ' ') : timezone;\n      \n      // Format timestamp in user's local timezone\n      const options = {\n        timeZone: timezone,\n        year: 'numeric',\n        month: 'short',\n        day: 'numeric',\n        hour: 'numeric',\n        minute: '2-digit',\n        hour12: true\n      };\n      const localTimeStr = timestamp.toLocaleString('en-US', options);\n      \n      if (result.key === 'next_pulse') {\n        if (diffMs <= 0) {\n          displayValue = 'Now (pulse should run soon)';\n        } else {\n          const diffMinutes = Math.floor(diffMs / (1000 * 60));\n          const diffHours = Math.floor(diffMinutes / 60);\n          const remainingMinutes = diffMinutes % 60;\n          \n          if (diffHours > 0) {\n            displayValue = `In ${diffHours}h ${remainingMinutes}m (${localTimeStr} ${tzName})`;\n          } else {\n            displayValue = `In ${diffMinutes}m (${localTimeStr} ${tzName})`;\n          }\n        }\n      } else {\n        // For other timestamps, show formatted local time with timezone\n        displayValue = `${localTimeStr} ${tzName}`;\n      }\n    }\n  } catch (e) {\n    // If parsing fails, use raw value\n  }\n}\n\nreturn {\n  json: {\n    ctx: {\n      ...ctx,\n      db: { config_key: result.key, config_value: result.value },\n      response: {\n        content: `**${result.key}:** ${displayValue}`\n      }\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        8816
      ],
      "id": "ed79c164-c1c3-40b9-adfb-6cb9d3ca307d",
      "name": "PrepareGetResponse"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Format set command response - merge DB result into ctx\nconst ctx = $('IfValidSet').first().json.ctx;\nconst result = $input.item?.json;\n\n// Check if database operation succeeded\nif (!result || !result.key) {\n  return {\n    json: {\n        ctx: {\n          ...ctx,\n          response: {\n            content: `\u274c Failed to set config value. Please try again.`\n          }\n        }\n    }\n  };\n}\n\n// Special confirmation for timezone - show current time\nlet content = `\u2705 Set **${result.key}** = ${result.value}`;\nif (result.key === 'timezone') {\n  try {\n    const now = new Date();\n    const timeStr = now.toLocaleString('en-US', { \n      timeZone: result.value, \n      hour: 'numeric', \n      minute: '2-digit',\n      hour12: true \n    });\n    content += `\\n\\n\ud83d\udd50 Current time: **${timeStr}**`;\n  } catch (e) {\n    // Timezone validation should catch this, but just in case\n  }\n}\n\nreturn {\n  json: {\n    ctx: {\n      ...ctx,\n      db: { config_key: result.key, config_value: result.value },\n      response: {\n        content\n      }\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        9040
      ],
      "id": "8e523999-62d1-4f38-8fd3-22e5708e9325",
      "name": "FormatSetResponse"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Format stats response - merge DB result into ctx\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\nconst stats = $input.item?.json;\n\nif (!stats) {\n  return {\n    json: {\n        ctx: {\n          ...ctx,\n          response: {\n            content: `\u274c Unable to retrieve statistics. Please try again.`\n          }\n        }\n    }\n  };\n}\n\nconst lastActivity = stats.last_activity \n  ? new Date(stats.last_activity).toLocaleString()\n  : 'Never';\n\nreturn {\n  json: {\n    ctx: {\n      ...ctx,\n      db: { stats },\n      response: {\n        content: `\ud83d\udcca **Statistics**\\n\\n` +\n          `Activities today: ${stats.activities_today || 0}\\n` +\n          `Notes this week: ${stats.notes_this_week || 0}\\n` +\n          `Total events: ${stats.total_events || 0}\\n` +\n          `Last activity: ${lastActivity}`\n      }\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        9616
      ],
      "id": "fba83474-329f-4db6-9939-11cfe17a7a3d",
      "name": "FormatStatsResponse"
    },
    {
      "parameters": {
        "jsCode": "// Format recent items response (numbered list for easy deletion)\nconst ctx = $('IfValidRecent').first().json.ctx;\nconst items = $input.all();\nconst itemType = ctx.validation.normalized_type;\nconst MAX_LENGTH = 1900; // Discord limit is 2000, leave margin for safety\n\nif (!items || items.length === 0) {\n  const typeLabel = itemType || 'projections';\n  return {\n    ctx: {\n      ...ctx,\n      response: {\n        content: `\ud83d\udced No recent ${typeLabel} found`\n      }\n    }\n  };\n}\n\n// Determine if mixed types or single type\nconst types = [...new Set(items.map(i => i.json.type))];\nconst isMixed = types.length > 1 || itemType === '';\n\n// Icons for each type (match bot confirmation emojis)\nconst icons = { activity: '\ud83d\udd18', note: '\ud83d\udcdd', todo: '\u2705' };\nconst titles = { activity: 'Activities', note: 'Notes', todo: 'Todos' };\n\nlet title, icon;\nif (isMixed) {\n  icon = '\ud83d\udccb';\n  title = 'Recent Projections';\n} else {\n  const firstType = items[0].json.type;\n  icon = icons[firstType] || '\ud83d\udccb';\n  title = titles[firstType] || 'Items';\n}\n\nlet responseText = `${icon} **${title} (${items.length})**\n\n`;\nlet itemsShown = 0;\nlet truncated = false;\n\nfor (let i = 0; i < items.length; i++) {\n  const data = items[i].json;\n  const dt = DateTime.fromISO(data.timestamp, { zone: 'utc' }).setZone('America/Los_Angeles');\n  const dateStr = dt.toFormat('MMM d');\n  const timeStr = dt.toFormat('HH:mm');\n  \n  const text = data.text && data.text.length > 50 \n    ? data.text.substring(0, 50) + '...' \n    : (data.text || '(no text)');\n  \n  // Build Discord message link from message_url in projection data\n  let link = '';\n  if (data.message_url) {\n    link = ` [\u2197](${data.message_url})`;\n  }\n  \n  // Type indicator for mixed view\n  const typeIcon = isMixed ? `${icons[data.type] || '\u2022'} ` : '';\n  \n  const line = `${i + 1}. ${typeIcon}${dateStr} ${timeStr} - **${data.category}** - ${text}${link}\n`;\n  \n  // Check if adding this line would exceed limit\n  if (responseText.length + line.length > MAX_LENGTH - 100) {\n    truncated = true;\n    break;\n  }\n  \n  responseText += line;\n  itemsShown++;\n}\n\n// Add truncation notice if needed\nif (truncated) {\n  responseText += `\n... and ${items.length - itemsShown} more (use a smaller limit)`;\n}\n\n// Add delete hint based on type\nif (!isMixed && !truncated) {\n  const deleteType = items[0].json.type;\n  responseText += `\n\ud83d\udca1 Use \\`::delete ${deleteType} <number>\\` to delete items`;\n}\n\nreturn {\n  ctx: {\n    ...ctx,\n    db: { recent_items: items.map(i => i.json) },\n    response: {\n      content: responseText\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        9232
      ],
      "id": "a38c0645-066f-4591-b097-de21fb82b1c1",
      "name": "FormatRecentResponse"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Format status response - merge DB result into ctx\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\nconst status = $input.item?.json;\n\nif (!status) {\n  return {\n    json: {\n        ctx: {\n          ...ctx,\n          response: {\n            content: `\u274c Unable to retrieve status. Please try again.`\n          }\n        }\n    }\n  };\n}\n\nconst isSleeping = status.last_activity_category === 'sleep';\nconst sleepIcon = isSleeping ? '\ud83d\ude34' : '\ud83d\udc41\ufe0f';\nconst sleepStatus = isSleeping ? 'Sleeping' : 'Awake';\nconst lastActivity = status.last_observation_at\n  ? new Date(status.last_observation_at).toLocaleString()\n  : 'Never';\nconst minutesAgo = Math.round(status.minutes_since_activity || 0);\nconst lastCategory = status.last_activity_category || 'unknown';\n\nreturn {\n  json: {\n    ctx: {\n      ...ctx,\n      db: { user_status: status },\n      response: {\n        content: `${sleepIcon} **Status**\\n\\n` +\n          `State: ${sleepStatus}\\n` +\n          `Last activity: ${lastActivity}\\n` +\n          `Last category: ${lastCategory}\\n` +\n          `Time since: ${minutesAgo} minutes ago`\n      }\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        9808
      ],
      "id": "6e1f0ee3-6930-49e6-a76a-3a5b85d942f3",
      "name": "FormatStatusResponse"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Unknown command handler\nconst ctx = $json.ctx;\nconst command = ctx.command?.name || 'unknown';\n\nreturn {\n  json: {\n    ctx: {\n      ...ctx,\n      response: {\n        content: `\u274c Unknown command: \\`${command}\\`\\n\\nUse \\`::help\\` to see available commands.`\n      }\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        10384
      ],
      "id": "f76de764-c075-42cc-95e3-538852ab8b22",
      "name": "HandleUnknownCommand"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Void projection by index (1-based position in recent list) - NEVER DELETE\nWITH input_array AS (\n  SELECT ARRAY[\n    NULLIF($2, '')::int,\n    NULLIF($3, '')::int,\n    NULLIF($4, '')::int,\n    NULLIF($5, '')::int,\n    NULLIF($6, '')::int,\n    NULLIF($7, '')::int,\n    NULLIF($8, '')::int,\n    NULLIF($9, '')::int,\n    NULLIF($10, '')::int\n  ] as indices\n),\ntarget_indices AS (\n  SELECT (unnest(indices) - 1) AS idx\n  FROM input_array\n),\nrecent_activities AS (\n  SELECT \n    p.id,\n    (p.data->>'timestamp')::timestamptz as timestamp,\n    p.data->>'description' as text,\n    'activity' as type,\n    (ROW_NUMBER() OVER (ORDER BY (p.data->>'timestamp')::timestamptz DESC) - 1) as idx\n  FROM projections p\n  WHERE p.projection_type = 'activity'\n    AND p.status IN ('auto_confirmed', 'confirmed')\n    AND $1::text = 'activity'\n  ORDER BY (p.data->>'timestamp')::timestamptz DESC\n  LIMIT 100\n),\nrecent_notes AS (\n  SELECT \n    p.id,\n    (p.data->>'timestamp')::timestamptz as timestamp,\n    p.data->>'text' as text,\n    'note' as type,\n    (ROW_NUMBER() OVER (ORDER BY (p.data->>'timestamp')::timestamptz DESC) - 1) as idx\n  FROM projections p\n  WHERE p.projection_type = 'note'\n    AND p.status IN ('auto_confirmed', 'confirmed')\n    AND $1::text = 'note'\n  ORDER BY (p.data->>'timestamp')::timestamptz DESC\n  LIMIT 100\n),\nvoided_activities AS (\n  UPDATE projections\n  SET status = 'voided',\n      voided_at = NOW(),\n      voided_reason = 'user_deleted'\n  WHERE id IN (\n    SELECT ra.id FROM recent_activities ra\n    INNER JOIN target_indices ti ON ra.idx = ti.idx\n  )\n  RETURNING id, (data->>'timestamp')::timestamptz as timestamp, data->>'description' as text, 'activity' as type\n),\nvoided_notes AS (\n  UPDATE projections\n  SET status = 'voided',\n      voided_at = NOW(),\n      voided_reason = 'user_deleted'\n  WHERE id IN (\n    SELECT rn.id FROM recent_notes rn\n    INNER JOIN target_indices ti ON rn.idx = ti.idx\n  )\n  RETURNING id, (data->>'timestamp')::timestamptz as timestamp, data->>'text' as text, 'note' as type\n)\nSELECT * FROM voided_activities\nUNION ALL\nSELECT * FROM voided_notes;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        -464,
        9424
      ],
      "id": "9eca4f90-6b61-48e7-8fd6-295adcdca359",
      "name": "VoidProjections",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Format void response (index-based soft-delete)\nconst ctx = $('IfValidDelete').first().json.ctx;\nconst voided = $input.all();\nconst itemType = ctx.validation.normalized_item_type || 'activity';\nconst indices = ctx.validation.indices || [];\n\n// Check if any items were actually voided\nif (!voided || voided.length === 0) {\n  const indexStr = indices.join(', ');\n  return {\n    ctx: {\n      ...ctx,\n      response: {\n        content: `\u274c No ${itemType}s found at position(s): ${indexStr}\\n\\nUse \\`::recent ${itemType}s\\` to see the current numbered list.`\n      }\n    }\n  };\n}\n\nif (voided.length === 1) {\n  const item = voided[0].json;\n  const text = item.text.length > 60 ? item.text.substring(0, 60) + '...' : item.text;\n  return {\n    ctx: {\n      ...ctx,\n      db: { voided_items: [item] },\n      response: {\n        content: `\u2705 Removed ${item.type} #${indices[0]}: \"${text}\"`\n      }\n    }\n  };\n}\n\nconst typeLabel = voided[0].json.type === 'activity' ? 'activities' : 'notes';\nreturn {\n  ctx: {\n    ...ctx,\n    db: { voided_items: voided.map(d => d.json) },\n    response: {\n      content: `\u2705 Removed ${voided.length} ${typeLabel} at positions: ${indices.join(', ')}`\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        9424
      ],
      "id": "1bc87344-c616-42bd-8fe4-8882e2174430",
      "name": "FormatVoidResponse"
    },
    {
      "parameters": {
        "jsCode": "// Validate GET command\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\nconst args = $('ParseCommandAndArgs').first()?.json?.ctx?.command?.args || [];\n\n// Check for module subcommand: ::get module <name>\nif (args[0] === 'module' || args[0] === 'modules') {\n  const moduleName = args[1];\n  \n  if (!moduleName) {\n    return {\n      ctx: {\n        ...ctx,\n        validation: {\n          valid: false,\n          error_message: `\u274c Missing module name. Syntax: \\`::get module <name>\\`\\n\\nUse \\`::modules\\` to see available modules.`\n        }\n      }\n    };\n  }\n  \n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: true,\n        target: 'module',\n        module_name: moduleName\n      }\n    }\n  };\n}\n\n// Standard config get\nconst key = args.join('_');\n\nif (!key || key.trim() === '') {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Missing config key. Syntax: \\`::get <key>\\`\\n\\nAvailable keys: north_star, summary_time, timezone, verbose, next_pulse\\nFor modules: \\`::get module <name>\\`\\nUse \\`::help\\` for more info.`\n      }\n    }\n  };\n}\n\nreturn {\n  ctx: {\n    ...ctx,\n    validation: {\n      valid: true,\n      target: 'config',\n      normalized_key: key\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -912,
        8768
      ],
      "id": "b665e2c9-44f1-48ed-a0dc-5ab2cae4e2ed",
      "name": "ValidateGet"
    },
    {
      "parameters": {
        "jsCode": "// Validate SET command\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\nconst args = $('ParseCommandAndArgs').first()?.json?.ctx?.command?.args || [];\n\n// Check for module subcommand: ::set module <name> <on|off|content>\nif (args[0] === 'module' || args[0] === 'modules') {\n  const moduleName = args[1];\n  const action = args[2];\n  \n  if (!moduleName) {\n    return {\n      ctx: {\n        ...ctx,\n        validation: {\n          valid: false,\n          error_message: `\u274c Missing module name. Syntax:\\n\\`::set module <name> on\\` - Enable module\\n\\`::set module <name> off\\` - Disable module\\n\\`::set module <name> <content>\\` - Update content\\n\\nUse \\`::modules\\` to see available modules.`\n        }\n      }\n    };\n  }\n  \n  if (!action) {\n    return {\n      ctx: {\n        ...ctx,\n        validation: {\n          valid: false,\n          error_message: `\u274c Missing action. Syntax:\\n\\`::set module ${moduleName} on\\` - Enable\\n\\`::set module ${moduleName} off\\` - Disable\\n\\`::set module ${moduleName} <content>\\` - Update content`\n        }\n      }\n    };\n  }\n  \n  // Check for on/off toggle\n  if (action.toLowerCase() === 'on') {\n    return {\n      ctx: {\n        ...ctx,\n        validation: {\n          valid: true,\n          target: 'module',\n          operation: 'toggle',\n          module_name: moduleName,\n          new_active: true\n        }\n      }\n    };\n  }\n  \n  if (action.toLowerCase() === 'off') {\n    return {\n      ctx: {\n        ...ctx,\n        validation: {\n          valid: true,\n          target: 'module',\n          operation: 'toggle',\n          module_name: moduleName,\n          new_active: false\n        }\n      }\n    };\n  }\n  \n  // Otherwise it's a content update - join all remaining args\n  const newContent = args.slice(2).join(' ');\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: true,\n        target: 'module',\n        operation: 'update',\n        module_name: moduleName,\n        new_content: newContent\n      }\n    }\n  };\n}\n\n// Standard config set - need at least 2 args (key and value)\nif (!args || args.length < 2) {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Missing arguments. Syntax: \\`::set <key> <value>\\`\\n\\nExample: \\`::set north_star Be present and intentional\\`\\nFor modules: \\`::set module <name> on|off|<content>\\`\\n\\nAvailable keys: north_star, summary_time, timezone, verbose`\n      }\n    }\n  };\n}\n\nconst key = args[0];\nif (!key || key.trim() === '') {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Invalid config key (empty string).\\n\\nExample: \\`::set north_star <your principle>\\``\n      }\n    }\n  };\n}\n\nlet valToSet = args.slice(1).join(' ');\n\n// Special handling for verbose key\nif (key === 'verbose') {\n  const val = valToSet.toLowerCase().trim();\n  if (val !== 'true' && val !== 'false') {\n    return {\n      ctx: {\n        ...ctx,\n        validation: {\n          valid: false,\n          error_message: `\u274c Invalid value for \\`verbose\\`. Use \\`true\\` or \\`false\\`.`\n        }\n      }\n    };\n  }\n  valToSet = val;\n}\n\n// Special handling for timezone key\nif (key === 'timezone' || key === 'tz') {\n  const input = valToSet.toLowerCase().trim();\n  \n  const aliases = {\n    'pacific': 'America/Los_Angeles',\n    'pst': 'America/Los_Angeles',\n    'pdt': 'America/Los_Angeles',\n    'la': 'America/Los_Angeles',\n    'los angeles': 'America/Los_Angeles',\n    'vancouver': 'America/Vancouver',\n    'seattle': 'America/Los_Angeles',\n    'sf': 'America/Los_Angeles',\n    'mountain': 'America/Denver',\n    'mst': 'America/Denver',\n    'mdt': 'America/Denver',\n    'denver': 'America/Denver',\n    'central': 'America/Chicago',\n    'cst': 'America/Chicago',\n    'cdt': 'America/Chicago',\n    'chicago': 'America/Chicago',\n    'eastern': 'America/New_York',\n    'est': 'America/New_York',\n    'edt': 'America/New_York',\n    'new york': 'America/New_York',\n    'nyc': 'America/New_York',\n    'toronto': 'America/Toronto',\n    'utc': 'UTC',\n    'gmt': 'UTC',\n    'london': 'Europe/London',\n    'uk': 'Europe/London',\n    'paris': 'Europe/Paris',\n    'berlin': 'Europe/Berlin',\n    'tokyo': 'Asia/Tokyo',\n    'sydney': 'Australia/Sydney',\n    'melbourne': 'Australia/Melbourne'\n  };\n  \n  const resolved = aliases[input] || valToSet;\n  \n  try {\n    const testDate = new Date();\n    testDate.toLocaleString('en-US', { timeZone: resolved });\n    valToSet = resolved;\n  } catch (e) {\n    return {\n      ctx: {\n        ...ctx,\n        validation: {\n          valid: false,\n          error_message: `\u274c Invalid timezone: \\`${valToSet}\\`\\n\\nExamples: \\`pacific\\`, \\`vancouver\\`, \\`America/Los_Angeles\\``\n        }\n      }\n    };\n  }\n}\n\nreturn {\n  ctx: {\n    ...ctx,\n    validation: {\n      valid: true,\n      target: 'config',\n      normalized_key: key === 'tz' ? 'timezone' : key,\n      normalized_value: valToSet\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -912,
        8960
      ],
      "id": "14603be3-d751-4be9-a5a1-46ad5f7249a7",
      "name": "ValidateSet"
    },
    {
      "parameters": {
        "jsCode": "// Validate DELETE command (uses indices, not short IDs)\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\nconst args = $('ParseCommandAndArgs').first()?.json?.ctx?.command?.args || [];\nconst itemType = args[0];\n\n// Validation: need item type (activity/note)\nif (!itemType || !['activity', 'note', 'activities', 'notes'].includes(itemType)) {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Invalid item type. Syntax: \\`::delete activity <index>\\` or \\`::delete note <index>\\`\\n\\nUse \\`::recent activities\\` or \\`::recent notes\\` to see numbered list.`\n      }\n    }\n  };\n}\n\n// Validation: need at least one index\nif (args.length < 2 || !args[1]) {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Missing index. Syntax: \\`::delete ${itemType} <index>\\`\\n\\nExample: \\`::delete ${itemType} 1\\` (deletes first item)\\nUse \\`::recent ${itemType === 'activity' || itemType === 'activities' ? 'activities' : 'notes'}\\` to see numbered list.`\n      }\n    }\n  };\n}\n\n// Validation: check indices are valid numbers (1-100)\nconst indices = args.slice(1).filter(arg => arg !== null && arg !== undefined);\nconst invalidIndices = [];\nconst validIndices = [];\n\nfor (const idx of indices) {\n  const num = parseInt(idx);\n  if (isNaN(num) || num < 1 || num > 100) {\n    invalidIndices.push(idx);\n  } else {\n    validIndices.push(num);\n  }\n}\n\nif (invalidIndices.length > 0) {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Invalid index: ${invalidIndices.join(', ')}\\n\\nIndices must be numbers between 1 and 100.\\nUse \\`::recent ${itemType === 'activity' || itemType === 'activities' ? 'activities' : 'notes'}\\` to see numbered list.`\n      }\n    }\n  };\n}\n\nif (validIndices.length === 0) {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c No valid indices provided.\\n\\nExample: \\`::delete ${itemType} 1 2 3\\``\n      }\n    }\n  };\n}\n\n// Normalize type to singular form\nconst normalizedItemType = itemType === 'activities' ? 'activity' : (itemType === 'notes' ? 'note' : itemType);\n\n// Valid - pass normalized values and indices\nreturn {\n  ctx: {\n    ...ctx,\n    validation: {\n      valid: true,\n      normalized_item_type: normalizedItemType,\n      indices: validIndices\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -912,
        9344
      ],
      "id": "99805555-a99c-4559-b170-f18f40233457",
      "name": "ValidateDelete"
    },
    {
      "parameters": {
        "jsCode": "// Validate RECENT command\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\nconst args = $('ParseCommandAndArgs').first()?.json?.ctx?.command?.args || [];\n\n// Default values - empty string means show all projections\nconst itemType = args[0] || '';\nconst limit = args[1] ? parseInt(args[1]) : 10;\n\n// Validation: check item type is valid\nconst validTypes = ['activities', 'activity', 'notes', 'note', 'todos', 'todo', ''];\nif (!validTypes.includes(itemType)) {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Invalid item type: \\`${itemType}\\`\\n\\nValid types: activities, notes, todos (or blank for all)\\nSyntax: \\`::recent [activities|notes|todos] [limit]\\`\\n\\nExample: \\`::recent todos 20\\``\n      }\n    }\n  };\n}\n\n// Validation: check limit is a valid number (1-100)\nif (isNaN(limit) || limit < 1 || limit > 100) {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Invalid limit: \\`${args[1]}\\`\\n\\nLimit must be a number between 1 and 100.\\nExample: \\`::recent activities 20\\``\n      }\n    }\n  };\n}\n\n// Normalize type to plural form for query\nlet normalizedType = itemType;\nif (itemType === 'activity') normalizedType = 'activities';\nelse if (itemType === 'note') normalizedType = 'notes';\nelse if (itemType === 'todo') normalizedType = 'todos';\n\n// Valid - pass normalized values\nreturn {\n  ctx: {\n    ...ctx,\n    validation: {\n      valid: true,\n      normalized_type: normalizedType,\n      normalized_limit: limit\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -912,
        9152
      ],
      "id": "4bf45858-8e0b-4aca-8713-44bf3f9627ab",
      "name": "ValidateRecent"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "valid-check-recent",
              "leftValue": "={{ $json.ctx.validation.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -688,
        9152
      ],
      "id": "db6e02d4-93be-4ebf-b85a-96aade6217dd",
      "name": "IfValidRecent"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "valid-check",
              "leftValue": "={{ $json.ctx.validation.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -688,
        8768
      ],
      "id": "0cb57d2d-bb81-4fd9-a1c8-6d4b3d2f8868",
      "name": "IfValidGet"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "valid-check",
              "leftValue": "={{ $json.ctx.validation.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -688,
        8960
      ],
      "id": "dbbb373a-e421-44cb-acdf-9ddd121a659d",
      "name": "IfValidSet"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "valid-check",
              "leftValue": "={{ $json.ctx.validation.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -688,
        9344
      ],
      "id": "f2b3226c-7f08-4e93-9684-8467d9fd3458",
      "name": "IfValidDelete"
    },
    {
      "parameters": {
        "resource": "message",
        "guildId": {
          "__rl": true,
          "value": "={{ $json.ctx.event.guild_id }}",
          "mode": "id"
        },
        "channelId": {
          "__rl": true,
          "value": "={{ $json.ctx.response.channel_id || $json.ctx.event.channel_id }}",
          "mode": "id"
        },
        "content": "={{ $json.ctx.response.content }}",
        "options": {}
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [
        -16,
        9664
      ],
      "id": "4ed54e00-c7e3-4db6-94c0-c525d95534e3",
      "name": "SendResponse",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 1000,
      "credentials": {
        "discordBotApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "DELETE",
        "url": "=https://discord.com/api/v10/channels/{{ $json.ctx.event.channel_id }}/messages/{{ $json.ctx.event.message_id }}/reactions/\ud83d\udd35/@me",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "discordBotApi",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -1360,
        9808
      ],
      "id": "remove-blue-reaction-execute-cmd",
      "name": "Remove\ud83d\udd35Reaction",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 1000,
      "credentials": {
        "discordBotApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "react",
        "guildId": {
          "__rl": true,
          "value": "={{ $json.ctx.event.guild_id }}",
          "mode": "id"
        },
        "channelId": {
          "__rl": true,
          "value": "={{ $json.ctx.event.channel_id }}",
          "mode": "id"
        },
        "messageId": "={{ $json.ctx.event.message_id }}",
        "emoji": "=\ud83d\udcbb"
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [
        -1360,
        9424
      ],
      "id": "e5ebd093-f037-4d4a-94a8-5830869a2dbc",
      "name": "ReactWith\ud83d\udcbb",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 1000,
      "credentials": {
        "discordBotApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Validate GENERATE command\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\nconst args = $('ParseCommandAndArgs').first()?.json?.ctx?.command?.args || [];\nconst generateType = args[0]?.toLowerCase();\n\n// Valid types: summary only\nconst validTypes = ['summary'];\n\nif (!generateType) {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Missing type. Syntax: \\`::generate <type>\\`\\n\\nValid types:\\n- \\`summary\\` - Generate daily summary now`\n      }\n    }\n  };\n}\n\nif (!validTypes.includes(generateType)) {\n  return {\n    ctx: {\n      ...ctx,\n      validation: {\n        valid: false,\n        error_message: `\u274c Invalid type: \\`${generateType}\\`\\n\\nValid types: summary`\n      }\n    }\n  };\n}\n\nreturn {\n  ctx: {\n    ...ctx,\n    validation: {\n      valid: true,\n      generate_type: generateType\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -912,
        9536
      ],
      "id": "validate-generate",
      "name": "ValidateGenerate"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "valid-check-generate",
              "leftValue": "={{ $json.ctx.validation.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -688,
        9536
      ],
      "id": "if-valid-generate",
      "name": "IfValidGenerate"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "check-summary",
                    "leftValue": "={{ $json.ctx.validation.generate_type }}",
                    "rightValue": "summary",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "summary"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        -464,
        9536
      ],
      "id": "switch-generate-type",
      "name": "SwitchGenerateType"
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "tpkLfvOCAeN5YzMR",
          "mode": "list",
          "cachedResultName": "Generate_Daily_Summary",
          "cachedResultUrl": "/workflow/tpkLfvOCAeN5YzMR"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "trigger_reason": "manual"
          },
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {
          "waitForSubWorkflow": false
        }
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [
        -240,
        9600
      ],
      "id": "execute-summary",
      "name": "ExecuteGenerateDailySummary"
    },
    {
      "parameters": {
        "jsCode": "// Format response for generate command\nconst ctx = $('IfValidGenerate').first().json.ctx;\nconst generateType = ctx.validation.generate_type;\nconst typeLabel = generateType === 'summary' ? 'daily summary' : 'pulse';\n\nreturn {\n  ctx: {\n    ...ctx,\n    response: {\n      content: `\u2705 Generating ${typeLabel}...`,\n      channel_id: $env.DISCORD_CHANNEL_KAIRON_LOGS\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -16,
        9536
      ],
      "id": "format-generate-response",
      "name": "FormatGenerateResponse"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Format validation error for response\nconst ctx = $json.ctx;\n\nreturn {\n  json: {\n    ctx: {\n      ...ctx,\n      response: {\n        content: ctx.validation.error_message\n      }\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -16,
        9856
      ],
      "id": "send-error-message",
      "name": "SendErrorMessage"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT name, module_type, active, priority, array_length(tags, 1) as tag_count\nFROM prompt_modules\nORDER BY module_type, priority, name",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        -464,
        10576
      ],
      "id": "query-list-modules",
      "name": "QueryListModules",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Format modules list response\nconst ctx = $('WhenExecutedByAnotherWorkflow').first().json.ctx;\nconst modules = $input.all();\n\nif (!modules || modules.length === 0) {\n  return {\n    ctx: {\n      ...ctx,\n      response: {\n        content: `\ud83d\udced No prompt modules found.`\n      }\n    }\n  };\n}\n\n// Group by module_type\nconst byType = {};\nfor (const mod of modules) {\n  const m = mod.json;\n  const type = m.module_type || 'unknown';\n  if (!byType[type]) byType[type] = [];\n  byType[type].push(m);\n}\n\nconst typeIcons = {\n  persona: '\ud83c\udfad',\n  technique: '\ud83e\udde0',\n  guardrail: '\ud83d\udee1\ufe0f',\n  format: '\ud83d\udcdd',\n  context: '\ud83d\udcca'\n};\n\nlet response = `\ud83e\udde9 **Prompt Modules (${modules.length})**\\n\\n`;\n\nfor (const [type, mods] of Object.entries(byType)) {\n  const icon = typeIcons[type] || '\u2022';\n  response += `**${icon} ${type}**\\n`;\n  for (const m of mods) {\n    const status = m.active ? '\u2705' : '\u274c';\n    response += `  ${status} \\`${m.name}\\` (priority: ${m.priority})\\n`;\n  }\n  response += '\\n';\n}\n\nresponse += `\ud83d\udca1 Use \\`::module <name>\\` to view details\\n`;\nresponse += `\ud83d\udca1 Use \\`::module <name> on/off\\` to toggle`;\n\nreturn {\n  ctx: {\n    ...ctx,\n    response: {\n      content: response\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        10576
      ],
      "id": "format-modules-response",
      "name": "FormatModulesResponse"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "check-view",
                    "leftValue": "={{ $json.ctx.validation.operation }}",
                    "rightValue": "view",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "view"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "check-toggle",
                    "leftValue": "={{ $json.ctx.validation.operation }}",
                    "rightValue": "toggle",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "toggle"
            },
            {
    

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Execute_Command. Uses executeWorkflowTrigger, postgres, discord, httpRequest. Event-driven trigger; 47 nodes.

Source: https://github.com/chriskevini/kairon/blob/ab924f228ceb22522b9a4dfa1ab4589eb86273ad/n8n-workflows/Execute_Command.json — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Slack & Telegram

03 - Command Handler. Uses executeWorkflowTrigger, telegram, executeCommand, postgres. Event-driven trigger; 53 nodes.

Execute Workflow Trigger, Telegram, Execute Command +2
Slack & Telegram

Unlock low-cost, high-control generative media workflows directly from n8n by integrating with ComfyUI. Ideal for indie creators, AI developers, or small teams seeking scalable media automation—from i

Execute Workflow Trigger, HTTP Request, Read Write File +2
Slack & Telegram

Uses the rentcast.io api to get approximate value of real estate. Updates the asset in YNAB. Get Rentcast.io api key Get YNAB API Key Get YNAB and

HTTP Request, Crypto, Execute Workflow Trigger +1
Slack & Telegram

Handle_Error. Uses errorTrigger, postgres, httpRequest, discord. Event-driven trigger; 9 nodes.

Error Trigger, Postgres, HTTP Request +1
Slack & Telegram

Handle_Quality_Rating. Uses executeWorkflowTrigger, postgres, discord. Event-driven trigger; 5 nodes.

Execute Workflow Trigger, Postgres, Discord