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 agency approval loop off one Google Sheet: draft goes out to the client, the client clicks approve or request-changes, the decision is written back with a timestamp, overdue drafts are chased and escalated, approved posts are pushed to your own publishing API, and a weekly report ranks your clients by approval turnaround. Two of the six are free; the sheet records sent_at and decision_at on every row, so after one week the report gives you your real hours spent chasing sign-off as a number measured from your own data, not an estimate.
Free workflow — copy & import
{
"name": "1 · Send Drafts to the Client for Approval",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 1
}
]
}
},
"id": "b769468b-c104-1068-b769-468bc1041068",
"name": "Check the sheet every hour",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
300
]
},
{
"parameters": {
"mode": "manual",
"assignments": {
"assignments": []
},
"options": {}
},
"id": "b769468b-c104-1069-b769-468bc1041069",
"name": "Approval settings",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
220,
300
]
},
{
"parameters": {
"operation": "read",
"options": {}
},
"id": "b769468b-c104-106a-b769-468bc104106a",
"name": "Read the Approval Log",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
440,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"version": 2
},
"conditions": [],
"combinator": "and"
}
},
"id": "b769468b-c104-106b-b769-468bc104106b",
"name": "Only rows ready for the client",
"type": "n8n-nodes-base.filter",
"typeVersion": 2,
"position": [
660,
300
]
},
{
"parameters": {
"jsCode": "const cfg = $('Approval settings').first().json;\nreturn items.map(i => {\n const r = i.json;\n const base = cfg.webhookBaseUrl;\n r.approveUrl = base + '?row=' + encodeURIComponent(r.row_id) + '&decision=approve';\n r.reviseUrl = base + '?row=' + encodeURIComponent(r.row_id) + '&decision=revise';\n r.sent_at = new Date().toISOString();\n r.emailBody = 'Hi ' + (r.client_contact || 'there') + ',\\n\\n' + cfg.agencyName + ' has a post ready for your sign-off.\\n\\nClient: ' + r.client + '\\nChannel: ' + r.channel + '\\nGoes out: ' + r.post_date + '\\n\\n--- Copy ---\\n' + r.copy + '\\n\\nAsset: ' + (r.asset_url || 'none attached') + '\\n\\nApprove it: ' + r.approveUrl + '\\nAsk for changes: ' + r.reviseUrl + '\\n\\nOne click is all we need — the link records your decision and nothing else.\\n\\n' + cfg.agencyName;\n return { json: r };\n});"
},
"id": "b769468b-c104-106c-b769-468bc104106c",
"name": "Build the approve and request-changes links",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
880,
300
]
},
{
"parameters": {
"subject": "Approval needed: {{$json.client}} — {{$json.channel}} post for {{$json.post_date}}",
"text": "={{$json.emailBody}}",
"toEmail": "={{ $json.email }}",
"fromEmail": "={{ $env.FROM_EMAIL }}",
"options": {}
},
"id": "b769468b-c104-106d-b769-468bc104106d",
"name": "Email the client for sign-off",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
1100,
300
]
},
{
"parameters": {
"operation": "update",
"options": {}
},
"id": "b769468b-c104-106e-b769-468bc104106e",
"name": "Mark the row as awaiting the client",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1320,
300
]
},
{
"parameters": {
"content": "SETUP — about 5 minutes.\n\n1. Copy the Approval Log sheet template. It needs these columns, spelled exactly: row_id, client, client_contact, client_email, channel, post_date, copy, asset_url, status, sent_at, decision_at, decision_by, revision_round, published_at. Every workflow in this pack reads and writes those names.\n\n2. Open the 'Approval settings' node and fill in four things: your sheet name, your agency name, the reply-to address you want clients to see, and your webhook base URL. The webhook URL comes from workflow 2 — activate that one first, copy its Production URL, and paste it here.\n\n3. Credentials. The Google Sheets node needs your own Google Sheets OAuth2 credential and the Email node needs your own SMTP credential. Both credential slots ship empty on purpose — no key, token or password is stored anywhere in this file. Create them in n8n under Credentials and select them on the two nodes.\n\n4. A row is only picked up when its status reads exactly 'Ready for client'. That is the switch your team flips when a draft is finished. As soon as the mail goes out the status becomes 'Awaiting client', so the same draft can never be sent twice.\n\n5. The schedule runs hourly. If your agency batches sign-off requests once a day, change 'every' to 24 on the trigger.\n\nThis workflow is free and works on its own together with workflow 2: send, capture, log. The paid workflows add chasing, revision rounds, publishing and reporting on top of the same sheet.",
"height": 220,
"width": 320
},
"id": "b769468b-c104-106f-b769-468bc104106f",
"name": "Setup",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1540,
100
]
}
],
"connections": {
"Check the sheet every hour": {
"main": [
[
{
"node": "Approval settings",
"type": "main",
"index": 0
}
]
]
},
"Approval settings": {
"main": [
[
{
"node": "Read the Approval Log",
"type": "main",
"index": 0
}
]
]
},
"Read the Approval Log": {
"main": [
[
{
"node": "Only rows ready for the client",
"type": "main",
"index": 0
}
]
]
},
"Only rows ready for the client": {
"main": [
[
{
"node": "Build the approve and request-changes links",
"type": "main",
"index": 0
}
]
]
},
"Build the approve and request-changes links": {
"main": [
[
{
"node": "Email the client for sign-off",
"type": "main",
"index": 0
}
]
]
},
"Email the client for sign-off": {
"main": [
[
{
"node": "Mark the row as awaiting the client",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"pinData": {},
"meta": {
"templateId": "n8n-social-content-approval-agency"
}
}