research/guarding-an-automated-social-post

Guarding an Automated Social Post: Four Checks Before Anything Goes Out

Engineering Note · November 6, 2026 · 6 minute read · Node.js · Automation · Social Media

A guard for an automated post can be small. Ours is 30 lines of Node.js and runs four checks: which account is logged in, whether today is a posting day, whether a hold file exists, and whether the live site shows what the post is about. It runs every check and lists every failure. We tested it against eight cases, and it caught all of them.

Everything below was run with Node.js 22.23.4 on Linux. The account names and page content are made up for the test, and the “site” is a throwaway local server so we could make it fail on demand. The outputs are the real ones from this run.

Why guard a post at all?

A scheduled job that posts for you is easy to forget about. That is the point of it. It also means nobody is watching when something changes: the job gets logged in to a different account, the schedule fires on the wrong day, or the page you promise in the post is down. A post cannot be fully taken back. Followers may have seen it, and screenshots last.

So we put the checks before the post, not after. We use four.

CheckWhat it prevents
AccountPosting from a personal account instead of the brand account
DayA run on a day it should not post, including a time-zone slip
Hold filePosting while you have asked it to wait
Live sitePromoting a page that is down or showing the wrong thing

What does the guard look like?

import { existsSync } from 'node:fs';

// Returns { ok, reasons }. Every check runs, so one run reports every problem.
export async function guard({ expectedAccount, actualAccount, allowedDays, now, holdFile, siteUrl, mustContain, timeZone = 'America/New_York' }) {
  const reasons = [];

  if (actualAccount !== expectedAccount) {
    reasons.push(`account: logged in as "${actualAccount}", expected "${expectedAccount}"`);
  }

  const day = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone }).format(now);
  if (!allowedDays.includes(day)) {
    reasons.push(`day: ${day} is not one of ${allowedDays.join(', ')}`);
  }

  if (existsSync(holdFile)) {
    reasons.push(`hold: ${holdFile} exists`);
  }

  try {
    const res = await fetch(siteUrl, { signal: AbortSignal.timeout(5000) });
    const body = await res.text();
    if (!res.ok) reasons.push(`site: ${siteUrl} returned ${res.status}`);
    else if (!body.includes(mustContain)) reasons.push(`site: ${siteUrl} is up but does not contain "${mustContain}"`);
  } catch (e) {
    reasons.push(`site: ${siteUrl} unreachable (${e.cause?.code ?? e.message})`);
  }

  return { ok: reasons.length === 0, reasons };
}

The function takes everything as arguments, including the current time and the paths. That is deliberate. It lets a test pass in a fake clock and a local server instead of waiting for a Monday.

Two details matter. First, Intl.DateTimeFormat with a timeZone gives the weekday in the zone you choose, with no date library. Second, the site check looks at the body, not only the status code. A site can answer 200 and still show the wrong page.

How did we test it?

We started a small local web server that serves a page containing the words “Weekly countdown”, with switches to make it return a 503 or the wrong page. Then we ran the guard against eight cases. The test file is below.

import { createServer } from 'node:http';
import { writeFileSync, rmSync } from 'node:fs';
import { guard } from './guard.mjs';

let page = '<h1>Weekly countdown</h1>';
const server = createServer((req, res) => {
  if (page === null) { res.statusCode = 503; return res.end('down'); }
  res.end(page);
}).listen(0);
await new Promise(r => server.once('listening', r));
const siteUrl = `http://127.0.0.1:${server.address().port}/`;

const base = {
  expectedAccount: '@repotop_demo', actualAccount: '@repotop_demo',
  allowedDays: ['Mon'], now: new Date('2026-11-09T14:00:00Z'), // Mon 9:00 AM New York
  holdFile: './HOLD', siteUrl, mustContain: 'Weekly countdown'
};
rmSync('./HOLD', { force: true });

async function show(name, overrides, setup) {
  setup?.();
  const r = await guard({ ...base, ...overrides });
  console.log(`${name}\n  ok=${r.ok}${r.reasons.map(x => `\n  - ${x}`).join('')}`);
}

await show('1. everything right', {});
await show('2. wrong account', { actualAccount: '@personal_account' });
await show('3. wrong day (Tuesday)', { now: new Date('2026-11-10T14:00:00Z') });
await show('4. hold file present', {}, () => writeFileSync('./HOLD', ''));
rmSync('./HOLD');
await show('5. site returns 503', {}, () => { page = null; });
await show('6. site up, wrong page', {}, () => { page = '<h1>Welcome to nginx!</h1>'; });
await show('7. everything wrong at once', { actualAccount: '@x', now: new Date('2026-11-10T14:00:00Z') }, () => writeFileSync('./HOLD', ''));
rmSync('./HOLD');
// Day boundary: 03:30 UTC Tuesday is still Monday 10:30 PM in New York.
page = '<h1>Weekly countdown</h1>';
await show('8. 03:30 UTC Tuesday = Monday night in New York', { now: new Date('2026-11-10T03:30:00Z') });
server.close();

Here is the output, with the local port number as it appeared in our run.

1. everything right
  ok=true
2. wrong account
  ok=false
  - account: logged in as "@personal_account", expected "@repotop_demo"
3. wrong day (Tuesday)
  ok=false
  - day: Tue is not one of Mon
4. hold file present
  ok=false
  - hold: ./HOLD exists
5. site returns 503
  ok=false
  - site: http://127.0.0.1:40295/ returned 503
6. site up, wrong page
  ok=false
  - site: http://127.0.0.1:40295/ is up but does not contain "Weekly countdown"
7. everything wrong at once
  ok=false
  - account: logged in as "@x", expected "@repotop_demo"
  - day: Tue is not one of Mon
  - hold: ./HOLD exists
  - site: http://127.0.0.1:40295/ is up but does not contain "Weekly countdown"
8. 03:30 UTC Tuesday = Monday night in New York
  ok=true

Case 1 passes, and each of cases 2 to 6 trips exactly the check we broke. In case 7 the account, day and hold checks all fail, and the site check fails too, because the test server was still showing the wrong page from case 6. That is the test setup, not a bug. It does show the value of reporting everything at once: one run, four reasons.

Why does the time zone matter for the day check?

Case 8 is the one most people get wrong. A scheduler on a server in UTC fires at 03:30 on a Tuesday. In New York that is still Monday night. We ran the same moment through both zones:

UTC               Tue 3:30 AM
America/New_York  Mon 10:30 PM

If the job should post on Mondays, a UTC day check would block a run that is still Monday where your audience lives. Pick the zone your audience lives in and pass it explicitly.

What would we add?

  • A check that the post has not already gone out today. Retries and duplicate triggers happen. Writing a small “posted” file after success, and checking for it, stops a double post.
  • A non-zero exit code on failure. Wrap the guard in a script that exits with status 1 when ok is false, so the scheduler records a failed run instead of a quiet success.
  • A notification on failure. A guard that blocks a post silently only moves the problem. Send yourself the reasons.

Limitations

  • We did not post to any real social network. The account name is a string we supplied; in a real job you would read it from the platform’s API, and we did not test any platform’s API here.
  • The site check tests for one phrase. A page can contain the phrase and still be broken in other ways.
  • The hold file only works if the job runs somewhere that can see it. A job in a container needs the file on a mounted path.
  • Our “everything wrong” case reused state left by an earlier case, which we explained above. The guard itself behaved correctly.
  • The day check allows or blocks whole days. It does not check the time of day.

Frequently asked questions

What should an automated social post check before it publishes?

Check four things: which account the job is logged in as, whether today is a day it should post, whether someone has left a hold file asking it to wait, and whether the page you are promoting is live and shows the right content. Each check is cheap, and each one prevents a post that is hard to take back.

Why use a hold file instead of just disabling the job?

A hold file is a file whose existence stops the post. Creating or deleting a file works from a phone, a terminal or another script, and it leaves the schedule itself untouched. Disabling and re-enabling a scheduled job means remembering to turn it back on.

Why check the live site and not just the status code?

A page can return 200 and still be wrong, such as a default server page or an error template. In our test the guard fetched the page and looked for an expected phrase, so a site that was up but showing the wrong content failed the check.

Why does the time zone matter for a day check?

Servers usually run on UTC. At 03:30 UTC on a Tuesday it is still 10:30 PM Monday in New York, so a day check done in UTC gives the wrong day for an evening or early-morning job. Pass the time zone you mean.

Should the guard stop at the first failed check?

We run every check and report all failures. If you fix the account and only then learn the site is down, you lose a run. Reporting everything at once shows the whole picture in one pass.

More from Research

← All research