{
  "name": "02b \u2014 Article callback",
  "nodes": [
    {
      "parameters": {
        "updates": [
          "message",
          "callback_query"
        ],
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "id": "11111111-2222-2222-2222-111111111111",
      "name": "Telegram Trigger",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Parse a Telegram update into a normalized command object.\n//\n// Supported text commands (in marketing chat only):\n//   /approve <exec_id>       \u2014 fire publish-article GH Action for a draft\n//   /reject <exec_id>        \u2014 drop the draft, no GH dispatch\n//   /redispatch <exec_id>    \u2014 re-fire GH dispatch even if state isn't awaiting_review\n//   /list  or  /queue        \u2014 show all drafts awaiting review with inline buttons\n//   /topic <free text>       \u2014 append a custom topic to content-plan, status=approved\n//   /edit <exec_id> <field>=<value>  \u2014 single-field edit (title|category|excerpt|read)\n//   @puffypet_bot <text>     \u2014 open a chat thread with Claude (mention anywhere in the message)\n//\n// Replies to any bot message (when text doesn't match a command above) also\n// route to the chat handler \u2014 they continue the existing thread.\n//\n// Inline button callbacks (from /list output) carry callback_data of shape:\n//   act:<approve|reject|edit|list>:<exec_id>\n//\n// Anything else is silently dropped.\nconst MARKETING_CHAT_ID = -5184755657;\n\nconst payload = $json;\n\n// Branch 1 \u2014 callback_query from inline button click\nif (payload.callback_query) {\n  const cq = payload.callback_query;\n  const chatId = cq.message?.chat?.id;\n  if (chatId !== MARKETING_CHAT_ID) return [];\n\n  const data = (cq.data || '').trim();\n  // Format: act:<command>:<exec_id?>\n  const m = data.match(/^act:([a-z]+)(?::(\\S+))?$/);\n  if (!m) return [];\n\n  const command = m[1].toLowerCase();\n  const exec_id = m[2] || null;\n\n  return [{\n    json: {\n      command,\n      exec_id,\n      source: 'callback_query',\n      callback_query_id: cq.id,\n      reply_to_message_id: cq.message?.message_id || null,\n      chat_id: chatId,\n    },\n  }];\n}\n\n// Branch 2 \u2014 text message\nconst msg = payload.message || payload;\nconst chatId = msg.chat?.id;\nconst text = (msg.text || '').trim();\n\nif (chatId !== MARKETING_CHAT_ID) return [];\nif (!text) return [];\n\n// /list and /queue (no args)\nif (/^\\/(list|queue)(?:@\\w+)?\\s*$/i.test(text)) {\n  return [{\n    json: {\n      command: 'list',\n      exec_id: null,\n      source: 'text',\n      reply_to_message_id: msg.message_id,\n      chat_id: chatId,\n    },\n  }];\n}\n\n// /approve | /reject | /redispatch <exec_id>\nconst m1 = text.match(/^\\/(approve|reject|redispatch)(?:@\\w+)?\\s+(\\S+)\\s*$/i);\nif (m1) {\n  return [{\n    json: {\n      command: m1[1].toLowerCase(),\n      exec_id: m1[2],\n      source: 'text',\n      reply_to_message_id: msg.message_id,\n      chat_id: chatId,\n    },\n  }];\n}\n\n// /topic <free text>\nconst m2 = text.match(/^\\/topic(?:@\\w+)?\\s+(.+)$/is);\nif (m2) {\n  return [{\n    json: {\n      command: 'topic',\n      topic_text: m2[1].trim(),\n      exec_id: null,\n      source: 'text',\n      reply_to_message_id: msg.message_id,\n      chat_id: chatId,\n    },\n  }];\n}\n\n// /edit <exec_id> <field>=<value>\n// Value can be unquoted (single token) or double-quoted (any text).\nconst m3 = text.match(/^\\/edit(?:@\\w+)?\\s+(\\S+)\\s+(\\w+)\\s*=\\s*(\"[^\"]*\"|.+?)\\s*$/i);\nif (m3) {\n  let value = m3[3];\n  if (value.startsWith('\"') && value.endsWith('\"')) {\n    value = value.slice(1, -1);\n  }\n  return [{\n    json: {\n      command: 'edit',\n      exec_id: m3[1],\n      edit_field: m3[2].toLowerCase(),\n      edit_value: value,\n      source: 'text',\n      reply_to_message_id: msg.message_id,\n      chat_id: chatId,\n    },\n  }];\n}\n\n// @puffypet_bot <text> \u2014 open a chat thread with Claude.\n// The mention can appear anywhere in the message; the rest (with the mention\n// stripped) becomes the prompt. Bot username is hardcoded \u2014 change here if the\n// bot is renamed via @BotFather.\nif (/@puffypet_bot\\b/i.test(text)) {\n  const stripped = text.replace(/@puffypet_bot\\b/gi, ' ').replace(/\\s+/g, ' ').trim();\n  if (stripped) {\n    return [{\n      json: {\n        command: 'chat',\n        prompt_text: stripped,\n        source_message_id: msg.message_id,\n        parent_bot_message_id: '',\n        username: msg.from?.username || msg.from?.first_name || '',\n        source: 'text',\n        reply_to_message_id: msg.message_id,\n        chat_id: chatId,\n      },\n    }];\n  }\n}\n\n// Reply to a bot message that didn't match a command \u2014 continue the chat thread.\n// Claude resolves whether this is a known thread (lookup by parent message_id)\n// or a new thread (e.g. reply to an article preview from 02a).\nif (msg.reply_to_message && msg.reply_to_message.from && msg.reply_to_message.from.is_bot) {\n  return [{\n    json: {\n      command: 'chat',\n      prompt_text: text,\n      source_message_id: msg.message_id,\n      parent_bot_message_id: String(msg.reply_to_message.message_id),\n      username: msg.from?.username || msg.from?.first_name || '',\n      source: 'text',\n      reply_to_message_id: msg.message_id,\n      chat_id: chatId,\n    },\n  }];\n}\n\n// Drop everything else\nreturn [];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ],
      "id": "22222222-3333-3333-3333-222222222222",
      "name": "Parse Command"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-class-draft-action",
                    "leftValue": "={{ $json.command }}",
                    "rightValue": "approve|reject|redispatch",
                    "operator": {
                      "type": "string",
                      "operation": "regex"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "draft-action"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-class-list",
                    "leftValue": "={{ $json.command }}",
                    "rightValue": "list",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "list"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-class-topic",
                    "leftValue": "={{ $json.command }}",
                    "rightValue": "topic",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "topic"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-class-edit",
                    "leftValue": "={{ $json.command }}",
                    "rightValue": "edit",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "edit"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-class-chat",
                    "leftValue": "={{ $json.command }}",
                    "rightValue": "chat",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "chat"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        480,
        0
      ],
      "id": "30000000-3000-3000-3000-300000000000",
      "name": "Route Class"
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "drafts",
          "mode": "name"
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "exec_id",
              "lookupValue": "={{ $json.exec_id }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        720,
        0
      ],
      "id": "33333333-4444-4444-4444-333333333333",
      "name": "Get Draft Row",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Merge command context with the draft row, validate, build branch input.\n// For /redispatch, skip the idempotency state check \u2014 the command exists\n// specifically to re-fire GH dispatch after a failure (state will be 'dispatched'\n// from the prior failed attempt; allowing retry is the whole point).\nconst cmd = $('Parse Command').first().json;\nconst draft = $input.first().json;\n\nif (!draft || !draft.exec_id) {\n  throw new Error(`Draft not found for exec_id=${cmd.exec_id}`);\n}\n\nconst skipIdempotency = cmd.command === 'redispatch';\nif (!skipIdempotency && draft.state !== 'awaiting_review') {\n  return [{\n    json: {\n      ...cmd,\n      draft,\n      already_processed: true,\n      message: `Already ${draft.state}, skipping`,\n    },\n  }];\n}\n\nlet article;\ntry {\n  article = JSON.parse(draft.article_json);\n} catch (e) {\n  throw new Error(`Could not parse article_json: ${e.message}`);\n}\n\nreturn [{\n  json: {\n    ...cmd,\n    draft,\n    article,\n    already_processed: false,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        0
      ],
      "id": "44444444-5555-5555-5555-444444444444",
      "name": "Hydrate"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-already-processed",
                    "leftValue": "={{ $json.already_processed }}",
                    "rightValue": true,
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "Already processed"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-approve",
                    "leftValue": "={{ $json.command }}",
                    "rightValue": "approve",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "Approve"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-reject",
                    "leftValue": "={{ $json.command }}",
                    "rightValue": "reject",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "Reject"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-redispatch",
                    "leftValue": "={{ $json.command }}",
                    "rightValue": "redispatch",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "Redispatch"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1200,
        0
      ],
      "id": "55555555-6666-6666-6666-555555555555",
      "name": "Route Action"
    },
    {
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "=\u26a0\ufe0f \u042d\u0442\u0443 \u0441\u0442\u0430\u0442\u044c\u044e \u0443\u0436\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u0440\u0430\u043d\u0435\u0435 ({{ $json.draft.state }}). \u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0434\u0435\u043b\u0430\u044e.\n<i>exec_id: <code>{{ $json.exec_id }}</code></i>",
        "replyToMessageId": "={{ $json.reply_to_message_id }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1440,
        -300
      ],
      "id": "66666666-7777-7777-7777-666666666666",
      "name": "Telegram: Already Processed",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Build the GitHub workflow_dispatch payload that publish-article.yml expects.\n// publish-article.mjs validates excerpt to be 1\u2013200 chars, so we trim the lede\n// to its first sentence (then hard-clamp at 200).\nconst { article, exec_id } = $input.first().json;\n\nconst readMatch = (article.read || '').match(/(\\d+)/);\nconst read_minutes = readMatch ? readMatch[1] : '6';\n\nconst now = new Date();\nconst pad = (n) => String(n).padStart(2, '0');\nconst date = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}`;\n\nconst monthNames = ['January','February','March','April','May','June','July','August','September','October','November','December'];\nconst date_label = `${monthNames[now.getUTCMonth()]} ${now.getUTCFullYear()}`;\n\nconst lede = (article.body || []).find(b => b.type === 'lede');\nconst ledeText = lede ? lede.text : '';\nconst sentenceMatch = ledeText.match(/^[^.!?]+[.!?]/);\nconst firstSentence = sentenceMatch ? sentenceMatch[0].trim() : ledeText;\nconst excerpt = firstSentence.length > 200\n  ? firstSentence.slice(0, 197).trim() + '...'\n  : firstSentence;\n\nconst inputs = {\n  id: article.id,\n  category: article.category,\n  title: article.title,\n  excerpt,\n  body_json: JSON.stringify(article.body),\n  date,\n  date_label,\n  read_minutes,\n  featured: 'false',\n};\n\nif (article.hero_svg) {\n  inputs.hero_svg = article.hero_svg;\n  inputs.hero_alt = article.hero_alt || '';\n  inputs.hero_caption = article.hero_caption || '';\n}\n\nreturn [{\n  json: {\n    body: { ref: 'main', inputs },\n    exec_id,\n    article_id: article.id,\n    article_title: article.title,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        0
      ],
      "id": "77777777-8888-8888-8888-777777777777",
      "name": "Build Dispatch Payload"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.github.com/repos/puffy-pet/puffy-site/actions/workflows/publish-article.yml/dispatches",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2022-11-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {
          "redirect": {
            "redirect": {}
          },
          "response": {
            "response": {
              "fullResponse": true
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1680,
        0
      ],
      "id": "88888888-9999-9999-9999-888888888888",
      "name": "GitHub: Dispatch Action",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "drafts",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "exec_id": "={{ $('Parse Command').first().json.exec_id }}",
            "state": "dispatched"
          },
          "matchingColumns": [
            "exec_id"
          ],
          "schema": [
            {
              "id": "exec_id",
              "displayName": "exec_id",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": true,
              "required": false,
              "removed": false
            },
            {
              "id": "state",
              "displayName": "state",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "required": false,
              "removed": false
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1920,
        0
      ],
      "id": "99999999-aaaa-aaaa-aaaa-999999999999",
      "name": "Mark Draft Dispatched",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "content-plan"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "id": "={{ $('Hydrate').first().json.draft.topic_id }}",
            "published_url": "=dispatched-{{ $('Parse Command').first().json.exec_id }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": true,
              "required": false,
              "removed": false
            },
            {
              "id": "published_url",
              "displayName": "published_url",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": false,
              "required": false,
              "removed": false
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2160,
        0
      ],
      "id": "99999999-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
      "name": "Mark Topic Published",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "chatId": "={{ $('Parse Command').first().json.chat_id }}",
        "text": "=\ud83d\ude80 <b>\u041f\u0440\u0438\u043d\u044f\u0442\u043e \u2014 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044e \u0432 \u0431\u043b\u043e\u0433</b>\n\n<b>{{ $('Build Dispatch Payload').first().json.article_title }}</b>\n\n\u0427\u0435\u0440\u0435\u0437 ~1 \u043c\u0438\u043d\u0443\u0442\u0443 \u0432 \u0440\u0435\u043f\u043e <a href=\"https://github.com/puffy-pet/puffy-site/pulls\">puffy-site</a> \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f PR <code>auto/publish-{{ $('Build Dispatch Payload').first().json.article_id }}</code>. \u041f\u043e\u0441\u043c\u043e\u0442\u0440\u0438 \u0447\u0442\u043e \u0432\u044b\u0448\u043b\u043e, \u043f\u043e\u043f\u0440\u0430\u0432\u044c \u0435\u0441\u043b\u0438 \u0447\u0442\u043e \u2014 \u0438 \u043d\u0430\u0436\u043c\u0438 Merge. \u0427\u0435\u0440\u0435\u0437 \u043c\u0438\u043d\u0443\u0442\u0443 \u043f\u043e\u0441\u043b\u0435 merge \u0441\u0442\u0430\u0442\u044c\u044f \u0431\u0443\u0434\u0435\u0442 live.\n\n<i>exec_id: <code>{{ $('Parse Command').first().json.exec_id }}</code></i>",
        "replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML",
          "disable_web_page_preview": true
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        2400,
        0
      ],
      "id": "aaaaaaaa-bbbb-bbbb-bbbb-aaaaaaaaaaaa",
      "name": "Telegram: Approved",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "drafts",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "exec_id": "={{ $('Parse Command').first().json.exec_id }}",
            "state": "rejected"
          },
          "matchingColumns": [
            "exec_id"
          ],
          "schema": [
            {
              "id": "exec_id",
              "displayName": "exec_id",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": true,
              "required": false,
              "removed": false
            },
            {
              "id": "state",
              "displayName": "state",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false,
              "required": false,
              "removed": false
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1440,
        200
      ],
      "id": "bbbbbbbb-cccc-cccc-cccc-bbbbbbbbbbbb",
      "name": "Mark Draft Rejected",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "chatId": "={{ $('Parse Command').first().json.chat_id }}",
        "text": "=\u2717 <b>\u041e\u0442\u043a\u043b\u043e\u043d\u0438\u043b</b> \u2014 \u0441\u0442\u0430\u0442\u044c\u044f \u043d\u0435 \u043f\u043e\u0439\u0434\u0451\u0442 \u0432 \u0431\u043b\u043e\u0433. \u0422\u0435\u043c\u0430 \u043e\u0441\u0442\u0430\u043b\u0430\u0441\u044c \u0432 content-plan, \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u0437\u0430\u043f\u0443\u0441\u043a\u0435 Claude \u043d\u0430\u043f\u0438\u0448\u0435\u0442 \u0434\u0440\u0443\u0433\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442.\n\n<i>exec_id: <code>{{ $('Parse Command').first().json.exec_id }}</code></i>",
        "replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1680,
        200
      ],
      "id": "cccccccc-dddd-dddd-dddd-cccccccccccc",
      "name": "Telegram: Rejected",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "drafts",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        720,
        400
      ],
      "id": "11ee0001-1111-1111-1111-111111111111",
      "name": "Read All Drafts",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Build a formatted message + inline-keyboard for /list (or /queue).\n// Shows all drafts with state=awaiting_review. Each draft gets [\u2705 Publish]\n// [\u270f Edit] [\u2717 Reject] inline buttons (callback_data: act:<command>:<exec_id>).\n// Fall back to copyable exec_ids + text commands when queue is empty.\nconst rows = $input.all().map(r => r.json);\nconst awaiting = rows.filter(r => r.state === 'awaiting_review');\nconst cmd = $('Parse Command').first().json;\n\nfunction escapeHtml(s) {\n  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nif (awaiting.length === 0) {\n  return [{\n    json: {\n      chat_id: cmd.chat_id,\n      reply_to_message_id: cmd.reply_to_message_id,\n      text: '\ud83d\udced <b>\u041e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u0443\u0441\u0442\u0430.</b>\\n\\n\u041d\u0435\u0442 \u0434\u0440\u0430\u0444\u0442\u043e\u0432, \u0436\u0434\u0443\u0449\u0438\u0445 \u0440\u0435\u0432\u044c\u044e. 02a \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0441\u0442\u0430\u0442\u044c\u044e \u0432 08:00 \u041c\u0421\u041a (cron), \u043b\u0438\u0431\u043e \u043d\u0430\u0436\u043c\u0438 Execute workflow \u043d\u0430 02a \u0432 n8n, \u043b\u0438\u0431\u043e \u043e\u0442\u043f\u0440\u0430\u0432\u044c <code>/topic &lt;\u0442\u0435\u043c\u0430&gt;</code>, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043a\u0430\u0437\u0430\u0442\u044c \u0442\u0435\u043c\u0443 \u0441\u0435\u0439\u0447\u0430\u0441.',\n      keyboard_rows: [],\n    },\n  }];\n}\n\nconst MAX_CARDS = 8;\nconst cards = awaiting.slice(0, MAX_CARDS);\n\nconst sections = cards.map((d, i) => {\n  let title = '?';\n  let excerpt = '';\n  try {\n    const article = JSON.parse(d.article_json);\n    title = article.title || '?';\n    const lede = (article.body || []).find(b => b.type === 'lede');\n    if (lede) {\n      const sm = (lede.text || '').match(/^[^.!?]+[.!?]/);\n      excerpt = sm ? sm[0].trim() : lede.text;\n      if (excerpt.length > 200) excerpt = excerpt.slice(0, 197) + '...';\n    }\n  } catch (e) {}\n\n  return `<b>${i + 1}. ${escapeHtml(title)}</b>\\n<i>${escapeHtml(excerpt)}</i>\\n<code>${d.exec_id}</code>`;\n});\n\nlet text = `\ud83d\udfe1 <b>\u041e\u0447\u0435\u0440\u0435\u0434\u044c \u043d\u0430 \u0440\u0435\u0432\u044c\u044e (${awaiting.length})</b>\\n\\n${sections.join('\\n\\n')}`;\nif (awaiting.length > MAX_CARDS) {\n  text += `\\n\\n<i>\u2026 \u0438 \u0435\u0449\u0451 ${awaiting.length - MAX_CARDS}. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 exec_id \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e.</i>`;\n}\ntext += `\\n\\n\u0416\u043c\u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043f\u043e\u0434 \u043d\u0443\u0436\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0451\u0439, \u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439 <code>/edit &lt;exec_id&gt; &lt;field&gt;=&lt;value&gt;</code>, <code>/topic &lt;\u0442\u0435\u043c\u0430&gt;</code>.`;\n\n// Build keyboard_rows in n8n's nested fixedCollection shape:\n//   [{ row: { buttons: [{ text, additionalFields: { callback_data } }] } }, ...]\nconst keyboard_rows = cards.map((d, i) => ({\n  row: {\n    buttons: [\n      { text: `${i + 1}. \u2705 Publish`, additionalFields: { callback_data: `act:approve:${d.exec_id}` } },\n      { text: '\u270f Edit',              additionalFields: { callback_data: `act:edit:${d.exec_id}` } },\n      { text: '\u2717 Reject',            additionalFields: { callback_data: `act:reject:${d.exec_id}` } },\n    ],\n  },\n}));\n\nreturn [{\n  json: {\n    chat_id: cmd.chat_id,\n    reply_to_message_id: cmd.reply_to_message_id,\n    text,\n    keyboard_rows,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        400
      ],
      "id": "11ee0002-2222-2222-2222-222222222222",
      "name": "Format List"
    },
    {
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "={{ $json.text }}",
        "replyToMessageId": "={{ $json.reply_to_message_id }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": "={{ $json.keyboard_rows }}"
        },
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML",
          "disable_web_page_preview": true
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1200,
        400
      ],
      "id": "11ee0003-3333-3333-3333-333333333333",
      "name": "Send List",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// /topic <free text> \u2014 build a content-plan row for an ad-hoc topic.\n// Output keys must EXACTLY match content-plan column headers \u2014 autoMapInputData\n// in the next node maps by name. No extra keys (chat_id etc) \u2014 Telegram nodes\n// downstream pull those from $('Parse Command') directly.\nconst cmd = $('Parse Command').first().json;\nconst raw = (cmd.topic_text || '').trim();\n\nif (!raw) {\n  throw new Error('Empty topic text');\n}\n\nconst slug = raw\n  .toLowerCase()\n  .replace(/[^a-z0-9\\s-]/g, '')\n  .replace(/\\s+/g, '-')\n  .replace(/-+/g, '-')\n  .replace(/^-|-$/g, '')\n  .slice(0, 40);\n\nconst now = new Date();\nconst pad = (n) => String(n).padStart(2, '0');\nconst dateSuffix = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}`;\nconst id = `${slug || 'manual-topic'}-${dateSuffix}`;\n\nconst week_start = (() => {\n  const d = new Date(now);\n  const day = d.getUTCDay();\n  const diff = day === 0 ? -6 : 1 - day;\n  d.setUTCDate(d.getUTCDate() + diff);\n  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;\n})();\n\nreturn [{\n  json: {\n    id,\n    week_start,\n    title: raw,\n    cluster: 'Manual',\n    category: 'Guides',\n    target_keyword: raw,\n    search_intent: 'Manual entry from /topic command',\n    channels: 'blog,instagram,tiktok,x',\n    length: '10 min',\n    seasonal: 'FALSE',\n    rationale: `Manual /topic from marketing chat at ${now.toISOString()}`,\n    status: 'approved',\n    created_at: now.toISOString(),\n    published_url: '',\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        600
      ],
      "id": "11ee0004-4444-4444-4444-444444444444",
      "name": "Build Topic Row"
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "content-plan"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        960,
        600
      ],
      "id": "11ee0005-5555-5555-5555-555555555555",
      "name": "Append Topic to Content Plan",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://n8n-production-5692.up.railway.app/webhook/02a-manual-topic",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($('Build Topic Row').first().json) }}",
        "options": {
          "redirect": {
            "redirect": {}
          },
          "timeout": 5000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1200,
        600
      ],
      "id": "11ee0011-1111-1111-1111-111111111111",
      "name": "Trigger 02a Webhook",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "chatId": "={{ $('Parse Command').first().json.chat_id }}",
        "text": "=\ud83d\udcdd <b>\u0422\u0435\u043c\u0430 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430 02a.</b>\n\n<b>{{ $('Build Topic Row').first().json.title }}</b>\n<code>{{ $('Build Topic Row').first().json.id }}</code>\n\n\u0413\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u2014 \u043f\u0440\u0435\u0432\u044c\u044e \u043f\u0440\u0438\u043b\u0435\u0442\u0438\u0442 \u0432 \u0447\u0430\u0442 \u0447\u0435\u0440\u0435\u0437 ~1.5\u20132 \u043c\u0438\u043d\u0443\u0442\u044b (Wikipedia \u2192 draft \u2192 polish \u2192 hero SVG, ~$0.06).\n\n<i>\u0415\u0441\u043b\u0438 02a \u043d\u0435 \u043e\u0442\u0432\u0435\u0442\u0438\u0442 \u0437\u0430 3 \u043c\u0438\u043d \u2014 \u043e\u0442\u043a\u0440\u043e\u0439 /list, \u0438\u043b\u0438 \u0437\u0430\u0439\u0434\u0438 \u0432 n8n \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044c Executions \u0443 02a.</i>",
        "replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1440,
        600
      ],
      "id": "11ee0006-6666-6666-6666-666666666666",
      "name": "Telegram: Topic Added",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "drafts",
          "mode": "name"
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "exec_id",
              "lookupValue": "={{ $json.exec_id }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        720,
        800
      ],
      "id": "11ee0007-7777-7777-7777-777777777777",
      "name": "Get Draft for Edit",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Apply a single-field edit to a draft's article_json.\n// Allowed fields: title, category, excerpt (rewrites first lede block), read.\n// Anything else is rejected with a friendly Telegram error.\nconst cmd = $('Parse Command').first().json;\nconst draft = $input.first().json;\n\nif (!draft || !draft.exec_id) {\n  return [{\n    json: {\n      ...cmd,\n      ok: false,\n      error: `Draft <code>${cmd.exec_id}</code> not found.`,\n    },\n  }];\n}\n\nconst field = (cmd.edit_field || '').toLowerCase();\nconst value = cmd.edit_value || '';\n\nconst ALLOWED = ['title', 'category', 'excerpt', 'read'];\nif (!ALLOWED.includes(field)) {\n  return [{\n    json: {\n      ...cmd,\n      ok: false,\n      error: `\u041f\u043e\u043b\u0435 <code>${field}</code> \u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f. \u0420\u0430\u0437\u0440\u0435\u0448\u0451\u043d\u043d\u044b\u0435: ${ALLOWED.join(', ')}.`,\n    },\n  }];\n}\n\nlet article;\ntry {\n  article = JSON.parse(draft.article_json);\n} catch (e) {\n  return [{\n    json: {\n      ...cmd,\n      ok: false,\n      error: `\u041d\u0435 \u0441\u043c\u043e\u0433 \u0440\u0430\u0441\u043f\u0430\u0440\u0441\u0438\u0442\u044c article_json: ${e.message}`,\n    },\n  }];\n}\n\nlet oldValue;\nif (field === 'excerpt') {\n  // \"excerpt\" maps to the lede block text\n  const lede = (article.body || []).find(b => b.type === 'lede');\n  oldValue = lede ? lede.text : '';\n  if (lede) {\n    lede.text = value;\n  } else {\n    (article.body = article.body || []).unshift({ type: 'lede', text: value });\n  }\n} else {\n  oldValue = article[field];\n  article[field] = value;\n}\n\nreturn [{\n  json: {\n    ...cmd,\n    ok: true,\n    article,\n    article_json: JSON.stringify(article),\n    field,\n    old_value: String(oldValue || ''),\n    new_value: value,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        800
      ],
      "id": "11ee0008-8888-8888-8888-888888888888",
      "name": "Apply Edit"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-edit-ok",
                    "leftValue": "={{ $json.ok }}",
                    "rightValue": true,
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "ok"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-edit-failed",
                    "leftValue": "={{ $json.ok }}",
                    "rightValue": true,
                    "operator": {
                      "type": "boolean",
                      "operation": "false",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "failed"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1200,
        800
      ],
      "id": "11ee0009-9999-9999-9999-999999999999",
      "name": "Edit OK?"
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "157MDKV96QneeVdVlWqnC2kjjQrIKVsljJFFmrrwvRl0",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "drafts",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "exec_id": "={{ $json.exec_id }}",
            "article_json": "={{ $json.article_json }}"
          },
          "matchingColumns": [
            "exec_id"
          ],
          "schema": [
            {
              "id": "exec_id",
              "displayName": "exec_id",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": true,
              "required": false,
              "removed": false
            },
            {
              "id": "article_json",
              "displayName": "article_json",
              "type": "string",
              "display": true,
              "canBeUsedToMatch": false,
              "required": false,
              "removed": false
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1440,
        800
      ],
      "id": "11ee000a-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
      "name": "Update Draft Article",
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "chatId": "={{ $('Parse Command').first().json.chat_id }}",
        "text": "=\u270f <b>\u041f\u043e\u043b\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043e</b>\n\n<b>{{ $('Apply Edit').first().json.field }}</b>:\n<i>\u0431\u044b\u043b\u043e</i>: {{ $('Apply Edit').first().json.old_value }}\n<i>\u0441\u0442\u0430\u043b\u043e</i>: {{ $('Apply Edit').first().json.new_value }}\n\n<code>{{ $('Parse Command').first().json.exec_id }}</code>\n\n\u041f\u0440\u043e\u0432\u0435\u0440\u044c \u0438 \u0436\u043c\u0438 <code>/approve {{ $('Parse Command').first().json.exec_id }}</code> (\u0438\u043b\u0438 \u2705 Publish \u0438\u0437 <code>/list</code>).",
        "replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1680,
        800
      ],
      "id": "11ee000b-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
      "name": "Telegram: Edit Done",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "chatId": "={{ $('Parse Command').first().json.chat_id }}",
        "text": "=\u26a0\ufe0f <b>\u041d\u0435 \u0441\u043c\u043e\u0433 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c</b>\n\n{{ $json.error }}\n\n\u0424\u043e\u0440\u043c\u0430\u0442: <code>/edit &lt;exec_id&gt; &lt;field&gt;=&lt;value&gt;</code>\n\u041f\u0440\u0438\u043c\u0435\u0440: <code>/edit abc123 title=\"\u041d\u043e\u0432\u043e\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\"</code>\n\n\u041f\u043e\u043b\u044f: <code>title</code>, <code>category</code>, <code>excerpt</code>, <code>read</code>.",
        "replyToMessageId": "={{ $('Parse Command').first().json.reply_to_message_id }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1440,
        1000
      ],
      "id": "11ee000c-cccc-cccc-cccc-cccccccccccc",
      "name": "Telegram: Edit Failed",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "id": "rule-is-callback",
                    "leftValue": "={{ $json.source }}",
                    "rightValue": "callback_query",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "callback"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        240,
        200
      ],
      "id": "11ee000d-dddd-dddd-dddd-dddddddddddd",
      "name": "Is Callback?"
    },
    {
      "parameters": {
        "resource": "callback",
        "operation": "answerQuery",
        "queryId": "={{ $json.callback_query_id }}",
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        480,
        200
      ],
      "id": "11ee000e-eeee-eeee-eeee-eeeeeeeeeeee",
      "name": "Answer Callback Query",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "02d-claude-chat-puffy",
          "mode": "id"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.2,
      "position": [
        720,
        1200
      ],
      "id": "11ee0010-0000-0000-0000-000000000010",
      "name": "Call 02d Chat"
    }
  ],
  "connections": {
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Parse Command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Command": {
      "main": [
        [
          {
            "node": "Route Class",
            "type": "main",
            "index": 0
          },
          {
            "node": "Is Callback?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Callback?": {
      "main": [
        [
          {
            "node": "Answer Callback Query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Class": {
      "main": [
        [
          {
            "node": "Get Draft Row",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Read All Drafts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Topic Row",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Draft for Edit",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Call 02d Chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Draft Row": {
      "main": [
        [
          {
            "node": "Hydrate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hydrate": {
      "main": [
        [
          {
            "node": "Route Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Action": {
      "main": [
        [
          {
            "node": "Telegram: Already Processed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Dispatch Payload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Draft Rejected",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Dispatch Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Dispatch Payload": {
      "main": [
        [
          {
            "node": "GitHub: Dispatch Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GitHub: Dispatch Action": {
      "main": [
        [
          {
            "node": "Mark Draft Dispatched",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Draft Dispatched": {
      "main": [
        [
          {
            "node": "Mark Topic Published",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Topic Published": {
      "main": [
        [
          {
            "node": "Telegram: Approved",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Draft Rejected": {
      "main": [
        [
          {
            "node": "Telegram: Rejected",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read All Drafts": {
      "main": [
        [
          {
            "node": "Format List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format List": {
      "main": [
        [
          {
            "node": "Send List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Topic Row": {
      "main": [
        [
          {
            "node": "Append Topic to Content Plan",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Topic to Content Plan": {
      "main": [
        [
          {
            "node": "Trigger 02a Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Trigger 02a Webhook": {
      "main": [
        [
          {
            "node": "Telegram: Topic Added",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Draft for Edit": {
      "main": [
        [
          {
            "node": "Apply Edit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply Edit": {
      "main": [
        [
          {
            "node": "Edit OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit OK?": {
      "main": [
        [
          {
            "node": "Update Draft Article",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram: Edit Failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Draft Article": {
      "main": [
        [
          {
            "node": "Telegram: Edit Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "versionId": "00000000-0000-0000-0000-000000000020",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "02b-article-callback-puffy",
  "tags": []
}