How to Schedule Social Media Posts from Airtable
Drive a multi-platform publishing pipeline from an Airtable base — automation script, status writeback, and review workflow.
The base
A practical Airtable content base has these fields:
- Body — long text
- Profiles — multi-select (
tiktok,instagram,youtube,twitter,linkedin,threads,pinterest,facebook,bluesky) - Media — attachment (single)
- Publish at — datetime
- Status — single select (Draft, Approved, Scheduled, Published, Failed)
- Postproxy ID — single line text (filled by automation)
- Result — long text (filled by automation)
The team works with views: “Drafts,” “Pending review,” “Approved,” “Live this week.”
Airtable automation: when status flips to Approved → schedule
In the base, Automations → Create automation:
- Trigger: When record matches conditions —
StatusisApprovedandPostproxy IDis empty. - Action: Run script. In the input config, pass
recordIdfrom the trigger.
const POSTPROXY_API_KEY = "YOUR_KEY";
const inputConfig = input.config();const recordId = inputConfig.recordId;
const table = base.getTable("Posts");const record = await table.selectRecordAsync(recordId);
const body = record.getCellValueAsString("Body");const profiles = record.getCellValue("Profiles").map((p) => p.name);const publishAt = new Date(record.getCellValue("Publish at")).toISOString();const attachments = record.getCellValue("Media") || [];const media = attachments.map((a) => a.url);
// Sensible per-platform defaults so platform-required params don't fail validationconst platforms = {};if (profiles.includes("tiktok")) { platforms.tiktok = { privacy_status: "PUBLIC_TO_EVERYONE" };}if (profiles.includes("youtube")) { platforms.youtube = { privacy_status: "public" };}if (profiles.includes("instagram") && media[0] && /\.(mp4|mov)$/i.test(media[0])) { platforms.instagram = { format: "reel" };}
const response = await fetch("https://api.postproxy.dev/api/posts", { method: "POST", headers: { Authorization: `Bearer ${POSTPROXY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ post: { body, scheduled_at: publishAt }, profiles, media, platforms, }),});
const result = await response.json();
if (result.id) { await table.updateRecordAsync(recordId, { "Postproxy ID": result.id, Status: { name: "Scheduled" }, });} else { await table.updateRecordAsync(recordId, { Status: { name: "Failed" }, Result: result.error || JSON.stringify(result.errors || result), });}The trigger condition (Postproxy ID is empty) is what stops a row from being re-published. Don’t clear the Postproxy ID by hand unless you actually want to republish.
A second automation: status writeback
Run on a schedule (every hour) — find records with Status = Scheduled and refresh their state from Postproxy:
const POSTPROXY_API_KEY = "YOUR_KEY";
const table = base.getTable("Posts");const query = await table.selectRecordsAsync({ fields: ["Postproxy ID", "Status"],});
for (const record of query.records) { if (record.getCellValueAsString("Status") !== "Scheduled") continue; const postId = record.getCellValueAsString("Postproxy ID"); if (!postId) continue;
const response = await fetch( `https://api.postproxy.dev/api/posts/${postId}`, { headers: { Authorization: `Bearer ${POSTPROXY_API_KEY}` } } ); const post = await response.json();
const summary = (post.platforms || []) .map((p) => `${p.network}: ${p.status}`) .join("\n");
const allPublished = (post.platforms || []).every( (p) => p.status === "published" ); const anyFailed = (post.platforms || []).some( (p) => p.status === "failed" );
await table.updateRecordAsync(record.id, { Result: summary, Status: { name: allPublished ? "Published" : anyFailed ? "Failed" : "Scheduled", }, });}Now the base shows live status next to each row. The response uses network (e.g. instagram) inside each platforms[] entry.
Webhooks instead of polling
Polling every hour is fine for ~100 posts a week. Past that, switch to Postproxy webhooks pointing at an Airtable webhook URL (or a small lambda that writes back to Airtable). See webhooks vs polling for the tradeoffs.
Review workflow
Because the trigger fires on Status = Approved, a reviewer flips a single field to schedule. Common workflow:
- Writer drafts in
Draft - Editor reviews, sets
Status→Approved - Automation fires, posts to Postproxy
- Status flips to
Scheduledwith the Postproxy ID - Hourly job updates per-platform results
If you want a hard “no surprises” gate — every approval needs a second pair of eyes — add a Reviewer linked field and condition the automation on Reviewer is not empty. See content approval workflows for variations.
Multi-brand bases
Agencies often have one base per client or one shared base with a Brand field. Pass profile_group_id in the request body to target a specific Postproxy profile group:
body: JSON.stringify({ post: { body, scheduled_at: publishAt }, profile_group_id: brandToProfileGroup[record.getCellValueAsString("Brand")], profiles, media, platforms,}),Limits
Airtable Automation scripts can run for up to 30 seconds per execution. For a single-base content calendar with ~50 posts/week, that’s enough. Past that, run the same script logic on a server using the Airtable API + Postproxy API directly — only thing that changes is where the script runs.
For the underlying Postproxy scheduling primitives, see Scheduling social media posts programmatically with cron and API.