원클릭으로
add-cron-job
Scaffold a new cron job with in-process guard, env-configurable schedule, SyncStatus DB locking, and job registry wiring.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Scaffold a new cron job with in-process guard, env-configurable schedule, SyncStatus DB locking, and job registry wiring.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Scaffold a new Express API endpoint with controller, route (OpenAPI annotations), response type, and barrel exports.
Low-level Cardano utilities with @meshsdk/core-cst
Cardano transaction building with @meshsdk/transaction
Cardano wallet integration with @meshsdk/wallet
Scaffold a new Prisma database model with project conventions (snake_case mapping, BigInt for lovelace, timestamps, relations).
Analyze feedback and evolve skills through structured improvement. The meta-skill that makes other skills better.
| name | add-cron-job |
| description | Scaffold a new cron job with in-process guard, env-configurable schedule, SyncStatus DB locking, and job registry wiring. |
Scaffold a new cron job following the project's established patterns: in-process guard, configurable schedule, optional SyncStatus distributed locking, and registration in the job registry.
$0 - Job name in kebab-case (e.g., cleanup-stale-votes, sync-delegations)$1 - Default cron schedule (e.g., 0 */12 * * * for every 12 hours, */30 * * * * for every 30 min)$2 - Short description (e.g., "Remove stale vote records older than 30 days")Create src/jobs/{$0}.job.ts:
/**
* {$2}
* Schedule: {$1} (configurable via {ENV_VAR_NAME} env variable)
*/
import cron from "node-cron";
import { prisma } from "../services";
// In-process guard to prevent overlapping runs in a single Node process
let isRunning = false;
/**
* Starts the {display name} cron job
*/
export const start{PascalCaseName}Job = () => {
const schedule = process.env.{ENV_VAR_NAME} || "{$1}";
const enabled = process.env.ENABLE_CRON_JOBS !== "false";
if (!enabled) {
console.log("[Cron] {Display name} job disabled via ENABLE_CRON_JOBS env variable");
return;
}
if (!cron.validate(schedule)) {
console.error(`[Cron] Invalid cron schedule for {$0}: ${schedule}`);
return;
}
cron.schedule(schedule, async () => {
if (isRunning) {
console.log(`[${new Date().toISOString()}] {Display name} job still running, skipping`);
return;
}
isRunning = true;
const timestamp = new Date().toISOString();
console.log(`\n[${timestamp}] Starting {display name} job...`);
try {
// TODO: Implement job logic here
// const result = await myServiceFunction(prisma);
console.log(`[${timestamp}] {Display name} job completed successfully`);
} catch (error: any) {
console.error(`[${timestamp}] {Display name} job failed:`, error.message);
} finally {
isRunning = false;
}
});
console.log(`[Cron] {Display name} job scheduled: ${schedule}`);
};
Naming conventions:
{$0}.job.ts (kebab-case)start{PascalCase}Job (e.g., startCleanupStaleVotesJob){SCREAMING_SNAKE}_SCHEDULE (e.g., CLEANUP_STALE_VOTES_SCHEDULE)Add to src/jobs/index.ts:
import { start{PascalCaseName}Job } from "./{$0}.job";
export const startAllJobs = () => {
console.log("[Cron] Initializing all cron jobs...");
startProposalSyncJob();
startVoterPowerSyncJob();
start{PascalCaseName}Job(); // ← Add here
console.log("[Cron] All cron jobs initialized");
};
For jobs running in GCP Cloud Run (multiple instances), add DB-level locking via the SyncStatus model to prevent concurrent execution:
import cron from "node-cron";
import { prisma } from "../services";
import { randomUUID } from "crypto";
let isRunning = false;
const JOB_NAME = "{$0}";
const LOCK_TIMEOUT_MINUTES = 30; // Auto-unlock after 30 min (crash recovery)
/**
* Attempt to acquire distributed lock
*/
async function acquireLock(instanceId: string): Promise<boolean> {
try {
// Upsert the SyncStatus record, only lock if not already running
const result = await prisma.syncStatus.upsert({
where: { jobName: JOB_NAME },
create: {
jobName: JOB_NAME,
displayName: "{Display Name}",
isRunning: true,
startedAt: new Date(),
lockedBy: instanceId,
expiresAt: new Date(Date.now() + LOCK_TIMEOUT_MINUTES * 60 * 1000),
},
update: {
isRunning: true,
startedAt: new Date(),
lockedBy: instanceId,
expiresAt: new Date(Date.now() + LOCK_TIMEOUT_MINUTES * 60 * 1000),
},
});
// Check if we actually got the lock (another instance might have it)
return result.lockedBy === instanceId;
} catch {
return false;
}
}
/**
* Release distributed lock and record result
*/
async function releaseLock(
result: "success" | "failed",
itemsProcessed?: number,
errorMessage?: string
) {
await prisma.syncStatus.update({
where: { jobName: JOB_NAME },
data: {
isRunning: false,
completedAt: new Date(),
lastResult: result,
itemsProcessed,
errorMessage,
lockedBy: null,
expiresAt: null,
},
});
}
export const start{PascalCaseName}Job = () => {
const schedule = process.env.{ENV_VAR_NAME} || "{$1}";
if (process.env.ENABLE_CRON_JOBS === "false") return;
cron.schedule(schedule, async () => {
if (isRunning) return;
isRunning = true;
const instanceId = randomUUID();
try {
const locked = await acquireLock(instanceId);
if (!locked) {
console.log(`[${JOB_NAME}] Another instance holds the lock, skipping`);
return;
}
// TODO: Implement job logic
// const result = await myServiceFunction(prisma);
await releaseLock("success", 0);
} catch (error: any) {
console.error(`[${JOB_NAME}] Failed:`, error.message);
await releaseLock("failed", undefined, error.message).catch(() => {});
} finally {
isRunning = false;
}
});
console.log(`[Cron] ${JOB_NAME} scheduled: ${schedule}`);
};
If the job should be triggerable via API, create a controller:
Create src/controllers/data/trigger{PascalCase}.ts:
import { Request, Response } from "express";
/**
* POST /data/trigger-{$0}
* Manually trigger the {display name} job
*/
export const postTrigger{PascalCase} = async (_req: Request, res: Response) => {
try {
// TODO: Call the service function directly
// const result = await myServiceFunction(prisma);
res.json({ success: true, message: "{Display name} completed" });
} catch (error: any) {
console.error("Manual {$0} trigger failed:", error.message);
res.status(500).json({
error: "Trigger failed",
message: error.message,
});
}
};
Then add to src/routes/data.route.ts:
import { postTrigger{PascalCase} } from "../controllers/data/trigger{PascalCase}";
router.post("/trigger-{$0}", postTrigger{PascalCase});
| Schedule | Expression | Use Case |
|---|---|---|
| Every 5 minutes | */5 * * * * | High-frequency data sync |
| Every 30 minutes | */30 * * * * | Medium-frequency updates |
| Every hour | 0 * * * * | Hourly aggregation |
| Every 6 hours | 0 */6 * * * | Low-frequency sync |
| Every 12 hours | 0 */12 * * * | Twice daily |
| Daily at midnight | 0 0 * * * | Daily cleanup/reports |
| Offset from other jobs | 30 */6 * * * | Avoid overlapping with other jobs (minute 30) |
src/jobs/{$0}.job.tsisRunning boolean) prevents overlapping runsENABLE_CRON_JOBS=false to disablecron.validate()src/jobs/index.ts via startAllJobs()finally block always resets isRunning = false/data/ routes