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:

  1. Trigger: When record matches conditions — Status is Approved and Postproxy ID is empty.
  2. Action: Run script. In the input config, pass recordId from 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 validation
const 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:

  1. Writer drafts in Draft
  2. Editor reviews, sets StatusApproved
  3. Automation fires, posts to Postproxy
  4. Status flips to Scheduled with the Postproxy ID
  5. 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.

Ready to get started?

Start with our free plan and scale as your needs grow. No credit card required.