{
  "id": "6SOSDrfhrqAX7DBS",
  "name": "Deduplicate and auto-post X videos with AI captions from a user",
  "tags": [],
  "nodes": [
    {
      "id": "b4721eab-d2f9-4687-be1b-15ef709f4eb9",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "\u8a2d\u5b9a\u3057\u305f\u6642\u9593\u9593\u9694\u3067\u30ef\u30fc\u30af\u30d5\u30ed\u30fc\u3092\u81ea\u52d5\u7684\u306b\u958b\u59cb\u3059\u308b\u30c8\u30ea\u30ac\u30fc\u3067\u3059\u3002",
      "position": [
        -160,
        256
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ced6b0eb-8a23-4a07-aba2-140f9b1c0480",
      "name": "Get User ID",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "\u6307\u5b9a\u30e6\u30fc\u30b6\u30fc\u540d\u304b\u3089 API \u3067\u5229\u7528\u3059\u308b user.id \u3092\u53d6\u5f97\u3057\u307e\u3059\u3002",
      "position": [
        64,
        256
      ],
      "parameters": {
        "url": "https://api.twitter.com/2/users/by/username/XXXXXXXXX",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "n8n-workflow"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "fd64a087-90ed-43c9-8305-358dd1b58b40",
      "name": "Get Tweets with Videos",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "\u30e6\u30fc\u30b6\u30fc\u306e\u6700\u65b0\u30c4\u30a4\u30fc\u30c8\uff08\u30e1\u30c7\u30a3\u30a2\u542b\u3080\uff09\u3092\u53d6\u5f97\u3057\u307e\u3059\u3002",
      "position": [
        288,
        256
      ],
      "parameters": {
        "url": "={{ 'https://api.twitter.com/2/users/' + $json.data.id + '/tweets?max_results=10&tweet.fields=attachments&expansions=attachments.media_keys&media.fields=type,url,variants' }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "69b14da0-3280-4c55-bc50-c4b7f355af23",
      "name": "Filter Video Tweets",
      "type": "n8n-nodes-base.code",
      "notes": "\u52d5\u753b\u30e1\u30c7\u30a3\u30a2\u306e\u307f\u62bd\u51fa\u3057\u307e\u3059\u3002",
      "position": [
        512,
        256
      ],
      "parameters": {
        "jsCode": "const tweets = $input.first().json.data || [];\nconst includes = $input.first().json.includes || {};\nconst media = includes.media || [];\nconst out = [];\nfor (const t of tweets) {\n  const keys = (t.attachments && t.attachments.media_keys) || [];\n  const hasVideo = keys.some(k => (media.find(m => m.media_key===k) || {}).type === 'video');\n  if (hasVideo) {\n    out.push({ tweet_id: t.id, text: t.text, url: `https://twitter.com/i/status/${t.id}` });\n  }\n}\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "5ae1e3f9-4821-41ea-b88e-c9041d7a933a",
      "name": "Check Existing URLs",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "\u5c65\u6b74\u7ba1\u7406\u306e\u305f\u3081\u30b7\u30fc\u30c8\u306b\u8ffd\u8a18\u3057\u307e\u3059\u3002",
      "position": [
        736,
        256
      ],
      "parameters": {
        "columns": {
          "value": {
            "URL": "={{ $json.url }}",
            "\u6587\u7ae0": "={{ $json.text }}",
            "\u30c4\u30a4\u30fc\u30c8ID": "={{ $json.tweet_id }}"
          },
          "schema": [
            {
              "id": "\u30c4\u30a4\u30fc\u30c8ID",
              "type": "string",
              "displayName": "\u30c4\u30a4\u30fc\u30c8ID",
              "canBeUsedToMatch": true
            },
            {
              "id": "\u6587\u7ae0",
              "type": "string",
              "displayName": "\u6587\u7ae0",
              "canBeUsedToMatch": true
            },
            {
              "id": "URL",
              "type": "string",
              "displayName": "URL",
              "canBeUsedToMatch": true
            },
            {
              "id": "setURL",
              "type": "string",
              "displayName": "setURL",
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ]
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 89155752,
          "cachedResultName": "\u66f4\u65b0\u60c5\u5831"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "19pJoBuba1o2PaJlx0inaGnGi45PpxbfB79MMhwLFH4s",
          "cachedResultName": "1.\u6295\u7a3f\u81ea\u52d5\u53ce\u96c6"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "d974879e-f343-4375-b521-4ff6f5780a9e",
      "name": "Edit Fields",
      "type": "n8n-nodes-base.set",
      "notes": "URL \u3092\u5f15\u7528\u52d5\u753b\u5f62\u5f0f\u3078\u5909\u63db\u3057\u307e\u3059\u3002",
      "position": [
        960,
        256
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1",
              "name": "=setURL",
              "type": "string",
              "value": "={{ $json.URL }}/video/1"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "325148aa-46c7-447c-badf-5193c567e622",
      "name": "Append or update row in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "setURL \u3092 upsert \u3057\u307e\u3059\u3002",
      "position": [
        1184,
        256
      ],
      "parameters": {
        "columns": {
          "value": {
            "setURL": "={{ $json.setURL }}",
            "\u30c4\u30a4\u30fc\u30c8ID": "={{ $('Check Existing URLs').item.json['\u30c4\u30a4\u30fc\u30c8ID'] }}"
          },
          "schema": [
            {
              "id": "\u30c4\u30a4\u30fc\u30c8ID",
              "type": "string",
              "displayName": "\u30c4\u30a4\u30fc\u30c8ID",
              "canBeUsedToMatch": true
            },
            {
              "id": "setURL",
              "type": "string",
              "displayName": "setURL",
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "\u30c4\u30a4\u30fc\u30c8ID"
          ]
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 89155752,
          "cachedResultName": "\u66f4\u65b0\u60c5\u5831"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "=19pJoBuba1o2PaJlx0inaGnGi45PpxbfB79MMhwLFH4s"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "380ba318-3a78-488e-9b63-4f820a56d568",
      "name": "Check New Videos",
      "type": "n8n-nodes-base.if",
      "notes": "\u672a\u5b8c\u4e86\u306e\u307f\u6b21\u3078\u6d41\u3057\u307e\u3059\u3002",
      "position": [
        1408,
        256
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1",
              "operator": {
                "type": "string",
                "operation": "notEqual"
              },
              "leftValue": "={{ $('Append or update row in sheet').item.json['\u9054\u6210\u72b6\u6cc1'] || '' }}",
              "rightValue": "\u5b8c\u4e86"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "5c7f361a-2e63-4eca-9107-a8d7135194f3",
      "name": "Generate Tweet Text",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "notes": "AI \u304c\u6295\u7a3f\u6587\u3092\u751f\u6210\u3057\u307e\u3059\u3002",
      "position": [
        1632,
        256
      ],
      "parameters": {
        "text": "={{ $json.text }}\n\n\u3053\u306e\u5185\u5bb9\u306b\u57fa\u3065\u3044\u3066\u3001\u9b45\u529b\u7684\u306a\u6295\u7a3f\u6587\u3092\u65e5\u672c\u8a9e\u3067\u751f\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\u4ee5\u4e0b\u306e\u8981\u7d20\u3092\u542b\u3081\u3066\u304f\u3060\u3055\u3044\uff1a\n- \u7c21\u6f54\u3067\u8208\u5473\u3092\u5f15\u304f\u8aac\u660e\n- \u9069\u5207\u306a\u30cf\u30c3\u30b7\u30e5\u30bf\u30b0\uff083-5\u500b\uff09\n- \u30a8\u30f3\u30b2\u30fc\u30b8\u30e1\u30f3\u30c8\u3092\u4fc3\u3059\u8981\u7d20\n\n\u6587\u5b57\u6570\u306f100-150\u6587\u5b57\u7a0b\u5ea6\u3067\u3001\u6700\u5f8c\u306b\u52d5\u753bURL\u3092\u914d\u7f6e\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
        "options": {
          "systemMessage": "\u3042\u306a\u305f\u306f\u30bd\u30fc\u30b7\u30e3\u30eb\u30e1\u30c7\u30a3\u30a2\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u4f5c\u6210\u306e\u5c02\u9580\u5bb6\u3067\u3059\u3002\u63d0\u4f9b\u3055\u308c\u305f\u5185\u5bb9\u304b\u3089\u9b45\u529b\u7684\u3067\u5171\u6709\u3055\u308c\u3084\u3059\u3044\u6295\u7a3f\u6587\u3092\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002"
        },
        "promptType": "define"
      },
      "typeVersion": 2
    },
    {
      "id": "44c907e2-890c-4d6e-beb4-34c0bfc22e8d",
      "name": "Post to X",
      "type": "n8n-nodes-base.twitter",
      "notes": "\u81ea\u52d5\u3067 X \u306b\u6295\u7a3f\u3057\u307e\u3059\u3002",
      "position": [
        1984,
        256
      ],
      "parameters": {
        "text": "={{ $json.output }}\n\n{{ $('Append or update row in sheet').item.json.setURL }}",
        "additionalFields": {}
      },
      "typeVersion": 2
    },
    {
      "id": "16e954d5-8e49-43f8-94a1-90b867531bdc",
      "name": "Update Spreadsheet",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "\u6295\u7a3f\u5b8c\u4e86\u3092\u30de\u30fc\u30af\u3057\u307e\u3059\u3002",
      "position": [
        2208,
        256
      ],
      "parameters": {
        "columns": {
          "value": {
            "\u9054\u6210\u72b6\u6cc1": "\u5b8c\u4e86",
            "\u30c4\u30a4\u30fc\u30c8ID": "={{ $('Append or update row in sheet').item.json['\u30c4\u30a4\u30fc\u30c8ID'] }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "\u30c4\u30a4\u30fc\u30c8ID"
          ]
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 89155752,
          "cachedResultName": "\u66f4\u65b0\u60c5\u5831"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "19pJoBuba1o2PaJlx0inaGnGi45PpxbfB79MMhwLFH4s"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "ea0648dc-eda5-417c-a3e1-d5ca7b925c20",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "notes": "LLM \u306e\u8cc7\u683c\u60c5\u5831\u3092\u53c2\u7167\u3057\u307e\u3059\u3002",
      "position": [
        1696,
        480
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "f850486d-8ec9-4a1a-842e-46ca8a92c1b2",
      "name": "\ud83d\udfe8 Sticky: Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -704,
        32
      ],
      "parameters": {
        "color": "yellow",
        "width": 396,
        "height": 260,
        "content": "## Template Overview (Read Me First)\n**Purpose**: Fetch recent X videos from a target user, generate a fresh caption with AI, and auto-post to your X account. All runs are logged to Google Sheets for deduping & auditing.\n\n### Requirements\n- X (Twitter) OAuth2 credential\n- Google Sheets OAuth2 credential\n- OpenRouter credential (LLM)\n\n### Flow\n1) Schedule \u2192 2) Get user.id \u2192 3) Get tweets (with media) \u2192 4) Filter videos \u2192 5) Append to Sheet \u2192\n6) Build shareable `setURL` \u2192 7) AI caption \u2192 8) Post to X \u2192 9) Mark `\u9054\u6210\u72b6\u6cc1=\u5b8c\u4e86`.\n\n### Security\n- Never hardcode tokens in HTTP nodes\u2014use **Credentials**.\n- Replace demo Sheet with your own; remove personal IDs before publishing.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "2e27e014-ffbe-4cbb-ba7a-e4ecb7016121",
      "name": "Sticky: Schedule Trigger",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -256,
        -16
      ],
      "parameters": {
        "color": "white",
        "width": 268,
        "height": 260,
        "content": "## Schedule Trigger \u2014 How to use\n- Polling cadence: e.g., every **1\u20133 hours** (interval) or switch to **Cron** for fine control.\n- Consider API rate limits & off-peak scheduling.\n- For manual runs during tests, click **Execute Workflow**.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "ff7793b2-6ed8-4dd9-8cf9-bf094084beff",
      "name": "Sticky: Get User ID",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        432
      ],
      "parameters": {
        "color": "white",
        "width": 300,
        "height": 308,
        "content": "## Get User ID (HTTP Request)\n- Endpoint: `/2/users/by/username/:handle` returns `data.id`.\n- **Action**: Replace the hardcoded username with your target handle (or feed it via a Set node / env var).\n- **Creds**: Use **Twitter OAuth2**. Do not paste bearer tokens directly.\n- Output fields used later: `data.id`.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "27854651-0068-49d9-87f8-06f4523bf950",
      "name": "Sticky: Get Tweets with Videos",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        -48
      ],
      "parameters": {
        "color": "white",
        "width": 380,
        "height": 292,
        "content": "## Get Tweets with Videos (HTTP Request)\n- Endpoint: `/2/users/:id/tweets` with expansions `attachments.media_keys` and `media.fields`.\n- Adjust `max_results` (5\u201320). Add `since_id` or `start_time` if you want stricter deduping.\n- Output contains `data[]`, `includes.media[]` for downstream filtering.\n- Tip: Use `tweet.fields=created_at` if you need time-based rules.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "4f5b8177-7605-40b0-9d7a-b714d21155e0",
      "name": "Sticky: Filter Video Tweets",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        -16
      ],
      "parameters": {
        "color": "white",
        "width": 300,
        "height": 260,
        "content": "## Filter Video Tweets (Code)\n- Filters items where any attached media has `type === 'video'`.\n- To include GIF videos, also accept `animated_gif`.\n- Emits one item per video tweet with `{ tweet_id, text, url }`.\n- Customize: add language filters, exclude replies/retweets, etc.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "65afa322-a9e1-464b-8cee-1fcccb4222b6",
      "name": "Sticky: Check Existing URLs",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        432
      ],
      "parameters": {
        "color": "white",
        "width": 396,
        "height": 260,
        "content": "## Check Existing URLs (Google Sheets \u2014 Append)\n- Appends `{ \u30c4\u30a4\u30fc\u30c8ID, \u6587\u7ae0, URL }` to your Sheet to maintain a registry.\n- Replace `documentId/sheetName` with your own copy.\n- Use this sheet to prevent duplicates and enable audit trails.\n- Tip: Add timestamp & account columns for more context.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "b3a11fdc-4781-41db-83d7-4b95cc5bd080",
      "name": "Sticky: Edit Fields",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        0
      ],
      "parameters": {
        "color": "white",
        "width": 284,
        "height": 260,
        "content": "## Edit Fields (Set)\n- Builds `setURL = {{URL}}/video/1` to point to the video playback UI.\n- This is optimized for native X videos; external links may require custom rules.\n- You can add UTM params here for analytics (e.g., `?utm_source=n8n`).\n"
      },
      "typeVersion": 1
    },
    {
      "id": "a34e1c1d-e631-4de1-baad-818a28dad740",
      "name": "Sticky: Append/Update (setURL)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1136,
        480
      ],
      "parameters": {
        "color": "white",
        "width": 332,
        "height": 260,
        "content": "## Append or update row in sheet (Google Sheets \u2014 Upsert)\n- Upserts `setURL` by key `\u30c4\u30a4\u30fc\u30c8ID` (**matchingColumns**).\n- Ensures the shareable URL is stored for posting and logging.\n- Remove real Sheet IDs before publishing a public template.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "064badd6-d0e8-411c-9170-e814952123c0",
      "name": "Sticky: Check New Videos",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        -32
      ],
      "parameters": {
        "color": "white",
        "width": 284,
        "height": 260,
        "content": "## Check New Videos (IF)\n- Condition: proceed only when `\u9054\u6210\u72b6\u6cc1` is not `'\u5b8c\u4e86'` (or row missing).\n- This prevents reposting the same tweet repeatedly.\n- Customize: add checks like age (`created_at`) or language before posting.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "9d337b5b-8b19-4702-9119-c70f3ab977a6",
      "name": "Sticky: OpenRouter Chat Model",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        608
      ],
      "parameters": {
        "color": "white",
        "width": 348,
        "height": 260,
        "content": "## OpenRouter Chat Model (Credential)\n- Select model (cost/quality tradeoff). Examples: `gpt-4o-mini`, etc.\n- Configure **OpenRouter** credentials in n8n \u2192 Credentials.\n- Optional: set retry rules in workflow settings for robustness.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3204d240-cf45-421b-8f37-43b2aede2bf7",
      "name": "Sticky: Generate Tweet Text",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        -32
      ],
      "parameters": {
        "color": "white",
        "width": 268,
        "height": 260,
        "content": "## Generate Tweet Text (AI Agent)\n- Prompt: 100\u2013150 chars, 3\u20135 hashtags, and an engagement CTA in Japanese.\n- Provide brand voice via constants (Set node) or via the Sheet.\n- Add compliance rules as needed (e.g., disclaimers, \u7981\u6b62\u8a9e\u8f9e\u66f8).\n"
      },
      "typeVersion": 1
    },
    {
      "id": "f26c937b-e425-4e59-bcc6-4e3b3d63145e",
      "name": "Sticky: Post to X",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1920,
        -16
      ],
      "parameters": {
        "color": "white",
        "width": 252,
        "height": 260,
        "content": "## Post to X (Twitter)\n- Posts `output` from AI plus the `setURL` generated earlier.\n- Test with a sandbox account to validate formatting & line breaks.\n- Consider adding a **dry-run** boolean to skip posting during tests.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "96c75931-3078-40b8-a730-8ae0f4e005e3",
      "name": "Sticky: Update Spreadsheet",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2192,
        400
      ],
      "parameters": {
        "color": "white",
        "width": 252,
        "height": 276,
        "content": "## Update Spreadsheet (Google Sheets)\n- Upserts `\u9054\u6210\u72b6\u6cc1 = \u5b8c\u4e86` for the posted `\u30c4\u30a4\u30fc\u30c8ID`.\n- Add a `posted_at` timestamp and `posted_text` columns for complete logs.\n- This supports idempotency and future analytics.\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "1068413b-38d0-45c7-ac4f-81edd645b61a",
  "connections": {
    "Post to X": {
      "main": [
        [
          {
            "node": "Update Spreadsheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Append or update row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get User ID": {
      "main": [
        [
          {
            "node": "Get Tweets with Videos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check New Videos": {
      "main": [
        [
          {
            "node": "Generate Tweet Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get User ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Existing URLs": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Video Tweets": {
      "main": [
        [
          {
            "node": "Check Existing URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Tweet Text": {
      "main": [
        [
          {
            "node": "Post to X",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Tweet Text",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Get Tweets with Videos": {
      "main": [
        [
          {
            "node": "Filter Video Tweets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append or update row in sheet": {
      "main": [
        [
          {
            "node": "Check New Videos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}