Schedule posts to social media APIs
Publish Northwind content from a Sheet automatically — drafts queued, posted on date.
Published Sep 17, 2025
Northwind’s marketing lead drafts a week of social posts in one sitting, then wants them to go out on the right days without anyone logging into a dashboard. Scheduling tools exist, but a spreadsheet is where the copy already lives — alongside the campaign, the channel, and the sign-off column.
This script turns that Sheet into a publishing queue. Each row is one post with a platform, the text, and a scheduled time. On every run it finds the rows that are due, posts them through the platform’s API, and stamps the row as posted so it never goes out twice. Add a time-driven trigger and the queue empties itself.
What you’ll need
- A Google Sheet with one row per post and a header in row 1, with these
columns:
platform,text,scheduledAt(a real date-time, not text), andposted(left blank — the script fills it). - The spreadsheet’s ID for the
POSTS_SHEET_IDconfig value. - An access token for each platform you post to, saved in Script Properties —
this script uses
MASTO_TOKENfor Mastodon. See Store API keys and secrets securely. - The base URL of your Mastodon instance for the
MASTODON_INSTANCEconfig.
The script
// The spreadsheet that holds your post queue.
const POSTS_SHEET_ID = '1abcPostsId';
// Base URL of your Mastodon instance — no trailing slash.
const MASTODON_INSTANCE = 'https://mastodon.example';
/**
* Finds every post that is due, publishes it, and marks the row as
* posted so it is never sent twice. Designed to run on a timer.
*/
function publishDuePosts() {
const sheet = SpreadsheetApp.openById(POSTS_SHEET_ID).getSheets()[0];
// 1. Read the whole sheet, splitting the header off the data rows.
const values = sheet.getDataRange().getValues();
const [header, ...rows] = values;
if (!rows.length) {
Logger.log('No posts in the queue — nothing to do.');
return;
}
// 2. Map column names to indexes so the code does not depend on
// column order — only on the header text.
const col = Object.fromEntries(header.map((name, i) => [name, i]));
const now = new Date();
let published = 0;
// 3. Walk every row. Skip anything already posted, anything without a
// real date in scheduledAt, and anything still in the future.
rows.forEach((row, i) => {
const alreadyPosted = row[col.posted];
const when = row[col.scheduledAt];
if (alreadyPosted || !(when instanceof Date) || when > now) return;
// 4. Publish the post, then stamp the posted column on the matching
// row in `values` (i + 1 accounts for the header row).
postToPlatform(row[col.platform], row[col.text]);
values[i + 1][col.posted] = new Date();
published++;
});
// 5. Write the whole grid back in one call — cheaper than cell-by-cell.
sheet.getDataRange().setValues(values);
Logger.log('Published ' + published + ' post(s).');
}
/**
* Sends one post to the named platform. Add a branch here for each
* extra network you support — the queue logic above stays the same.
*/
function postToPlatform(platform, text) {
if (platform === 'mastodon') {
const token = PropertiesService.getScriptProperties()
.getProperty('MASTO_TOKEN');
UrlFetchApp.fetch(MASTODON_INSTANCE + '/api/v1/statuses', {
method: 'post',
headers: { Authorization: 'Bearer ' + token },
payload: { status: text },
});
return;
}
// An unknown platform is a data error — surface it rather than
// silently dropping the post.
throw new Error('Unsupported platform: ' + platform);
}
How it works
publishDuePostsopens the queue spreadsheet and reads the entire grid, splitting row 1 off as the header.- If there are no data rows, it logs a message and stops.
- It builds a
collookup from header text to column index, so the script keeps working even if someone reorders the columns. - It walks every row and skips three cases: posts already stamped in
posted, rows wherescheduledAtis not a real date, and posts whose time is still in the future. - For each due post it calls
postToPlatform, then writes the current time into that row’spostedcell — the guard in step 4 means the next run will skip it. - After the loop it writes the whole grid back in a single
setValuescall. postToPlatformholds one branch per network. The Mastodon branch reads the token from Script Properties and POSTs the status; an unknown platform throws so a typo never goes unnoticed.
Example run
The queue sheet before a run, with the clock at 09:05 on 17 Sept:
| platform | text | scheduledAt | posted |
|---|---|---|---|
| mastodon | New case study: how Northwind cut onboarding time | 2025-09-17 09:00 | |
| mastodon | We’re hiring a junior designer — link in bio | 2025-09-17 14:00 | |
| mastodon | Friday tip: name your layers | 2025-09-18 09:00 |
After publishDuePosts runs, only the first row is due — it is published and
stamped. The other two are left untouched until their time comes:
| platform | text | scheduledAt | posted |
|---|---|---|---|
| mastodon | New case study: how Northwind cut onboarding time | 2025-09-17 09:00 | 2025-09-17 09:05:12 |
| mastodon | We’re hiring a junior designer — link in bio | 2025-09-17 14:00 | |
| mastodon | Friday tip: name your layers | 2025-09-18 09:00 |
The log reads Published 1 post(s).
Trigger it
This is a scheduled job — it does nothing useful unless it runs on a timer:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add Trigger.
- Choose
publishDuePosts, event source Time-driven, and a Minutes timer of every 15 or 30 minutes.
The interval is also your scheduling precision: a post set for 09:00 goes out at the first run after 09:00, so a 15-minute timer means posts land within a quarter-hour of their slot.
Watch out for
- Make
scheduledAta real date-time, not text. Format the column as a date in the Sheet — theinstanceof Dateguard skips any cell that is plain text, so a mistyped date silently never posts. - Tokens expire. Mastodon access tokens are long-lived, but most other networks (X, LinkedIn, Facebook) use short-lived OAuth tokens that need refreshing. Plan for a refresh step before adding those branches.
- Watch the platform rate limits. Mastodon allows generous posting, but a long backlog firing at once can still trip a limit — keep the timer frequent so the queue drains a few posts at a time rather than dozens.
- A failed API call throws and stops the run, leaving later rows unposted. They
are picked up on the next run because their
postedcell is still blank, but check the execution log if a post seems stuck. - The script does not validate post length. Mastodon rejects statuses over its
character limit, so keep the
textcolumn within 500 characters.
Related
Sync calendar bookings with Calendly
Bridge Google Calendar and Calendly — Northwind bookings on either side appear on both.
Updated Jan 7, 2026
Connect to an air-quality and weather feed
Build a Northwind environmental dashboard — current London AQI plus 5-day forecast.
Updated Dec 30, 2025
Build a podcast and media stats tracker
Pull Northwind's podcast download numbers across platforms into a single sheet.
Updated Dec 10, 2025
Track real-estate listings for new matches
Monitor property feeds for Northwind office hunts — alert when a match appears.
Updated Nov 28, 2025
Translate columns with a translation API
Localise Northwind text in bulk without manual work — via Google Translate or DeepL.
Updated Nov 24, 2025