n8n Social Media Workflow for Agency Content Approval — 6 Client Sign-Off Automations for Marketing Agencies

Six ready-to-import n8n workflows that run the whole client sign-off loop for an agency social calendar: send the posts for approval, capture the client's one-click decision, chase the silence, flag the posts due before they are approved, hand approved posts to your scheduler, and run internal QA before the client ever sees a draft. Two of the six ship free to the n8n template library, and workflow 4 reports your agency's own median hours between 'sent for review' and 'decision' from the timestamps your own sheet records — the hours are measured on your data, not claimed here.

Free workflow — copy & import

{
  "name": "1 · Send this batch of posts to each client for sign-off",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24
            }
          ]
        }
      },
      "id": "5a69579e-2205-7f67-5a69-579e22057f67",
      "name": "Every 24 hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        300
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "options": {}
      },
      "id": "5a69579e-2205-7f68-5a69-579e22057f68",
      "name": "Read the content calendar",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const REVIEW_BASE = 'https://YOUR-N8N-HOST/webhook/client-decision';\nconst rows = items.map(i => i.json).filter(r => String(r.Status || '').trim() === 'Ready for Review');\nconst byClient = {};\nfor (const r of rows) { const c = String(r.Client || 'Unassigned').trim(); (byClient[c] = byClient[c] || []).push(r); }\nreturn Object.keys(byClient).map(c => {\n  const list = byClient[c];\n  const lines = list.map(r => r.RowId + ' | ' + r.Channel + ' | planned ' + r.PublishDate + '\\n' + r.Caption + '\\nApprove: ' + REVIEW_BASE + '?row=' + encodeURIComponent(r.RowId) + '&decision=approve\\nRequest changes: ' + REVIEW_BASE + '?row=' + encodeURIComponent(r.RowId) + '&decision=changes').join('\\n\\n');\n  return { json: { client: c, reviewerEmail: list[0].ReviewerEmail, postsInThisEmail: list.length, rowIds: list.map(r => r.RowId).join(','), body: 'Posts waiting for your sign-off: ' + list.length + '\\n\\n' + lines + '\\n\\nClicking a link records your decision straight away. If you request changes we will come back with a new version.' } };\n});"
      },
      "id": "5a69579e-2205-7f69-5a69-579e22057f69",
      "name": "Group the posts waiting for review by client and build the decision links",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ]
    },
    {
      "parameters": {
        "subject": "=Sign-off needed: {{ $json.postsInThisEmail }} post(s) for {{ $json.client }}",
        "text": "={{ $json.body }}",
        "toEmail": "={{ $json.reviewerEmail }}",
        "fromEmail": "={{ $env.FROM_EMAIL }}",
        "options": {}
      },
      "id": "5a69579e-2205-7f6a-5a69-579e22057f6a",
      "name": "Email the client one sign-off request for all their posts",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const out = [];\nconst stamp = new Date().toISOString();\nfor (const i of items) {\n  const j = i.json;\n  for (const id of String(j.rowIds || '').split(',').filter(Boolean)) {\n    out.push({ json: { RowId: id, Status: 'Sent for Review', SentAt: stamp, ReviewerEmail: j.reviewerEmail, LastReminderStage: '' } });\n  }\n}\nreturn out;"
      },
      "id": "5a69579e-2205-7f6b-5a69-579e22057f6b",
      "name": "Expand back to one item per post row for the status update",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        300
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "options": {}
      },
      "id": "5a69579e-2205-7f6c-5a69-579e22057f6c",
      "name": "Mark those rows Sent for Review with the timestamp",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1100,
        300
      ]
    },
    {
      "parameters": {
        "content": "SETUP (about 5 minutes)\n\n1. Content calendar. Make a Google Sheet with a tab called 'Content Calendar' and these column headers, spelled exactly: RowId, Client, Channel, Caption, AssetUrl, PublishDate, Status, ReviewerEmail, SentAt, DecisionAt, DecisionNote, LastReminderStage, LastReminderAt, InternalReviewer, InternalDecision, InternalNote, InternalSentAt, Round, Creator, AccountManager, PublishedAt, LastError. RowId must be unique per post — every workflow in this pack matches on it.\n\n2. Sheet ID. Open the sheet, copy the long id out of its address bar and paste it over PASTE_YOUR_GOOGLE_SHEET_ID in both calendar steps of this workflow.\n\n3. Credentials. Select your own Google Sheets credential and your own email (SMTP) credential in the credential fields. Nothing is stored in this file — the fields are empty on purpose and stay yours.\n\n4. Review links. In 'Group the posts waiting for review by client and build the decision links', replace https://YOUR-N8N-HOST with the public base address of your own n8n instance. The path /webhook/client-decision must match workflow 2. If your n8n is not reachable from the internet the client's click cannot arrive — use an n8n Cloud instance or a tunnel.\n\n5. Status values used across the pack: Draft, Ready for Internal QA, In revision, Ready for Review, Sent for Review, Approved, Changes requested.\n\nRUN IT: put one test row in the sheet with Status 'Ready for Review' and your own address in ReviewerEmail, execute the workflow, and check the email you receive.",
        "height": 220,
        "width": 320
      },
      "id": "5a69579e-2205-7f6d-5a69-579e22057f6d",
      "name": "Setup — read this first",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1320,
        100
      ]
    }
  ],
  "connections": {
    "Every 24 hours": {
      "main": [
        [
          {
            "node": "Read the content calendar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the content calendar": {
      "main": [
        [
          {
            "node": "Group the posts waiting for review by client and build the decision links",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group the posts waiting for review by client and build the decision links": {
      "main": [
        [
          {
            "node": "Email the client one sign-off request for all their posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email the client one sign-off request for all their posts": {
      "main": [
        [
          {
            "node": "Expand back to one item per post row for the status update",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand back to one item per post row for the status update": {
      "main": [
        [
          {
            "node": "Mark those rows Sent for Review with the timestamp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateId": "n8n-social-media-content-approval-agency"
  }
}

Get the complete version $59

This page is the working piece. The full pack has everything below.

6 workflows, 2 of them free on the n8n template library — every request, reminder, decision and publish hand-off timestamped in your own content calendar, so nothing goes live without a reco

The alternative is a per-seat content-approval SaaS: every product in this category is billed per user per month and renews for as long as the agency uses it, and each extra client reviewer you invite is another paid seat every mo

Send it to me