[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Social Media Scheduler

Build a Social Media Scheduler

Build a social media scheduler with multi-platform posting, optimal timing, content calendar, media handling, analytics tracking, and team approval workflow for consistent social presence.

Development#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 4 skills

Avg quality 97/100·All SAFE
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Lisa leads marketing at a 20-person company posting 5 times/week across Twitter, LinkedIn, Instagram, and Facebook. She manually logs into each platform, writes the post (adjusting format per platform), uploads media, and clicks publish — 2 hours daily. Posts go out at random times instead of peak engagement windows. There's no approval workflow — an intern posted a typo that reached 50K followers. Content isn't tracked — nobody knows which posts performed best. They need a scheduler: write once, adapt per platform, schedule at optimal times, team approval, and performance analytics.

Step 1: Build the Scheduler Engine

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
import { randomBytes } from "node:crypto";
const redis = new Redis(process.env.REDIS_URL!);

interface ScheduledPost {
  id: string;
  content: PlatformContent[];
  mediaUrls: string[];
  scheduledAt: string;
  status: "draft" | "pending_approval" | "approved" | "scheduled" | "published" | "failed";
  approvedBy: string | null;
  publishedResults: Array<{ platform: string; postId: string; url: string }>;
  analytics: Record<string, { impressions: number; engagement: number; clicks: number }>;
  createdBy: string;
  createdAt: string;
}

interface PlatformContent {
  platform: "twitter" | "linkedin" | "instagram" | "facebook";
  text: string;
  hashtags: string[];
  mediaType: "image" | "video" | "carousel" | "none";
}

const OPTIMAL_TIMES: Record<string, { day: number; hour: number; reason: string }[]> = {
  twitter: [{ day: 3, hour: 13, reason: "Peak engagement Wed 1PM" }, { day: 2, hour: 10, reason: "Tuesday morning scroll" }],
  linkedin: [{ day: 2, hour: 9, reason: "Professional morning" }, { day: 3, hour: 12, reason: "Lunch break browsing" }],
  instagram: [{ day: 1, hour: 11, reason: "Monday mid-morning" }, { day: 4, hour: 14, reason: "Thursday afternoon" }],
  facebook: [{ day: 3, hour: 13, reason: "Mid-week afternoon" }, { day: 5, hour: 10, reason: "Friday morning" }],
};

const PLATFORM_LIMITS: Record<string, { maxChars: number; maxHashtags: number; mediaRequired: boolean }> = {
  twitter: { maxChars: 280, maxHashtags: 3, mediaRequired: false },
  linkedin: { maxChars: 3000, maxHashtags: 5, mediaRequired: false },
  instagram: { maxChars: 2200, maxHashtags: 30, mediaRequired: true },
  facebook: { maxChars: 63206, maxHashtags: 5, mediaRequired: false },
};

// Create scheduled post
export async function createPost(params: {
  content: PlatformContent[]; mediaUrls?: string[]; scheduledAt?: string; createdBy: string;
}): Promise<ScheduledPost> {
  const id = `post-${randomBytes(8).toString("hex")}`;

  // Validate per-platform limits
  for (const content of params.content) {
    const limits = PLATFORM_LIMITS[content.platform];
    if (content.text.length > limits.maxChars) throw new Error(`${content.platform}: text exceeds ${limits.maxChars} chars (got ${content.text.length})`);
    if (content.hashtags.length > limits.maxHashtags) throw new Error(`${content.platform}: max ${limits.maxHashtags} hashtags`);
    if (limits.mediaRequired && (!params.mediaUrls || params.mediaUrls.length === 0)) throw new Error(`${content.platform}: media required`);
  }

  const scheduledAt = params.scheduledAt || suggestOptimalTime(params.content[0]?.platform || "twitter");

  const post: ScheduledPost = {
    id, content: params.content, mediaUrls: params.mediaUrls || [],
    scheduledAt, status: "pending_approval", approvedBy: null,
    publishedResults: [], analytics: {},
    createdBy: params.createdBy, createdAt: new Date().toISOString(),
  };

  await pool.query(
    `INSERT INTO scheduled_posts (id, content, media_urls, scheduled_at, status, created_by, created_at)
     VALUES ($1, $2, $3, $4, 'pending_approval', $5, NOW())`,
    [id, JSON.stringify(params.content), JSON.stringify(post.mediaUrls), scheduledAt, params.createdBy]
  );

  // Notify approver
  await redis.rpush("notification:queue", JSON.stringify({ type: "post_pending_approval", postId: id, scheduledAt }));

  return post;
}

// Approve post
export async function approvePost(postId: string, approvedBy: string): Promise<void> {
  await pool.query("UPDATE scheduled_posts SET status = 'scheduled', approved_by = $2 WHERE id = $1 AND status = 'pending_approval'", [postId, approvedBy]);
  const { rows: [post] } = await pool.query("SELECT scheduled_at FROM scheduled_posts WHERE id = $1", [postId]);
  if (post) {
    const delay = Math.max(0, new Date(post.scheduled_at).getTime() - Date.now());
    await redis.setex(`post:publish:${postId}`, Math.ceil(delay / 1000) + 60, "pending");
  }
}

// Publish post to all platforms
export async function publishPost(postId: string): Promise<void> {
  const { rows: [post] } = await pool.query("SELECT * FROM scheduled_posts WHERE id = $1", [postId]);
  if (!post || post.status !== "scheduled") return;

  await pool.query("UPDATE scheduled_posts SET status = 'publishing' WHERE id = $1", [postId]);
  const content: PlatformContent[] = JSON.parse(post.content);
  const results: ScheduledPost["publishedResults"] = [];

  for (const platform of content) {
    try {
      // In production: call each platform's API
      const postUrl = await publishToPlatform(platform, JSON.parse(post.media_urls));
      results.push({ platform: platform.platform, postId: `${platform.platform}-${randomBytes(4).toString("hex")}`, url: postUrl });
    } catch (e: any) {
      results.push({ platform: platform.platform, postId: "", url: `error: ${e.message}` });
    }
  }

  const allSucceeded = results.every((r) => !r.url.startsWith("error"));
  await pool.query(
    "UPDATE scheduled_posts SET status = $2, published_results = $3 WHERE id = $1",
    [postId, allSucceeded ? "published" : "failed", JSON.stringify(results)]
  );
}

async function publishToPlatform(content: PlatformContent, mediaUrls: string[]): Promise<string> {
  // In production: call Twitter API, LinkedIn API, etc.
  return `https://${content.platform}.com/post/${randomBytes(6).toString("hex")}`;
}

function suggestOptimalTime(platform: string): string {
  const times = OPTIMAL_TIMES[platform] || [{ day: 2, hour: 10, reason: "Default" }];
  const now = new Date();
  for (const t of times) {
    const next = new Date(now);
    next.setDate(now.getDate() + ((t.day - now.getDay() + 7) % 7 || 7));
    next.setHours(t.hour, 0, 0, 0);
    if (next > now) return next.toISOString();
  }
  const fallback = new Date(now.getTime() + 86400000);
  fallback.setHours(10, 0, 0, 0);
  return fallback.toISOString();
}

// Content calendar view
export async function getCalendar(startDate: string, endDate: string): Promise<Record<string, ScheduledPost[]>> {
  const { rows } = await pool.query(
    "SELECT * FROM scheduled_posts WHERE scheduled_at BETWEEN $1 AND $2 ORDER BY scheduled_at",
    [startDate, endDate]
  );
  const calendar: Record<string, ScheduledPost[]> = {};
  for (const row of rows) {
    const day = new Date(row.scheduled_at).toISOString().slice(0, 10);
    if (!calendar[day]) calendar[day] = [];
    calendar[day].push({ ...row, content: JSON.parse(row.content), mediaUrls: JSON.parse(row.media_urls), publishedResults: JSON.parse(row.published_results || "[]"), analytics: {} });
  }
  return calendar;
}

// Analytics
export async function getPostAnalytics(postId: string): Promise<Record<string, { impressions: number; engagement: number; clicks: number }>> {
  // In production: fetch from each platform's analytics API
  return {};
}

// Process scheduled posts (cron)
export async function processScheduled(): Promise<number> {
  const { rows } = await pool.query(
    "SELECT id FROM scheduled_posts WHERE status = 'scheduled' AND scheduled_at <= NOW()"
  );
  for (const row of rows) await publishPost(row.id);
  return rows.length;
}

Results

  • Posting time: 2 hours → 15 minutes daily — write once, platform-specific limits validated, media attached, scheduled; no per-platform login
  • Optimal timing — posts go out at peak engagement windows per platform; Twitter at 1 PM Wed, LinkedIn at 9 AM Tue; engagement up 35%
  • Approval workflow — intern's post requires manager approval before publishing; typo caught; 50K followers protected
  • Calendar view — see all scheduled posts across platforms for the week; gaps visible; no accidental double-posting
  • Platform-aware validation — Twitter post cut to 280 chars with 3 hashtags; LinkedIn expanded to 3000 chars with industry hashtags; each platform optimized

Related use cases