{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "Import CSV or Excel and schedule social posts every 5 hours with PinFlow",
  "tags": [],
  "nodes": [
    {
      "id": "befda17c-30da-41c3-924a-ee00461ee83b",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1584,
        -176
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Import CSV or Excel and schedule social posts every 5 hours with PinFlow\n\n### How it works\n\nThis workflow lets a user upload a CSV, XLSX, or XLS campaign file and imports its rows for social media scheduling. It validates the rows, spaces scheduled content on a five-hour cadence, then loops through each row to download its image and upload it to PinFlow. For each valid row, it schedules corresponding Pinterest, Instagram, and Facebook posts before moving to the next row.\n\n### Setup steps\n\n- Configure the form trigger so it accepts the campaign spreadsheet as a binary file upload.\n- Ensure the spreadsheet has the columns expected by the validation code, such as image URL, post text/caption, destination account or board fields, links, and any timing-related fields used by the workflow.\n- Set up PinFlow credentials on all PinFlow nodes and verify the selected Pinterest, Instagram, and Facebook destinations are available.\n- Review the extract-from-file nodes to confirm each file format reads the correct sheet, delimiter, headers, and binary property from the upload.\n- Test with a small campaign file first to confirm images download successfully and posts are scheduled at the intended five-hour interval.\n\n### Customization\n\nAdjust the validation code to change required columns, default values, or the five-hour scheduling interval. The platform scheduling chain can also be modified to add, remove, or reorder social networks."
      },
      "typeVersion": 1
    },
    {
      "id": "8332b61f-2c62-4c10-aa51-850b9eb7e625",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1024,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 352,
        "content": "## Upload and detect file\n\nStarts the workflow from a form upload and routes the submitted campaign file based on whether it is CSV, XLSX, or XLS."
      },
      "typeVersion": 1
    },
    {
      "id": "eb10d2b2-ace2-46fe-9837-c9c453a05b2e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -544,
        -176
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 784,
        "content": "## Extract spreadsheet rows\n\nParses the uploaded file using the matching extractor for CSV, XLSX, or XLS formats and outputs campaign rows for downstream processing."
      },
      "typeVersion": 1
    },
    {
      "id": "02cdc1e7-4065-4c15-a1fa-6cc441437b4c",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -272,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 320,
        "content": "## Validate and loop rows\n\nValidates the imported campaign data, calculates the scheduled posting sequence, and iterates through each campaign row one at a time."
      },
      "typeVersion": 1
    },
    {
      "id": "851ee820-dff9-4078-98d2-2b3a9ab5709b",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        272,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 336,
        "content": "## Prepare PinFlow image\n\nDownloads the image referenced by the current row and uploads it to PinFlow so it can be attached to scheduled social posts. Failed or completed image-handling paths return control to the row loop."
      },
      "typeVersion": 1
    },
    {
      "id": "36bb30f5-7db9-44bc-9388-d5856bdb2ed6",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        -32
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 304,
        "content": "## Schedule social posts\n\nCreates the scheduled posts in PinFlow across Pinterest, Instagram, and Facebook, then returns to the loop to process the next imported row."
      },
      "typeVersion": 1
    },
    {
      "id": "2f13cbb2-8fd2-4c24-96b1-4071d2a441cf",
      "name": "When Campaign File Uploaded",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -980,
        260
      ],
      "parameters": {
        "options": {
          "path": "pinflow-bulk-campaign",
          "buttonLabel": "Validate and schedule",
          "respondWithOptions": {
            "values": {
              "respondWith": "text",
              "formSubmittedText": "The campaign file was received and is being scheduled."
            }
          }
        },
        "formTitle": "PinFlow Bulk Campaign Import",
        "formFields": {
          "values": [
            {
              "fieldName": "data",
              "fieldType": "file",
              "fieldLabel": "Campaign file",
              "multipleFiles": false,
              "requiredField": true,
              "acceptFileTypes": ".csv,.xlsx,.xls"
            },
            {
              "fieldName": "fileFormat",
              "fieldType": "dropdown",
              "fieldLabel": "File format",
              "defaultValue": "CSV",
              "fieldOptions": {
                "values": [
                  {
                    "option": "CSV"
                  },
                  {
                    "option": "Excel XLSX"
                  },
                  {
                    "option": "Excel XLS"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldName": "intervalHours",
              "fieldType": "number",
              "fieldLabel": "Hours between posts",
              "defaultValue": "5",
              "requiredField": true
            },
            {
              "fieldName": "firstPublishAt",
              "fieldType": "text",
              "fieldLabel": "First publish time (optional ISO UTC)",
              "placeholder": "2026-08-04T08:00:00Z",
              "requiredField": false
            }
          ]
        },
        "responseMode": "onReceived",
        "authentication": "basicAuth",
        "formDescription": "Upload a CSV or Excel campaign file. Every row is validated, copied to PinFlow storage, and scheduled across Pinterest, Instagram, and Facebook."
      },
      "typeVersion": 2.5
    },
    {
      "id": "68acd0db-d39c-4198-9a03-6c1c3c054162",
      "name": "Route by File Format",
      "type": "n8n-nodes-base.switch",
      "position": [
        -740,
        260
      ],
      "parameters": {
        "mode": "expression",
        "output": "={{ $json.fileFormat === 'CSV' ? 0 : ($json.fileFormat === 'Excel XLSX' ? 1 : 2) }}",
        "numberOutputs": 3
      },
      "typeVersion": 3.4
    },
    {
      "id": "b66c4eca-d680-4218-9bb9-b357341f31e6",
      "name": "Extract CSV Data",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -500,
        80
      ],
      "parameters": {
        "options": {
          "enableBOM": true,
          "headerRow": true,
          "relaxQuotes": true
        },
        "operation": "csv",
        "binaryPropertyName": "data"
      },
      "typeVersion": 1.1
    },
    {
      "id": "891b6259-4c15-4295-a058-c433547dad98",
      "name": "Extract XLSX Data",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -500,
        260
      ],
      "parameters": {
        "options": {
          "headerRow": true,
          "includeEmptyCells": true
        },
        "operation": "xlsx",
        "binaryPropertyName": "data"
      },
      "typeVersion": 1.1
    },
    {
      "id": "7ce97ed3-e579-4a90-b4b9-af43823fe2e5",
      "name": "Extract XLS Data",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -500,
        440
      ],
      "parameters": {
        "options": {
          "headerRow": true,
          "includeEmptyCells": true
        },
        "operation": "xls",
        "binaryPropertyName": "data"
      },
      "typeVersion": 1.1
    },
    {
      "id": "a4a323d3-e214-4ee9-879b-46f50ef3aadd",
      "name": "Validate and Schedule Posts",
      "type": "n8n-nodes-base.code",
      "position": [
        -220,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const rows = $input.all();\nconst form = $('When Campaign File Uploaded').first().json;\nconst intervalHours = Math.min(Math.max(Number(form.intervalHours || 5), 1), 168);\nconst requestedStart = String(form.firstPublishAt || '').trim();\nconst first = requestedStart ? new Date(requestedStart) : new Date(Date.now() + 60 * 60 * 1000);\nif (Number.isNaN(first.getTime())) throw new Error('firstPublishAt must be a valid ISO datetime');\nif (!rows.length) throw new Error('The spreadsheet has no content rows');\nreturn rows.map((item, index) => {\n  const row = item.json;\n  const imageUrl = String(row.image_url || row.imageUrl || '').trim();\n  const title = String(row.title || '').trim();\n  const description = String(row.description || row.caption || '').trim();\n  const destinationUrl = String(row.destination_url || row.destinationUrl || row.link || '').trim();\n  if (!imageUrl) throw new Error(`Row ${index + 2}: image_url is required`);\n  if (!/^https:\\/\\//i.test(imageUrl)) throw new Error(`Row ${index + 2}: image_url must use HTTPS`);\n  if (!title && !description) throw new Error(`Row ${index + 2}: title or description is required`);\n  let scheduledAt;\n  if (row.scheduled_at || row.scheduledAt) {\n    const explicit = new Date(row.scheduled_at || row.scheduledAt);\n    if (Number.isNaN(explicit.getTime())) throw new Error(`Row ${index + 2}: invalid scheduled_at`);\n    scheduledAt = explicit.toISOString();\n  } else {\n    scheduledAt = new Date(first.getTime() + index * intervalHours * 3600000).toISOString();\n  }\n  return { json: {\n    rowNumber: index + 2, imageUrl, title, description, destinationUrl, scheduledAt,\n    instagramCaption: String(row.instagram_caption || description || title).trim(),\n    facebookMessage: String(row.facebook_message || description || title).trim()\n  } };\n});",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "bd6a3ed7-ec14-4f25-a159-0fc9f573d597",
      "name": "Loop Over Post Rows",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        60,
        260
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 3
    },
    {
      "id": "a392c49b-2562-4f76-9df5-cecbdbfe9aa7",
      "name": "Fetch Post Image",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "maxTries": 3,
      "position": [
        320,
        260
      ],
      "parameters": {
        "url": "={{ $json.imageUrl }}",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          }
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.4,
      "waitBetweenTries": 2000
    },
    {
      "id": "da1a8136-ea26-4c8e-87ef-16db695953fd",
      "name": "Upload Image to PinFlow",
      "type": "n8n-nodes-pinflow-pinterest.pinFlow",
      "onError": "continueErrorOutput",
      "maxTries": 3,
      "position": [
        580,
        260
      ],
      "parameters": {
        "resource": "media",
        "operation": "uploadImage",
        "binaryPropertyName": "data"
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 2000
    },
    {
      "id": "e46efb57-d235-4a97-b319-3a90be708257",
      "name": "Schedule Pinterest Pin",
      "type": "n8n-nodes-pinflow-pinterest.pinFlow",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        840,
        100
      ],
      "parameters": {
        "link": "={{ $('Validate and Schedule Posts').item.json.destinationUrl }}",
        "title": "={{ $('Validate and Schedule Posts').item.json.title }}",
        "boardId": "",
        "mediaUrl": "={{ $json.url }}",
        "resource": "pin",
        "operation": "schedule",
        "description": "={{ $('Validate and Schedule Posts').item.json.description }}",
        "scheduledAt": "={{ $('Validate and Schedule Posts').item.json.scheduledAt }}",
        "pinCreateMode": "scheduled",
        "pinterestProfileId": ""
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 2000
    },
    {
      "id": "9bcf665a-d570-4404-8c18-6ce3e5f0f2c7",
      "name": "Schedule Instagram Post",
      "type": "n8n-nodes-pinflow-pinterest.pinFlow",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        1080,
        100
      ],
      "parameters": {
        "resource": "instagramPost",
        "operation": "create",
        "instagramCaption": "={{ $('Validate and Schedule Posts').item.json.instagramCaption }}",
        "instagramMediaUrl": "={{ $('Upload Image to PinFlow').item.json.url }}",
        "instagramMediaType": "image",
        "instagramProfileId": "",
        "instagramScheduledAt": "={{ $('Validate and Schedule Posts').item.json.scheduledAt }}",
        "instagramCreateStatus": "scheduled"
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 2000
    },
    {
      "id": "159f7529-8818-46a5-9eec-c446338c9ee8",
      "name": "Schedule Facebook Post",
      "type": "n8n-nodes-pinflow-pinterest.pinFlow",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        1320,
        100
      ],
      "parameters": {
        "resource": "facebookPost",
        "operation": "create",
        "facebookPageId": "",
        "facebookMessage": "={{ $('Validate and Schedule Posts').item.json.facebookMessage }}",
        "facebookMediaUrl": "={{ $('Upload Image to PinFlow').item.json.url }}",
        "facebookMediaType": "image",
        "facebookScheduledAt": "={{ $('Validate and Schedule Posts').item.json.scheduledAt }}",
        "facebookCreateStatus": "scheduled"
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 2000
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "f3a0a150-e447-45cc-b1b7-a861e5f0c00d",
  "connections": {
    "Extract CSV Data": {
      "main": [
        [
          {
            "node": "Validate and Schedule Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract XLS Data": {
      "main": [
        [
          {
            "node": "Validate and Schedule Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Post Image": {
      "main": [
        [
          {
            "node": "Upload Image to PinFlow",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Post Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract XLSX Data": {
      "main": [
        [
          {
            "node": "Validate and Schedule Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Post Rows": {
      "main": [
        [],
        [
          {
            "node": "Fetch Post Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by File Format": {
      "main": [
        [
          {
            "node": "Extract CSV Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract XLSX Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract XLS Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Facebook Post": {
      "main": [
        [
          {
            "node": "Loop Over Post Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Pinterest Pin": {
      "main": [
        [
          {
            "node": "Schedule Instagram Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Instagram Post": {
      "main": [
        [
          {
            "node": "Schedule Facebook Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Image to PinFlow": {
      "main": [
        [
          {
            "node": "Schedule Pinterest Pin",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Post Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate and Schedule Posts": {
      "main": [
        [
          {
            "node": "Loop Over Post Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Campaign File Uploaded": {
      "main": [
        [
          {
            "node": "Route by File Format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}