Build a content calendar with status automation
Move posts through draft → review → scheduled → published based on date and approval columns.
Published Dec 13, 2025
Northwind’s content team plans every blog post and newsletter in a single
Calendar sheet. The trouble is that the status column never keeps up: a post
gets approved but stays marked draft, or its publish date passes and the row
still says scheduled. Nobody updates statuses by hand reliably, so the
calendar slowly drifts out of sync with reality.
This script keeps the status column honest. On a schedule it reads every row and
moves each post one step forward when the conditions are met — an approved draft
becomes scheduled, and a scheduled post whose publish date has arrived becomes
published. The team gets a calendar they can trust at a glance, without anyone
policing it.
What you’ll need
- A Google Sheet with a
Calendartab (the first sheet) and a header row in row 1. - A
statuscolumn holding one ofdraft,scheduledorpublished. - An
approvedcolumn — a checkbox or any truthy value once a post is signed off. - A
publishDatecolumn containing real dates (formatted as dates, not text). - The spreadsheet ID, which you can copy from the sheet’s URL.
The script
// The spreadsheet that holds your content calendar.
const CALENDAR_SHEET_ID = '1abcContentCalendarId';
/**
* Walks every row of the content calendar and advances each post's status
* one step when its conditions are met:
* draft + approved -> scheduled
* scheduled + date passed -> published
*/
function advanceContentStatuses() {
const sheet = SpreadsheetApp.openById(CALENDAR_SHEET_ID).getSheets()[0];
// 1. Read the whole sheet in one call — header plus data rows.
const values = sheet.getDataRange().getValues();
if (values.length < 2) {
Logger.log('No content rows to process — nothing to do.');
return;
}
// 2. Split off the header and build a name -> column-index lookup,
// so the code never depends on a fixed column order.
const [header, ...rows] = values;
const col = Object.fromEntries(header.map((name, i) => [name, i]));
const today = new Date();
// 3. Check each row and update the status cell in place.
let changed = 0;
rows.forEach((row, i) => {
const status = row[col.status];
// An approved draft is ready to be scheduled.
if (status === 'draft' && row[col.approved]) {
values[i + 1][col.status] = 'scheduled';
changed++;
}
// A scheduled post whose publish date has arrived is now published.
if (
status === 'scheduled' &&
row[col.publishDate] instanceof Date &&
row[col.publishDate] <= today
) {
values[i + 1][col.status] = 'published';
changed++;
}
});
// 4. Write the whole grid back in one call — only the changed
// status cells differ from what was already there.
sheet.getDataRange().setValues(values);
Logger.log('Advanced ' + changed + ' post(s).');
}
How it works
advanceContentStatusesopens the calendar spreadsheet and reads the entire data range in a single call, which is far faster than reading cell by cell.- If the sheet holds only a header (or is empty), it logs a message and stops.
- It splits the header from the data rows and builds a
collookup that maps each column name to its index — so renaming or reordering columns will not break the script, as long as the header names stay the same. - For each row it checks two transitions. A post marked
draftwith a truthyapprovedvalue moves toscheduled. A post markedscheduledwhosepublishDateis a real date on or before today moves topublished. - Each change is written back into the in-memory
valuesgrid, not straight to the sheet. - After every row is checked, the whole grid is written back in one
setValuescall. Only the status cells that changed will actually differ.
Example run
Say the Calendar tab looks like this before a run, with today being 13 May:
| title | status | approved | publishDate |
|---|---|---|---|
| Spring product update | draft | ✓ | 2025-05-20 |
| Hiring announcement | scheduled | ✓ | 2025-05-12 |
| Q3 roadmap teaser | draft | 2025-06-01 |
After the script runs:
| title | status | approved | publishDate |
|---|---|---|---|
| Spring product update | scheduled | ✓ | 2025-05-20 |
| Hiring announcement | published | ✓ | 2025-05-12 |
| Q3 roadmap teaser | draft | 2025-06-01 |
The approved draft advanced to scheduled, the scheduled post whose date had
passed advanced to published, and the unapproved draft was left alone.
Trigger it
Run this on a time-driven trigger so statuses stay current without anyone touching them:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add Trigger.
- Choose
advanceContentStatuses, an Hour timer, and Every hour. - Save and approve the authorisation prompt.
Hourly is plenty for a content calendar — publish dates are days apart, so nothing waits long for its status to catch up.
Watch out for
- Statuses only ever move forward. The script never demotes a post, so if you un-approve something or change a date, fix the status cell by hand.
- The
publishDatecolumn must contain real dates. A date typed as text fails theinstanceof Datecheck and the post will never advance — format the column as a date. - The transition is one step per run, but because the trigger fires hourly an
approved draft with a past publish date reaches
publishedwithin two runs. setValuesrewrites the whole grid every run. If editors are typing in the sheet at the same moment, a rare edit could be overwritten — an hourly trigger keeps that window small, but avoid running it during a busy editing session.- The script assumes the calendar is the first sheet. If you add tabs, point
getSheets()[0]at the correct one or fetch it by name withgetSheetByName('Calendar').
Related
Build an OKR tracker with progress rollups
Aggregate Northwind's key-result progress into objective-level scores automatically.
Updated Jan 14, 2026
Build a recurring-task generator
Spawn new Northwind task rows on a daily or weekly cadence from a Recurring sheet.
Updated Jan 10, 2026
Build a multi-sheet search-and-jump tool
Find a value across every tab of a workbook and click through to the cell that contains it.
Updated Jan 7, 2026
Auto-rebuild grouped summaries on edit
Refresh pivot-style rollups the instant data changes — no manual recompute.
Updated Jan 4, 2026
Build a data-quality scorecard
Grade any sheet on completeness, validity, and freshness — surface gaps as a single score.
Updated Dec 31, 2025