{
  "nodes": [
    {
      "id": "f56b36cc-07c2-4b30-9ccd-14cbf024e24e",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        480,
        512
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 0 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a3db91a5-2081-4652-b10d-2eae58b67fc1",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        704,
        512
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={\n  \"includeSourceLink\": true,\n  \"waitTimeoutSeconds\": 15,\n  \"maxTitleLength\": 100,\n  \"categoryId\": \"22\",\n  \"privacyStatus\": \"public\",\n  \"notifySubscribers\": false,\n  \"defaultLanguage\": \"en\",\n  \"ageRestricted\": false\n}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "cd666bdb-afb2-4581-94fe-1a890594e15c",
      "name": "Fetch Instagram Posts",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        512
      ],
      "parameters": {
        "url": "https://graph.instagram.com/me/media",
        "options": {},
        "jsonQuery": "={\n  \"fields\": \"id,caption,media_url,media_type,timestamp,permalink\",\n  \"limit\": \"25\"\n}",
        "sendQuery": true,
        "specifyQuery": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "alwaysOutputData": true
    },
    {
      "id": "f8823bb8-e746-4afc-9e1f-82467e790fcf",
      "name": "Split Out",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        1200,
        512
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "data"
      },
      "typeVersion": 1
    },
    {
      "id": "25e84b3b-2ba8-406c-af40-db775c423eb5",
      "name": "Filter Videos Only",
      "type": "n8n-nodes-base.filter",
      "position": [
        1424,
        512
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "video-check",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.media_type }}",
              "rightValue": "VIDEO"
            },
            {
              "id": "reels-check",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.media_type }}",
              "rightValue": "REELS"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "79ca0127-80ec-4783-beb6-751640786cc0",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1648,
        512
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "8fa6eac8-accd-42bf-8d51-645a37e06bff",
      "name": "Check If Already Uploaded",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        1872,
        576
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "postId",
              "keyValue": "={{ $json.id }}"
            }
          ]
        },
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "oOxou5oOQXuS4cQY",
          "cachedResultUrl": "/projects/vZdjtqHcrLoO32Ib/datatables/oOxou5oOQXuS4cQY",
          "cachedResultName": "Instagram To YouTube"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "49f0159a-e36a-4a31-a708-5fe93638707f",
      "name": "If Not Uploaded Yet",
      "type": "n8n-nodes-base.if",
      "position": [
        2096,
        576
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "eab5894a-31d7-43f4-8866-104c416eed3f",
              "operator": {
                "type": "string",
                "operation": "notExists",
                "singleValue": true
              },
              "leftValue": "={{ $json.postId }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "d4cf02f7-c2c3-469a-a37d-5f699b14a7c9",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "position": [
        2320,
        512
      ],
      "parameters": {
        "amount": "={{ $('Configuration').item.json.waitTimeoutSeconds }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "a392de42-c87f-4ed9-8987-25c671cbc7e8",
      "name": "Process Title and Tags",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        512
      ],
      "parameters": {
        "jsCode": "// Get configuration from the Configuration node\nconst config = $('Configuration').first().json;\nconst includeSourceLink = config.includeSourceLink;\nconst MAX_TITLE_LENGTH = config.maxTitleLength || 100;\n\n// Get the post data from the Loop Over Items node\nconst caption = $('Loop Over Items').item.json.caption || '';\nconst postId = $('Loop Over Items').item.json.id;\nconst mediaUrl = $('Loop Over Items').item.json.media_url;\nconst permalink = $('Loop Over Items').item.json.permalink;\n\n// Extract hashtags\nconst hashtagRegex = /#[\\w\\u00C0-\\u024F]+/gu;\nconst hashtags = caption.match(hashtagRegex) || [];\n\n// Get text before first hashtag\nconst firstHashtagIndex = caption.indexOf('#');\nlet textBeforeHashtags = firstHashtagIndex === -1 \n  ? caption.trim() \n  : caption.substring(0, firstHashtagIndex).trim();\n\n// Build title\nlet title = textBeforeHashtags;\n\n// If no text before hashtags, use first hashtag\nif (!title && hashtags.length > 0) {\n  title = hashtags[0].replace('#', '');\n}\n\n// If still no title, use generic\nif (!title) {\n  title = 'Instagram Video';\n}\n\n// Smart truncation at word boundary\nif (title.length > MAX_TITLE_LENGTH) {\n  title = title.substring(0, MAX_TITLE_LENGTH);\n  const lastSpace = title.lastIndexOf(' ');\n  if (lastSpace > MAX_TITLE_LENGTH * 0.6) {\n    title = title.substring(0, lastSpace);\n  }\n  title = title.trim() + '...';\n}\n\n// Build description - conditionally include source link\nlet description = caption;\n\nif (includeSourceLink) {\n  description += `\\n\\n---\\nOriginally posted on Instagram: ${permalink}`;\n}\n\n// Extract tags for YouTube (without # symbol, max 10)\nconst tags = hashtags\n  .map(h => h.replace('#', ''))\n  .filter(t => t.length > 1 && t.length <= 30)\n  .slice(0, 10);\n\n// Return processed data\nreturn [{\n  json: {\n    postId: postId,\n    title: title,\n    description: description,\n    tags: tags,\n    mediaUrl: mediaUrl,\n    originalCaption: caption,\n    hashtagCount: hashtags.length,\n    sourceLinkIncluded: includeSourceLink\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4b1118e5-8b2e-4f1e-82e7-8d3dad1918aa",
      "name": "Download Video",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2768,
        512
      ],
      "parameters": {
        "url": "={{ $json.mediaUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "b7f81c01-978c-48b0-8b53-0f71d610fd13",
      "name": "Upload to YouTube",
      "type": "n8n-nodes-base.youTube",
      "position": [
        2992,
        512
      ],
      "parameters": {
        "title": "={{ $('Process Title and Tags').item.json.title }}",
        "options": {
          "tags": "={{ $('Process Title and Tags').item.json.tags.join(',') }}",
          "license": "youtube",
          "embeddable": true,
          "description": "={{ $('Process Title and Tags').item.json.description }}",
          "privacyStatus": "={{ $('Configuration').item.json.privacyStatus }}",
          "defaultLanguage": "={{ $('Configuration').item.json.defaultLanguage }}",
          "notifySubscribers": "={{ $('Configuration').item.json.notifySubscribers }}",
          "publicStatsViewable": true,
          "selfDeclaredMadeForKids": false
        },
        "resource": "video",
        "operation": "upload",
        "categoryId": "={{ $('Configuration').item.json.categoryId }}",
        "regionCode": "US"
      },
      "credentials": {
        "youTubeOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "72339ade-f7d9-4950-8946-6f63192625f3",
      "name": "Save Upload Record",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        3216,
        512
      ],
      "parameters": {
        "columns": {
          "value": {
            "postId": "={{ $('Process Title and Tags').item.json.postId }}",
            "youtubeId": "={{ $json.id }}"
          },
          "schema": [
            {
              "id": "postId",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "postId",
              "defaultMatch": false
            },
            {
              "id": "youtubeId",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "youtubeId",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "postId"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "oOxou5oOQXuS4cQY",
          "cachedResultUrl": "/projects/vZdjtqHcrLoO32Ib/datatables/oOxou5oOQXuS4cQY",
          "cachedResultName": "Instagram To YouTube"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f99eaf5e-218f-4121-8711-e88de0b65983",
      "name": "Done",
      "type": "n8n-nodes-base.set",
      "position": [
        1648,
        320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ffd3b7ab-4ac6-4c71-8ee1-a6c93732a701",
              "name": "status",
              "type": "string",
              "value": "Workflow completed"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "78ebe9fc-71af-4285-ba65-63029674a12a",
      "name": "Sticky Note - Instructions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -256,
        96
      ],
      "parameters": {
        "width": 678,
        "height": 1104,
        "content": "## Instagram to YouTube Uploader\n\nAutomatically upload Instagram videos to YouTube with scheduled intervals.\n\n### Why This Template?\nThis workflow posts videos at a set time, then progressively uploads remaining content with configurable gaps between each upload. It's a simpler alternative to managing a spreadsheet-based publishing queue - all configuration lives in one node.\n\n### Configuration Options\nEdit the **Configuration** node:\n\n```json\n{\n  \"includeSourceLink\": true,    // Instagram link in description\n  \"waitTimeoutSeconds\": 900,    // Gap between uploads (seconds)\n  \"maxTitleLength\": 100,        // Max title length\n  \"categoryId\": \"24\",           // YouTube category\n  \"privacyStatus\": \"public\",    // public/private/unlisted\n  \"notifySubscribers\": false,   // Notify on upload\n  \"defaultLanguage\": \"en\",      // Video language\n  \"ageRestricted\": false        // 18+ content flag\n}\n```\n\n### Key Settings Explained\n- **includeSourceLink**: Set `false` for unverified accounts that can't add external links\n- **waitTimeoutSeconds**: Gap between uploads (900 = 15 min)\n- **ageRestricted**: Set `true` for 18+ content\n\n### Features\n- Video-only filter (skips images)\n- Hashtag to YouTube tags extraction\n- Deduplication via Data Table\n- COPPA compliance options\n\n### Setup\n1. Edit **Configuration** node\n2. Set **Bearer Auth** in **Fetch Instagram Posts**\n3. Create Data Table: `postId`, `youtubeId`\n4. Set **YouTube OAuth2** in **Upload to YouTube**\n\n### Category IDs\n| ID | Category |\n|----|----------|\n| 22 | People & Blogs |\n| 24 | Entertainment |\n| 20 | Gaming |\n| 10 | Music |"
      },
      "typeVersion": 1
    },
    {
      "id": "f37d44ef-5e72-4562-b30b-92f9c07e05c1",
      "name": "Sticky Note - Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        432
      ],
      "parameters": {
        "color": 5,
        "width": 256,
        "height": 224,
        "content": "## Configuration"
      },
      "typeVersion": 1
    },
    {
      "id": "ce3781f6-19f2-41a7-b214-aababc076914",
      "name": "Sticky Note - Fetch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        944,
        432
      ],
      "parameters": {
        "color": 6,
        "width": 500,
        "height": 304,
        "content": "## Fetch & Filter Videos\n"
      },
      "typeVersion": 1
    },
    {
      "id": "08766e8e-9bcf-4b98-a482-a3475d3fa16f",
      "name": "Sticky Note - Check",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1840,
        432
      ],
      "parameters": {
        "color": 7,
        "width": 388,
        "height": 384,
        "content": "## Deduplication"
      },
      "typeVersion": 1
    },
    {
      "id": "0f1a89af-347a-41c2-8037-7678d2c3600d",
      "name": "Sticky Note - Upload",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2512,
        432
      ],
      "parameters": {
        "color": 3,
        "width": 740,
        "height": 304,
        "content": "## Process & Upload"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Wait": {
      "main": [
        [
          {
            "node": "Process Title and Tags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out": {
      "main": [
        [
          {
            "node": "Filter Videos Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Fetch Instagram Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Video": {
      "main": [
        [
          {
            "node": "Upload to YouTube",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check If Already Uploaded",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to YouTube": {
      "main": [
        [
          {
            "node": "Save Upload Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Videos Only": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Upload Record": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Not Uploaded Yet": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Instagram Posts": {
      "main": [
        [
          {
            "node": "Split Out",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Title and Tags": {
      "main": [
        [
          {
            "node": "Download Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check If Already Uploaded": {
      "main": [
        [
          {
            "node": "If Not Uploaded Yet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}