ワンクリックで
cron-scheduler
>-
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
>-
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Route complex requests to the right specialist agent or chain of agents. This skill acts as the central brain of an agent swarm — it analyses what the user needs, determines which specialist domain(s) are required, and coordinates parallel or sequential agent execution. Use this skill when a request spans multiple domains (e.g., "research competitors and create a pitch deck"), when you need to decide which specialist should handle an ambiguous request, or when a task requires a multi-step pipeline across different skills. Triggers on: multi-step requests, cross-domain tasks, "coordinate", "plan this out", "I need help with multiple things", or any complex request that touches more than one specialist area. Also triggers when the user seems unsure which tool or approach to use.
>-
Hybrid DAG execution primitive combining deterministic and agentic nodes with hard iteration caps
">"
Act as a brand ambassador — create authentic, platform-specific social media content that embodies a brand's identity and connects with audiences. Use this skill whenever the user asks to "create social media content for a brand", "act as a brand ambassador", "write ambassador posts", "promote [brand] on social media", "create influencer-style content", "write authentic brand content", "social media ambassador", or any request involving representing a brand through social content. Also triggers on "ambassador voice", "brand promotion posts", "influencer content", "authentic brand posts", "UGC-style content", or when someone wants social media content that sounds like a real person recommending a brand rather than corporate marketing copy. Even if the user just says "help me promote [brand]" or "I need content for [brand]'s social channels" — use this skill.
>-
| id | cron-scheduler |
| name | cron-scheduler |
| type | skill |
| version | 1.0.0 |
| created | 20/03/2026 |
| modified | 20/03/2026 |
| status | active |
| metadata | {"author":"NodeJS-Starter-V1","version":"1.0.0","locale":"en-AU"} |
| description | >- |
| context | fork |
vercel.json/api/cron/* endpointsstate-machine instead)genesis-orchestrator instead)CRON_SECRETConfiguration: apps/web/vercel.json → crons array
| Route | Schedule | Purpose |
|---|---|---|
/api/cron/cleanup-old-runs | Daily 2:00 AM | Delete completed agent runs older than 30 days |
/api/cron/health-check | Every 5 minutes | Ping backend, check responsiveness |
/api/cron/daily-report | Daily 9:00 AM | Generate yesterday's agent activity summary |
Location: apps/web/lib/audit/scheduled-audit-runner.ts
In-process scheduler using setInterval with AuditSchedule type and simplified cron parsing. Supports add/remove/toggle schedules.
Every cron route handler must follow this structure:
import { NextResponse } from 'next/server';
import { logger } from '@/lib/logger';
/**
* {Job Name} Cron Job
*
* Runs {schedule description} ({cron expression})
* {What it does}
*/
export async function GET(request: Request) {
const startedAt = Date.now();
try {
// 1. Authenticate
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new NextResponse('Unauthorised', { status: 401 });
}
// 2. Execute job logic
const result = await executeJob();
// 3. Log success with duration
const duration = Date.now() - startedAt;
logger.info('cron_job_completed', {
job: 'job-name',
duration_ms: duration,
result,
});
// 4. Return structured response
return NextResponse.json({
success: true,
...result,
duration_ms: duration,
timestamp: new Date().toISOString(),
});
} catch (error) {
const duration = Date.now() - startedAt;
logger.error('cron_job_failed', {
job: 'job-name',
duration_ms: duration,
error: error instanceof Error ? error.message : 'Unknown error',
});
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}
apps/web/app/api/cron/
├── cleanup-old-runs/route.ts # Existing
├── health-check/route.ts # Existing
├── daily-report/route.ts # Existing
└── {new-job-name}/route.ts # New jobs go here
When adding a new cron job, update apps/web/vercel.json:
{
"crons": [
{
"path": "/api/cron/{job-name}",
"schedule": "{cron expression}"
}
]
}
Every cron endpoint must validate the CRON_SECRET environment variable:
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new NextResponse('Unauthorised', { status: 401 });
}
No authentication or hardcoded secrets are REJECTED. Store CRON_SECRET in .env.local.
The existing cron jobs lack overlap protection. Use one of these patterns:
import { createClient } from '@/lib/supabase/server';
async function acquireLock(jobName: string, ttlSeconds: number): Promise<boolean> {
const supabase = await createClient();
const lockId = `cron_lock_${jobName}`;
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
// Attempt to insert lock row — fails if already locked
const { error } = await supabase
.from('cron_locks')
.upsert(
{ id: lockId, locked_at: new Date().toISOString(), expires_at: expiresAt },
{ onConflict: 'id', ignoreDuplicates: false }
)
.lt('expires_at', new Date().toISOString()); // Only if expired
return !error;
}
async function releaseLock(jobName: string): Promise<void> {
const supabase = await createClient();
await supabase
.from('cron_locks')
.delete()
.eq('id', `cron_lock_${jobName}`);
}
Usage in a cron handler:
export async function GET(request: Request) {
// ... auth check ...
const locked = await acquireLock('cleanup-old-runs', 300);
if (!locked) {
return NextResponse.json({ skipped: true, reason: 'Already running' });
}
try {
const result = await executeJob();
return NextResponse.json({ success: true, ...result });
} finally {
await releaseLock('cleanup-old-runs');
}
}
class ScheduledAuditRunner {
private running: Set<string> = new Set();
async runAudit(type: AuditType, config?: AuditConfig): Promise<ScheduledAuditResult> {
const lockKey = `${type}_${JSON.stringify(config ?? {})}`;
if (this.running.has(lockKey)) {
return { status: 'skipped', reason: 'Already running' } as ScheduledAuditResult;
}
this.running.add(lockKey);
try {
// ... existing audit logic ...
} finally {
this.running.delete(lockKey);
}
}
}
CREATE TABLE IF NOT EXISTS cron_locks (
id TEXT PRIMARY KEY,
locked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
-- Auto-cleanup expired locks
CREATE INDEX idx_cron_locks_expires ON cron_locks (expires_at);
from contextlib import asynccontextmanager
from asyncio import create_task, sleep, Task
_background_tasks: list[Task] = []
async def periodic_cleanup(interval_seconds: int = 3600) -> None:
"""Run cleanup every hour."""
while True:
try:
await run_cleanup_job()
except Exception as e:
logger.error("periodic_cleanup_failed", error=str(e))
await sleep(interval_seconds)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: launch background tasks
task = create_task(periodic_cleanup(3600))
_background_tasks.append(task)
yield
# Shutdown: cancel all background tasks
for task in _background_tasks:
task.cancel()
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
scheduler = AsyncIOScheduler(timezone="Australia/Brisbane")
scheduler.add_job(
cleanup_old_runs,
CronTrigger(hour=2, minute=0),
id="cleanup_old_runs",
replace_existing=True,
misfire_grace_time=300,
)
scheduler.start()
┌─────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌─────────── day of month (1-31)
│ │ │ ┌─────────── month (1-12)
│ │ │ │ ┌─────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
| Expression | Schedule | Use Case |
|---|---|---|
*/5 * * * * | Every 5 minutes | Health checks |
0 * * * * | Every hour | Metrics aggregation |
0 2 * * * | Daily 2:00 AM | Data cleanup |
0 9 * * * | Daily 9:00 AM | Daily reports |
0 9 * * 1-5 | Weekdays 9:00 AM | Business reports |
0 0 1 * * | Monthly (1st) | Monthly aggregation |
0 0 * * 0 | Weekly (Sunday) | Weekly cleanup |
Vercel cron runs in UTC. Convert Australian times:
| AEST (UTC+10) | AEDT (UTC+11) | UTC | Cron |
|---|---|---|---|
| 9:00 AM AEST | 10:00 AM AEDT | 23:00 (prev day) | 0 23 * * * |
| 2:00 AM AEST | 3:00 AM AEDT | 16:00 (prev day) | 0 16 * * * |
| 12:00 PM AEST | 1:00 PM AEDT | 02:00 | 0 2 * * * |
For the backend (APScheduler), set timezone="Australia/Brisbane" to avoid manual conversion.
Cron jobs may execute more than once (retries, clock drift). Design for idempotency:
// GOOD: Idempotent — deletes by criteria, safe to re-run
await supabase.from('agent_runs').delete()
.in('status', ['completed', 'failed'])
.lt('completed_at', cutoffDate.toISOString());
// GOOD: Idempotent insert — upsert on unique key
await supabase.from('daily_reports')
.upsert({ date: yesterday, data: report }, { onConflict: 'date' });
REJECTED: Plain .insert() without upsert — creates duplicates on retry.
Every cron job must emit these log events:
// On start
logger.info('cron_job_started', { job: 'cleanup-old-runs' });
// On success
logger.info('cron_job_completed', {
job: 'cleanup-old-runs',
duration_ms: 1234,
records_processed: 42,
});
// On failure
logger.error('cron_job_failed', {
job: 'cleanup-old-runs',
duration_ms: 567,
error: 'Connection timeout',
});
// On skip (overlap protection)
logger.warn('cron_job_skipped', {
job: 'cleanup-old-runs',
reason: 'Already running',
});
Alert after 3+ consecutive failures. Track failure count per job in the database or in-memory store. Log cron_job_alert with consecutive_failures count when threshold is breached.
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
No CRON_SECRET validation | Public endpoint, anyone can trigger | Bearer token auth on every handler |
setInterval for critical jobs | Drift, no persistence across restarts | Vercel cron or APScheduler |
| No overlap protection | Concurrent runs corrupt shared state | Database lock or in-memory flag |
| Hardcoded UTC offsets | Breaks on AEST/AEDT transition | Use Australia/Brisbane timezone |
| Non-idempotent inserts | Duplicate records on retry | Upsert with unique constraint |
| Silent failures | Jobs fail without anyone knowing | Structured logging + alerting |
apps/web/app/api/cron/{job-name}/route.tsapps/web/vercel.jsonCRON_SECRET validation at top of handlerAbortSignal.timeout)cron_job_started log eventcron_job_completed log event with duration_mscron_job_failed log event with error detailscron_job_skipped log event for overlap protectionAustralia/Brisbane timezone for APScheduler[AGENT_ACTIVATED]: Cron Scheduler
[PHASE]: {Design | Implementation | Review}
[STATUS]: {in_progress | complete}
{scheduling analysis or implementation guidance}
[NEXT_ACTION]: {what to do next}
structured-logging patternscorrelation_id per execution for tracing across servicesAUTH_VALIDATION_MISSING_TOKEN (401)SYS_RUNTIME_INTERNAL (500)WORKFLOW_CONFLICT_LOCKED (409)PENDING → RUNNING → COMPLETED/FAILED (same as NodeStatus)UNLOCKED → LOCKED → UNLOCKED (with TTL expiry)timestamp in ISO 8601