← Back to Blog
Tutorial

Automating Your App with Scheduled Tasks

Jul 6, 2026·7 min read

Apps that do things on their own

The best apps aren't just reactive — they do things proactively. Every night the database should clean up expired sessions. Every morning a digest email should go out to users. Every hour prices should sync from an external API. Without scheduled tasks, you end up manually triggering these things or forgetting about them entirely.

Spinini's Scheduled Tasks panel builds cron scheduling directly into your project. No separate cron server, no external service, no configuration files.

Setting up a scheduled task

Click the Clock icon in the sidebar to open the Scheduled Tasks panel.

Click New task and fill in:

  • Name — a human-readable label ("Nightly cleanup", "Hourly price sync")
  • Schedule — a cron expression (see below)
  • Trigger — the URL in your app to call, or a terminal command to run

Click Save. The task is active immediately.

Understanding cron expressions

Cron expressions are five fields: minute, hour, day-of-month, month, day-of-week.

┌─── minute (0-59)
│ ┌─── hour (0-23)
│ │ ┌─── day of month (1-31)
│ │ │ ┌─── month (1-12)
│ │ │ │ ┌─── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *

Common examples:

ExpressionMeaning
`0 9 * * *`Every day at 9:00 AM
`0 * * * *`Every hour on the hour
`*/15 * * * *`Every 15 minutes
`0 9 * * 1`Every Monday at 9 AM
`0 0 1 * *`First day of every month at midnight

The panel has a helper UI that builds the expression for you if you prefer not to write cron syntax manually.

Common use cases

Database cleanup

Remove expired sessions, old notifications, stale temporary records:

// routes/tasks/cleanup.js
export async function GET() {
  const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // 7 days ago

  const { count } = await prisma.session.deleteMany({
    where: { expiresAt: { lt: cutoff } }
  })

  console.log(`Deleted ${count} expired sessions`)
  return Response.json({ deleted: count })
}

Set the task to trigger GET /tasks/cleanup every night at 2 AM: 0 2 * * *

Email digest

Send a weekly summary email to all active users:

// routes/tasks/weekly-digest.js
export async function POST() {
  const users = await prisma.user.findMany({
    where: { emailNotifications: true },
    include: { recentActivity: { take: 5 } }
  })

  for (const user of users) {
    await sendEmail({
      to: user.email,
      subject: 'Your weekly summary',
      html: renderDigestTemplate(user)
    })
  }

  return Response.json({ sent: users.length })
}

Schedule: 0 9 * * 1 (Monday 9 AM)

Price sync from external API

// routes/tasks/sync-prices.js
export async function GET() {
  const prices = await fetch('https://api.yourprovider.com/prices')
    .then(r => r.json())

  for (const [id, price] of Object.entries(prices)) {
    await prisma.product.update({
      where: { externalId: id },
      data: { price: price.usd }
    })
  }

  return Response.json({ updated: Object.keys(prices).length })
}

Schedule: 0 * * * * (every hour)

Report generation

# scripts/generate_report.py
import json
from datetime import datetime, timedelta
from db import query

yesterday = datetime.now() - timedelta(days=1)

metrics = {
    'new_signups': query('SELECT COUNT(*) FROM users WHERE created_at > %s', [yesterday]),
    'active_users': query('SELECT COUNT(DISTINCT user_id) FROM events WHERE ts > %s', [yesterday]),
    'revenue': query('SELECT SUM(amount) FROM payments WHERE created_at > %s', [yesterday])
}

print(json.dumps(metrics))

Set the task as a command: python scripts/generate_report.py >> /var/log/reports.log 2>&1

Schedule: 0 6 * * * (6 AM daily)

Securing your task endpoints

If your task endpoints modify data, protect them from being called by anyone on the internet:

export async function GET(request) {
  const token = request.headers.get('x-task-token')
  if (token !== process.env.TASK_SECRET) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // ... do the work
}

Set TASK_SECRET in your project's Secrets panel. Configure the same value in the Scheduled Tasks panel's "Headers" field for that task.

Monitoring and logs

The Scheduled Tasks panel shows each task's:

  • Last run time — when it was last triggered
  • Last result — the HTTP status code or exit code
  • Output — the last 500 lines of stdout/stderr from the task

If a task fails, the output shows the error so you can debug it directly without guessing.

Ready to start building?

Free account includes 100 AI credits/month. No credit card required.

Start for free →