.claude/skills/scheduled-actions/SKILL.md
Scheduled Actions system for background task processing in this application. Covers action scheduling, handler creation, webhook configuration, and cron processing. Use this skill when creating, debugging, or configuring scheduled actions.
npx skillsauth add NextSpark-js/nextspark scheduled-actionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns for background task processing and webhook systems.
SCHEDULED ACTIONS SYSTEM:
Core Layer (core/lib/scheduled-actions/):
├── scheduler.ts # scheduleAction(), scheduleRecurringAction()
├── processor.ts # Cron processing logic
├── registry.ts # Handler registration
└── types.ts # TypeScript interfaces
Theme Layer (contents/themes/{theme}/lib/scheduled-actions/):
├── index.ts # Handler initialization + registerAllHandlers()
├── entity-hooks.ts # Entity event → action mapping
└── handlers/ # Handler implementations
├── webhook.ts # Webhook sender
├── email.ts # Email sender (if configured)
└── {custom}.ts # Custom handlers
Configuration (contents/themes/{theme}/config/app.config.ts):
└── scheduledActions: {
enabled: true,
deduplication: { windowSeconds: 10 },
webhooks: { endpoints: {...}, patterns: {...} }
}
Flow:
Entity Event → Entity Hook → scheduleAction() → DB Table → Cron → Handler → Result
📍 Context-Aware Paths: Core layer (
core/lib/scheduled-actions/) is read-only in consumer projects. Create handlers incontents/themes/{theme}/lib/scheduled-actions/handlers/. Seecore-theme-responsibilitiesskill for complete rules.
CRITICAL: Initialization happens in instrumentation.ts at server startup.
Server Start (instrumentation.ts)
│
├─ initializeScheduledActions() # Sync - registers handlers
│ └─ Calls theme's registerAllHandlers()
│ ├─ Register action handlers (e.g., content:publish)
│ └─ Register entity hooks (e.g., on entity.contents.updated)
│
└─ initializeRecurringActions() # Async - creates DB rows (once per server)
└─ Calls theme's registerRecurringActions()
└─ Creates recurring action rows in DB if not exist
(e.g., social:refresh-tokens every 30 minutes)
Then...
Cron Endpoint (/api/v1/cron/process) - Called every ~1 minute
│
└─ processPendingActions() # Async - executes due actions
└─ Processes actions where scheduledAt <= now
// instrumentation.ts (root of project)
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const {
initializeScheduledActions,
initializeRecurringActions,
} = await import('@nextsparkjs/core/lib/scheduled-actions')
console.log('[Instrumentation] Initializing scheduled actions...')
// 1. Register handlers + entity hooks (sync, idempotent)
initializeScheduledActions()
// 2. Create recurring actions in DB (async, idempotent)
await initializeRecurringActions()
console.log('[Instrumentation] ✅ Scheduled actions initialized')
}
}
// app/api/v1/cron/process/route.ts
import {
processPendingActions,
cleanupOldActions
} from '@nextsparkjs/core/lib/scheduled-actions'
export async function GET(request: NextRequest): Promise<NextResponse> {
// Handlers already registered via instrumentation.ts
// 1. Validate CRON_SECRET...
// 2. Process pending actions...
// 3. Cleanup old actions...
}
// contents/themes/{theme}/lib/scheduled-actions/index.ts
// Called by initializeScheduledActions() - registers handlers
export function registerAllHandlers() {
registerContentPublishHandler() // One-time actions
registerTokenRefreshHandler() // Handler for recurring action
registerEntityHooks() // Entity event → action mapping
}
// Called by initializeRecurringActions() - creates DB rows
export async function registerRecurringActions(): Promise<void> {
// Check if already exists to avoid duplicates
const existing = await queryWithRLS(
`SELECT id FROM "scheduled_actions" WHERE "actionType" = $1 AND "recurringInterval" IS NOT NULL`,
['social:refresh-tokens'],
null
)
if (existing.length === 0) {
await scheduleRecurringAction('social:refresh-tokens', {}, 'every-30-minutes')
}
}
| Type | Trigger | Created By | Example |
|------|---------|------------|---------|
| One-time | Entity event | Entity hooks | content:publish when content.status='scheduled' |
| Recurring | Cron interval | registerRecurringActions() | social:refresh-tokens every 30 min |
✅ ALWAYS use
instrumentation.tsfor scheduled actions initialization. This is the correct place because:
- Runs ONCE at server startup (not on every cron request)
- Entity hooks are registered before any API requests
- Official Next.js 13+ pattern for global initialization
- Idempotent functions handle edge cases safely
| File | Purpose |
|------|---------|
| core/lib/scheduled-actions/scheduler.ts | scheduleAction(), scheduleRecurringAction() |
| core/lib/scheduled-actions/processor.ts | Cron processing logic |
| core/lib/scheduled-actions/registry.ts | Handler registration |
| contents/themes/{theme}/lib/scheduled-actions/index.ts | Handler initialization |
| contents/themes/{theme}/lib/scheduled-actions/handlers/ | Handler implementations |
| contents/themes/{theme}/config/app.config.ts | Configuration section |
// contents/themes/{theme}/lib/scheduled-actions/handlers/{name}.ts
import { registerScheduledAction } from '@/core/lib/scheduled-actions'
interface MyPayload {
entityId: string
teamId: string
data: Record<string, unknown>
}
export function registerMyHandler() {
registerScheduledAction('my-action:type', async (payload, action) => {
const data = payload as MyPayload
try {
// Implementation logic
await processMyAction(data)
return {
success: true,
message: 'Action completed successfully'
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error'
}
}
})
}
// contents/themes/{theme}/lib/scheduled-actions/index.ts
import { registerMyHandler } from './handlers/my-handler'
let initialized = false
export function registerAllHandlers() {
if (initialized) return
initialized = true
// Register all handlers
registerWebhookHandler()
registerMyHandler() // Add new handler here
}
| Type | Description | Use Case |
|------|-------------|----------|
| webhook | Send HTTP POST to external endpoint | Integrations, notifications |
| email | Send transactional emails | User notifications |
| data-processor | Process/transform data | ETL, aggregations |
| cleanup | Clean up old records | Maintenance tasks |
// contents/themes/{theme}/lib/scheduled-actions/entity-hooks.ts
import { scheduleAction } from '@/core/lib/scheduled-actions'
import { hookSystem } from '@/core/lib/hooks'
export function registerEntityHooks() {
// Hook for task creation
hookSystem.register('entity.tasks.created', async ({ entity, teamId }) => {
await scheduleAction({
type: 'webhook:send',
payload: {
endpointKey: 'tasks',
event: 'task.created',
data: entity
},
scheduledFor: new Date(),
teamId
})
})
// Hook for task updates
hookSystem.register('entity.tasks.updated', async ({ entity, teamId }) => {
await scheduleAction({
type: 'webhook:send',
payload: {
endpointKey: 'tasks',
event: 'task.updated',
data: entity
},
scheduledFor: new Date(),
teamId
})
})
}
// contents/themes/{theme}/config/app.config.ts
export const appConfig = {
// ... other config
scheduledActions: {
enabled: true,
deduplication: {
windowSeconds: 10 // 0 to disable
},
webhooks: {
endpoints: {
// Key -> Environment variable mapping
tasks: 'WEBHOOK_URL_TASKS',
subscriptions: 'WEBHOOK_URL_SUBSCRIPTIONS',
default: 'WEBHOOK_URL_DEFAULT'
},
patterns: {
// Event pattern -> Endpoint key
'task.*': 'tasks',
'subscription.*': 'subscriptions',
'*': 'default' // Fallback
}
}
}
}
# Required for cron processing
CRON_SECRET=your-secure-secret-min-32-chars
# Webhook URLs (one per endpoint key)
WEBHOOK_URL_TASKS=https://your-webhook-url/tasks
WEBHOOK_URL_SUBSCRIPTIONS=https://your-webhook-url/subs
WEBHOOK_URL_DEFAULT=https://fallback-url
import { scheduleAction } from '@/core/lib/scheduled-actions'
await scheduleAction({
type: 'my-action:type',
payload: {
entityId: 'abc123',
data: { field: 'value' }
},
scheduledFor: new Date(), // Now
teamId: 'team_123'
})
await scheduleAction({
type: 'reminder:send',
payload: { userId: 'user_123', message: 'Follow up' },
scheduledFor: new Date(Date.now() + 24 * 60 * 60 * 1000), // Tomorrow
teamId: 'team_123'
})
IMPORTANT: Recurring actions should be created in registerRecurringActions(), NOT ad-hoc.
// In theme's lib/scheduled-actions/index.ts
export async function registerRecurringActions(): Promise<void> {
const { scheduleRecurringAction } = await import('@nextsparkjs/core/lib/scheduled-actions')
// Check if already exists (idempotent)
const existing = await queryWithRLS(
`SELECT id FROM "scheduled_actions" WHERE "actionType" = $1 AND "recurringInterval" IS NOT NULL AND status = 'pending'`,
['report:generate'],
null
)
if (existing.length > 0) return
// Available intervals: 'every-minute', 'every-5-minutes', 'every-15-minutes',
// 'every-30-minutes', 'every-hour', 'every-6-hours', 'every-day'
await scheduleRecurringAction(
'report:generate',
{ reportType: 'daily-summary' },
'every-day'
)
}
Flow: After processing a recurring action, it auto-reschedules for the next interval.
For recurring actions, you can control how the next execution time is calculated using the recurrenceType parameter.
Maintains exact schedule times, ignoring execution delays. Ideal for reports, batch jobs, or any task that should run at specific times.
await scheduleRecurringAction(
'report:daily',
{ type: 'sales' },
'daily',
{
scheduledAt: new Date('2026-02-05T12:00:00Z'),
recurrenceType: 'fixed' // or omit, defaults to 'fixed'
}
)
Behavior:
Calculates intervals from actual completion time. Ideal for token refreshes, polling, or tasks where consistent spacing matters more than exact timing.
await scheduleRecurringAction(
'social:refresh-tokens',
{},
'every-30-minutes',
{
recurrenceType: 'rolling'
}
)
Behavior:
| Scenario | Fixed | Rolling | |----------|-------|---------| | Daily report at 9:00 AM | ✅ Runs at 9:00 daily (or as soon as possible) | ❌ Drifts if delayed | | Token refresh every 30 min | ❌ Can stack up if delayed | ✅ Consistent 30 min gaps | | Batch cleanup at midnight | ✅ Runs at midnight sharp | ❌ Drifts based on execution time | | API polling every 5 min | ⚠️ Can create bursts if delayed | ✅ Steady 5 min spacing |
// Example 1: Fixed - Daily backup at 2:00 AM
await scheduleRecurringAction(
'backup:database',
{ retention: 30 },
'daily',
{
scheduledAt: new Date('2026-02-05T02:00:00Z'),
recurrenceType: 'fixed'
}
)
// Example 2: Rolling - Check external API every 15 minutes
await scheduleRecurringAction(
'external:sync',
{ endpoint: 'https://api.example.com' },
'every-15-minutes',
{
recurrenceType: 'rolling'
}
)
// Example 3: Fixed - Weekly report on Mondays at 8:00 AM
await scheduleRecurringAction(
'report:weekly',
{ format: 'pdf' },
'weekly',
{
scheduledAt: new Date('2026-02-10T08:00:00Z'), // Monday
recurrenceType: 'fixed'
}
)
curl "http://localhost:5173/api/v1/devtools/scheduled-actions?status=pending" \
-H "Authorization: Bearer API_KEY"
curl "http://localhost:5173/api/v1/devtools/scheduled-actions?status=failed" \
-H "Authorization: Bearer API_KEY"
curl "http://localhost:5173/api/v1/cron/process" \
-H "x-cron-secret: CRON_SECRET"
[ScheduledActions] Processing 5 pending actions
[ScheduledActions] Action abc123 completed successfully
[ScheduledActions] Action xyz789 failed: Connection timeout
[ScheduledActions] Handler not found for type: unknown:type
Prevents duplicate actions when the same event fires multiple times in quick succession.
scheduledActions: {
deduplication: {
windowSeconds: 10 // Actions with same type+payload within 10s are deduplicated
}
}
type + payloadSet windowSeconds: 0 to disable deduplication entirely.
| Endpoint | Method | Auth | Purpose |
|----------|--------|------|---------|
| /api/v1/cron/process | POST | x-cron-secret | Trigger action processing |
| /api/v1/devtools/scheduled-actions | GET | API Key | List actions (debug) |
| /api/v1/devtools/scheduled-actions/:id | DELETE | API Key | Delete action (debug) |
| Issue | Cause | Solution |
|-------|-------|----------|
| Handler not found | Not registered | Add to index.ts registerAllHandlers() |
| Webhook not sent | Pattern mismatch | Check patterns in app.config.ts |
| Duplicate actions | Dedup disabled | Set windowSeconds > 0 |
| Actions stuck pending | Cron not running | Verify cron service and CRON_SECRET |
| 401 on cron endpoint | Wrong header | Use x-cron-secret (not Authorization) |
| Env variable undefined | Not set | Add to .env and restart server |
| Recurring action not created | Missing registerRecurringActions() | Export function from theme's index.ts |
| Recurring action not running | Handlers not initialized | Ensure instrumentation.ts exists and runs |
| Entity hooks not firing | Handlers not registered early | Use instrumentation.ts, not cron endpoint |
// NEVER: Process actions synchronously in API routes
// This blocks the response
app.post('/api/entity', async (req, res) => {
const entity = await createEntity(req.body)
await sendWebhook(entity) // WRONG - blocks response
res.json(entity)
})
// CORRECT: Schedule action for async processing
app.post('/api/entity', async (req, res) => {
const entity = await createEntity(req.body)
await scheduleAction({
type: 'webhook:send',
payload: { entity },
scheduledFor: new Date(),
teamId: req.teamId
})
res.json(entity)
})
// NEVER: Store sensitive data in payload
await scheduleAction({
type: 'email:send',
payload: {
password: 'secret123' // WRONG - stored in DB
}
})
// CORRECT: Store references only
await scheduleAction({
type: 'email:send',
payload: {
userId: 'user_123', // Lookup at processing time
templateKey: 'password-reset'
}
})
// NEVER: Forget to handle errors in handlers
registerScheduledAction('my:action', async (payload) => {
await riskyOperation(payload) // WRONG - unhandled rejection
})
// CORRECT: Always return success/failure
registerScheduledAction('my:action', async (payload) => {
try {
await riskyOperation(payload)
return { success: true, message: 'Done' }
} catch (error) {
return { success: false, message: error.message }
}
})
// NEVER: Initialize in API routes (adds overhead to every request)
// /api/v1/cron/process/route.ts
export async function GET(request: NextRequest) {
initializeScheduledActions() // WRONG - runs on every cron call
await initializeRecurringActions() // WRONG - unnecessary DB queries
// ...process actions
}
// CORRECT: Initialize in instrumentation.ts (runs once at startup)
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const {
initializeScheduledActions,
initializeRecurringActions,
} = await import('@nextsparkjs/core/lib/scheduled-actions')
initializeScheduledActions() // ✅ Registers handlers + hooks
await initializeRecurringActions() // ✅ Creates recurring actions in DB
}
}
handlers/ directoryindex.ts registerAllHandlers(){ success, message } objectnode core/scripts/build/registry.mjsentity-hooks.tswebhooks.endpoints configwebhooks.patterns config.env.env.examplescheduledActions.enabled: true in configCRON_SECRET is set[ScheduledActions] logsentity-api - API endpoints that trigger entity hooksservice-layer - Service patterns for action processingnextjs-api-development - Cron endpoint patternsdatabase-migrations - scheduled_actions table structureFull documentation: core/docs/20-scheduled-actions/
01-overview.md - System overview02-scheduling.md - Scheduling patterns03-handlers.md - Handler development04-webhooks.md - Webhook configuration05-cron.md - Cron processing06-deduplication.md - Deduplication systemdevelopment
Zod validation patterns for this Next.js application. Covers schema definition, API validation, form integration, error formatting, and type inference. Use this skill when implementing validation for APIs, forms, or entity schemas.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
testing
Test coverage metrics and registry system for this Next.js application. Covers FEATURE_REGISTRY, FLOW_REGISTRY, TAGS_REGISTRY, and coverage metrics interpretation. Use this skill when evaluating test coverage, identifying gaps, or planning testing priorities.
development
TanStack Query (React Query) patterns for data fetching in this Next.js application. Covers useQuery, useMutation, optimistic updates, cache invalidation, and anti-patterns. Use this skill when implementing data fetching or state management with server data.