{
  "name": "Save the newest posts of a Facebook page to Google Sheets",
  "nodes": [
    {
      "id": "0d6d861a-05c7-4dae-a6af-1d5dc4c462ac",
      "name": "Sticky Note main",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -820,
        -826
      ],
      "parameters": {
        "content": "## Track a competitor's Facebook posts in Google Sheets\n\n### Who's it for\nMarketing and social teams who monitor a public Facebook page: their own, a client's, or a competitor's. Keep a sheet of posts and see how engagement changes without manually copying data.\n\n### How it works\nEvery morning the workflow runs Alfalfa's Facebook Posts Scraper on Apify, looks back two days, and writes the latest post text, links, media, likes, comments, shares and views to Google Sheets. The final node matches on a stable post key: new posts get a row; previously seen posts refresh that row instead of making duplicates.\n\n### How to set up\n1. Import the header-only CSV supplied with this workflow into a new Google Sheet. Keep the postKey header: it is the match column.\n2. Add an Apify credential and a Google Sheets credential in n8n.\n3. Paste the Sheet URL into **Upsert posts by key** and select its first tab.\n4. In **Settings**, replace the example Facebook page URL; raise resultsLimit if the page may publish over 100 posts in two days.\n5. Run twice and check that the number of rows stays the same for repeated post keys; then activate the schedule.\n\n### Billing and limits\n$2 per 1,000 returned posts on Apify Free, no start fee; each daily run bills every returned post again, including rows that Google Sheets updates. The two-day overlap tolerates a late run, but a longer outage needs a wider lookback. Public content only; no Facebook login or cookies. Video transcripts are off.\n",
        "width": 900,
        "height": 786
      }
    },
    {
      "id": "d71a09e9-ad98-4ad3-9504-c9f462a1e634",
      "name": "Sticky Note 1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -700,
        200
      ],
      "parameters": {
        "content": "## 1. When and what\nRuns daily at 9:00 AM in the workflow or instance timezone; the Settings node holds the page URL and the post limit.",
        "width": 420,
        "height": 148,
        "color": 7
      }
    },
    {
      "id": "d8c66447-939a-49de-acf0-658c2f432fa7",
      "name": "Sticky Note 2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -260,
        200
      ],
      "parameters": {
        "content": "## 2. Scrape\nRuns the Facebook Posts Scraper on Apify and reads the run's dataset.",
        "width": 420,
        "height": 148,
        "color": 7
      }
    },
    {
      "id": "3a8bb6ab-8454-4444-97fa-3e23481d8d2f",
      "name": "Sticky Note 3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        220,
        200
      ],
      "parameters": {
        "content": "## 3. Tidy and store\nNew posts get a row; the same post key updates its engagement numbers.",
        "width": 420,
        "height": 148,
        "color": 7
      }
    },
    {
      "id": "4700e58d-9a18-4fc0-965e-16f1310ed8ea",
      "name": "Every morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -620,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 9,
              "triggerAtMinute": 0
            }
          ]
        }
      }
    },
    {
      "id": "cec1f9fb-d55c-45e6-87ff-bef3a800fcd0",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -400,
        0
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "e3f83c89-3ec1-4b46-9670-684fab702b6f",
              "name": "pageUrl",
              "value": "https://www.facebook.com/NASA/",
              "type": "string"
            },
            {
              "id": "73471b92-2747-4bc0-976e-47304b475821",
              "name": "resultsLimit",
              "value": 100,
              "type": "number"
            },
            {
              "id": "bb7862fb-debd-4115-825f-c23c4a663dd4",
              "name": "onlyPostsNewerThan",
              "value": "2 days",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "caf67857-5a6f-413b-80a7-8a6534a06a30",
      "name": "Scrape Facebook posts",
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        -180,
        0
      ],
      "parameters": {
        "actorId": {
          "__rl": true,
          "mode": "list",
          "value": "9fbE1ad5CkBc1hhbu",
          "cachedResultUrl": "https://console.apify.com/actors/9fbE1ad5CkBc1hhbu/input",
          "cachedResultName": "Facebook Posts Scraper (alfalfa/facebook-posts-scraper)"
        },
        "operation": "Run actor",
        "timeout": {},
        "customBody": "={\n  \"startUrls\": [{ \"url\": \"{{ $json.pageUrl }}\" }],\n  \"resultsLimit\": {{ $json.resultsLimit }},\n  \"onlyPostsNewerThan\": \"{{ $json.onlyPostsNewerThan }}\"\n}"
      }
    },
    {
      "id": "c40515c6-1b6d-45a2-9887-392eb6b27ef8",
      "name": "Get post results",
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        40,
        0
      ],
      "parameters": {
        "resource": "Datasets",
        "operation": "Get items",
        "datasetId": "={{ $json.defaultDatasetId }}",
        "limit": {}
      }
    },
    {
      "id": "ed755825-b29b-40e4-974a-10cdd45fd5a2",
      "name": "Flatten to columns",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        260,
        0
      ],
      "parameters": {
        "jsCode": "// Prefix the match key so Google Sheets keeps numeric Facebook IDs as text.\n// Keep the latest item if the same post appears twice in one Actor dataset.\nconst rows = new Map();\nfor (const { json: p } of $input.all()) {\n  const postId = p.postId == null ? '' : String(p.postId);\n  const postKey = postId ? `fb:${postId}` : (p.url ? `url:${p.url}` : '');\n  if (!postKey) continue;\n  const media = Array.isArray(p.media) ? p.media : [];\n  rows.set(postKey, { json: {\n    postKey,\n    postId,\n    page: p.pageName || '',\n    postedAt: p.time || '',\n    text: p.text || '',\n    url: p.url || '',\n    likes: p.likes ?? '',\n    comments: p.comments ?? '',\n    shares: p.shares ?? '',\n    isVideo: p.isVideo ? 'yes' : 'no',\n    views: p.viewsCount ?? '',\n    link: p.link || '',\n    media: media.map(m => m?.thumbnail || m?.url).filter(Boolean).join(' '),\n    lastSeenAt: p.scrapedAt || new Date().toISOString()\n  }});\n}\nreturn [...rows.values()];\n"
      }
    },
    {
      "id": "cbb4ea90-655f-44c6-9ec2-40e1fe07dceb",
      "name": "Upsert posts by key",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        480,
        0
      ],
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID_HERE/edit"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "postKey"
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      }
    }
  ],
  "connections": {
    "Every morning": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Scrape Facebook posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Facebook posts": {
      "main": [
        [
          {
            "node": "Get post results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get post results": {
      "main": [
        [
          {
            "node": "Flatten to columns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flatten to columns": {
      "main": [
        [
          {
            "node": "Upsert posts by key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
