Scheduling a social media post sounds simple. You pick a time, you publish at that time. But if you've tried to build this yourself, you know there are at least three ways it can quietly fail:
- Your cron job runs but the token expired overnight
- The video is still processing on Instagram's side when you try to publish
- Your server restarts and the in-memory job queue is gone
This tutorial covers a robust approach to scheduling posts to Instagram, TikTok, and YouTube using Node.js — including how to handle all three failure modes above.
Prerequisites
- Node.js 18+
- An InReelForge account (free tier works) — indreelforge.com
- Your social accounts connected in the InReelForge dashboard
Setup
mkdir social-scheduler && cd social-scheduler
npm init -y
npm install indreelforge dotenv
Create .env:
INDREELFORGE_API_KEY=your_api_key_here
Basic Scheduling
The simplest case — schedule a post with a future timestamp:
import InReelForge from 'indreelforge';
import 'dotenv/config';
const client = new InReelForge({ apiKey: process.env.INDREELFORGE_API_KEY });
async function schedulePost(videoPath, publishAt) {
const result = await client.publish({
video: videoPath,
platforms: ['instagram', 'tiktok', 'youtube'],
caption: 'New video dropping now 🎬',
hashtags: ['content', 'video'],
schedule: publishAt.toISOString(),
});
console.log('Scheduled:', result.jobId);
console.log('Status per platform:', result.platforms);
return result;
}
// Schedule for tomorrow at 9am UTC
const tomorrow9am = new Date();
tomorrow9am.setUTCDate(tomorrow9am.getUTCDate() + 1);
tomorrow9am.setUTCHours(9, 0, 0, 0);
await schedulePost('./my-video.mp4', tomorrow9am);
The schedule is persisted server-side in a database-backed queue — if InReelForge restarts, your job survives. You don't need to run your own scheduler.
Per-Platform Scheduling
Different platforms have different peak times. You can send each platform its own schedule:
const result = await client.publish({
video: './video.mp4',
caption: 'Default caption',
platforms: ['instagram', 'tiktok', 'youtube'],
overrides: {
instagram: {
schedule: '2026-06-01T11:00:00Z', // 11am UTC
caption: 'Instagram-specific caption with #reels #viral',
},
tiktok: {
schedule: '2026-06-01T18:00:00Z', // 6pm UTC — peak TikTok time
caption: 'TikTok hook caption here',
},
youtube: {
schedule: '2026-06-01T14:00:00Z', // 2pm UTC
caption: 'Full YouTube description with timestamps\n\n0:00 Intro\n0:30 Main content',
},
},
});
Building a Simple Content Queue
If you're managing a backlog of content, here's a pattern for a queue processor:
import { readdir, readFile } from 'fs/promises';
import path from 'path';
import InReelForge from 'indreelforge';
const client = new InReelForge({ apiKey: process.env.INDREELFORGE_API_KEY });
// Each file in /queue/ is a JSON job descriptor
async function processQueue(queueDir) {
const files = await readdir(queueDir);
const pending = files.filter(f => f.endsWith('.json'));
for (const file of pending) {
const job = JSON.parse(await readFile(path.join(queueDir, file), 'utf8'));
try {
const result = await client.publish({
video: job.videoPath,
platforms: job.platforms,
caption: job.caption,
hashtags: job.hashtags,
schedule: job.publishAt,
});
console.log(`✓ Scheduled ${file} → jobId: ${result.jobId}`);
// Move to /done/
await rename(
path.join(queueDir, file),
path.join(queueDir, '../done', file)
);
} catch (err) {
console.error(`✗ Failed ${file}:`, err.message);
}
}
}
await processQueue('./queue');
A job JSON file (queue/post-001.json) looks like:
{
"videoPath": "./videos/demo.mp4",
"platforms": ["instagram", "tiktok", "youtube"],
"caption": "New video — check it out!",
"hashtags": ["saas", "demo"],
"publishAt": "2026-06-01T09:00:00Z"
}
Checking Job Status
After scheduling, poll for results or set up a webhook:
// Poll approach
async function waitForResult(jobId, intervalMs = 5000) {
while (true) {
const status = await client.getJob(jobId);
const allDone = Object.values(status.platforms).every(
s => s === 'published' || s === 'failed'
);
if (allDone) return status;
console.log('Still processing...', status.platforms);
await new Promise(r => setTimeout(r, intervalMs));
}
}
const final = await waitForResult(result.jobId);
console.log('Final status:', final.platforms);
Webhook Approach (Better for Production)
Instead of polling, receive a callback when each platform finishes:
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/indreelforge', (req, res) => {
const sig = req.headers['x-irf-signature'];
const digest = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(digest))) {
return res.status(403).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log(`${event.platform}: ${event.status}`);
// event.status: 'published' | 'failed'
// event.platform: 'instagram' | 'tiktok' | 'youtube' etc.
// event.url: public URL of the published post (when published)
res.sendStatus(200);
});
app.listen(3000);
Set your webhook URL in the InReelForge dashboard under Settings → Webhooks.
Common Mistakes
Don't schedule in the past. The API will reject timestamps more than 5 minutes behind the current time.
Don't assume immediate publishing. Even "publish now" goes through a queue — Instagram and TikTok take 30–120 seconds to process video.
Do validate video before scheduling. Use the /validate endpoint to check format compatibility before uploading:
const validation = await client.validate({
video: './video.mp4',
platforms: ['instagram', 'tiktok'],
});
if (!validation.compatible) {
console.log('Issues:', validation.issues);
// e.g. [{ platform: 'instagram', issue: 'aspect_ratio', detail: 'Expected 9:16, got 16:9' }]
}
Full Example Script
import InReelForge from 'indreelforge';
import 'dotenv/config';
const client = new InReelForge({ apiKey: process.env.INDREELFORGE_API_KEY });
const video = './product-launch.mp4';
const platforms = ['instagram', 'tiktok', 'youtube'];
const publishAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 24h from now
// 1. Validate
const check = await client.validate({ video, platforms });
if (!check.compatible) {
console.error('Video not compatible:', check.issues);
process.exit(1);
}
// 2. Schedule
const job = await client.publish({
video,
platforms,
caption: 'We just launched something big. Check it out.',
hashtags: ['launch', 'saas', 'buildinpublic'],
schedule: publishAt,
overrides: {
youtube: { caption: 'Full product demo with timestamps...' },
},
});
console.log(`Scheduled ${platforms.length} posts for ${publishAt}`);
console.log(`Job ID: ${job.jobId}`);
Full API reference: docs.indreelforge.com
Free tier — 10 posts/month: indreelforge.com
Top comments (0)