Cron Jobs in .temps.yaml

Define a cron schedule in .temps.yaml and Temps runs it on every environment, automatically. No external scheduler, no third-party service, and no extra configuration — Temps reads the file after each deployment and begins firing HTTP requests to your endpoint on schedule. Schedules are evaluated in UTC.

For why scheduled work in containers is better done as an HTTP call than as crontab inside the image, see Cron jobs in production containers. This page covers how Temps does it.


Configuration file

Cron jobs are defined in a .temps.yaml file in your project directory.

File location

The .temps.yaml file should be placed in your project's root directory. For most projects, this is the repository root:

your-repo/
  ├── .temps.yaml          ← Place here (repository root)
  ├── src/
  ├── package.json
  └── ...

For monorepos, if your project is in a subdirectory, place .temps.yaml in that subdirectory:

monorepo/
  ├── apps/
  │   └── my-app/
  │       ├── .temps.yaml  ← Place here (project directory)
  │       ├── src/
  │       └── package.json
  └── packages/

The file location matches your project's configured directory in Temps.

Basic Format

cron:
  - path: "/api/cleanup"
    schedule: "0 * * * *"
    name: "Hourly Cleanup"  # Optional: descriptive name

Multiple Cron Jobs

cron:
  - path: "/api/cleanup"
    schedule: "0 * * * *"
    name: "Hourly Cleanup"

  - path: "/api/backup"
    schedule: "0 2 * * *"
    name: "Daily Backup"

  - path: "/api/digest"
    schedule: "0 9 * * *"
    name: "Daily Email Digest"

Full Configuration Example

# .temps.yaml
cron:
  - path: "/api/cleanup"
    schedule: "0 * * * *"
    name: "Hourly Cleanup"

  - path: "/api/backup"
    schedule: "0 2 * * *"
    name: "Daily Backup at 02:00 UTC"

  - path: "/api/weekly-report"
    schedule: "0 9 * * MON"
    name: "Weekly Report on Mondays"

# Other configuration options
build:
  dockerfile: Dockerfile

env:
  NODE_ENV: production

How It Works

  1. Add .temps.yaml to your project directory (repository root or subdirectory for monorepos)
  2. Commit and push to Git
  3. Deploy your application
  4. After deployment completes, Temps reads the .temps.yaml file
  5. Cron jobs are configured automatically for that environment
  6. Jobs run on the specified schedule

Timing: Cron jobs are configured after your deployment is live. The configuration job runs automatically as part of the deployment workflow.


Quick start

Schedule a cron job

  1. 1

    Create a .temps.yaml file in your project's root directory (or the project's subdirectory in a monorepo).

  2. 2

    Add a cron entry with path: "/api/cleanup", schedule: "0 * * * *", and an optional name: "Hourly Cleanup".

  3. 3

    Create the endpoint that will be invoked (e.g. app/api/cleanup/route.ts) and validate the Authorization: Bearer ${CRON_SECRET} header inside it.

  4. 4

    Commit and push .temps.yaml to Git, then deploy your application.

  5. 5

    After the deployment goes live, Temps reads .temps.yaml and configures the cron job for that environment automatically.

    Checkpoint: Open Project Settings then Cron Jobs and confirm the new job is listed with its schedule before relying on it.

Step 1: Create .temps.yaml file

Create a .temps.yaml file in your repository root:

cron:
  - path: "/api/cleanup"
    schedule: "0 * * * *"
    name: "Hourly Cleanup"

Step 2: Deploy your application

When you deploy, Temps automatically:

  • Reads your .temps.yaml file
  • Configures the cron jobs
  • Starts scheduling them

Step 3: Create your endpoint

Create the endpoint that will be invoked:

// app/api/cleanup/route.ts (Next.js)
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  // Verify cron secret (recommended)
  const authHeader = request.headers.get('authorization');
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Perform cleanup task
  await cleanupOldRecords();
  await archiveExpiredSessions();

  return NextResponse.json({
    success: true,
    message: 'Cleanup completed',
    timestamp: new Date().toISOString()
  });
}

Your cron job will run automatically on the schedule you defined.


Cron expression syntax

Temps uses the 5-field cron format, and schedules are evaluated in UTC: 0 2 * * * runs at 02:00 UTC, whatever the server's or your own timezone. Convert local times to UTC when you write a schedule.

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (SUN - SAT, or 1 - 7 where 1 = Sunday)
│ │ │ │ │
│ │ │ │ │
* * * * *

Use day names for the day-of-week field. The scheduler numbers weekdays 1–7 starting with Sunday, not the 0–6 numbering many cron references use. MON, MON-FRI or SAT,SUN mean the same thing everywhere; a numeric 1 means Sunday, and 0 is rejected as invalid.

Schedules must be at least one minute apart.

Common Schedules

  • Name
    Every 5 minutes
    Description
    schedule: "*/5 * * * *"
    
  • Name
    Every hour at minute 0
    Description
    schedule: "0 * * * *"
    
  • Name
    Daily at 02:30 UTC
    Description
    schedule: "30 2 * * *"
    
  • Name
    Weekly on Monday at 09:00 UTC
    Description
    schedule: "0 9 * * MON"
    
  • Name
    Monthly on the 1st at 00:00 UTC
    Description
    schedule: "0 0 1 * *"
    
  • Name
    Every weekday at 08:00 UTC
    Description
    schedule: "0 8 * * MON-FRI"
    
  • Name
    Twice daily (06:00 and 18:00 UTC)
    Description
    schedule: "0 6,18 * * *"
    

Use crontab.guru to check the minute, hour, day and month fields. It uses 0–6 for weekdays, so write the day-of-week field with names (MON-FRI) to get the same result in Temps.


Endpoint invocation

HTTP Method

Temps calls your endpoint with a GET request and an X-Cron-Job: true header.

Authentication

Temps automatically handles cron authentication for you:

  1. A CRON_SECRET environment variable is injected into your container on every deployment
  2. The cron scheduler sends an Authorization header when invoking your endpoints:
Authorization: Bearer <CRON_SECRET>

You don't need to configure anything. The secret is generated per project/environment and stays stable across redeployments.

Verify in your endpoint:

Verify Cron Secret

// app/api/cleanup/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  // Verify cron secret
  const authHeader = request.headers.get('authorization');
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Perform cleanup task
  await cleanupOldRecords();

  return NextResponse.json({ success: true });
}

Security: Always validate the Authorization header in your cron endpoints to prevent unauthorized invocations.


Per-environment scheduling

Cron jobs defined in .temps.yaml are configured per environment when you deploy. This means:

  • Same .temps.yaml file - The file is shared across all environments
  • Environment-specific execution - Each environment (dev, staging, production) runs its own cron jobs
  • Isolated execution - Production and staging crons don't interfere with each other
  • Configured after deployment - Cron jobs are set up automatically after each deployment completes

Example: Different Behavior Per Environment

While the schedule is the same, you can make your endpoint behave differently based on environment:

// app/api/backup/route.ts
export async function GET(request: Request) {
  const env = process.env.NODE_ENV;

  if (env === 'production') {
    // Production: Full backup every 6 hours
    await performFullBackup();
  } else {
    // Staging: Lightweight backup daily
    await performLightBackup();
  }

  return Response.json({ success: true });
}
# .temps.yaml
cron:
  - path: "/api/backup"
    schedule: "0 */6 * * *"  # Every 6 hours (production will do full backup)
    name: "Database Backup"

Common use cases

1. Database Backups

Schedule regular database backups:

# .temps.yaml
cron:
  - path: "/api/backup"
    schedule: "0 2 * * *"  # Daily at 02:00 UTC
    name: "Database Backup"

If the database is a Temps-managed service, scheduled backups to S3 already cover it; a cron endpoint like this is for data Temps doesn't back up.

// app/api/backup/route.ts
export async function GET(request: Request) {
  // Verify auth...

  const { S3Client, PutObjectCommand } = await import('@aws-sdk/client-s3');

  // Dump database
  const dump = await dumpDatabase();

  // Upload to S3
  const s3 = new S3Client({ /* config */ });
  await s3.send(new PutObjectCommand({
    Bucket: 'backups',
    Key: `database-${Date.now()}.sql`,
    Body: dump
  }));

  return Response.json({ success: true });
}

2. Data Cleanup

Remove old records, expired sessions, or temporary files:

# .temps.yaml
cron:
  - path: "/api/cleanup"
    schedule: "0 3 * * *"  # Daily at 03:00 UTC
    name: "Daily Cleanup"
// app/api/cleanup/route.ts
export async function GET(request: Request) {
  // Verify auth...

  const sevenDaysAgo = new Date();
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

  // Delete expired sessions
  await db.session.deleteMany({
    where: { expiresAt: { lt: sevenDaysAgo } }
  });

  return Response.json({ success: true });
}

3. Email Digests

Send daily/weekly email summaries:

# .temps.yaml
cron:
  - path: "/api/digest"
    schedule: "0 9 * * *"  # Daily at 09:00 UTC
    name: "Daily Email Digest"

4. Cache Warming

Pre-populate caches before peak traffic:

# .temps.yaml
cron:
  - path: "/api/warm-cache"
    schedule: "0 6 * * *"  # Daily at 06:00 UTC
    name: "Cache Warming"

5. API Sync

Synchronize data from external APIs:

# .temps.yaml
cron:
  - path: "/api/sync"
    schedule: "*/15 * * * *"  # Every 15 minutes
    name: "External API Sync"

Monitoring and logs

View Cron Jobs

In the Temps dashboard:

  1. Go to Project Settings → Cron Jobs
  2. See all configured cron jobs
  3. Open a job to see its execution history and which runs failed

Or via the CLI:

# List all cron jobs for an environment
bunx @temps-sdk/cli environments crons list -p my-app -e production

Execution Details

Each execution records:

  • Executed at - When the request was sent
  • URL - The endpoint that was called
  • HTTP status - Response code from the endpoint (any non-2xx counts as a failure)
  • Response time - How long the endpoint took to answer
  • Response headers - Headers your endpoint returned
  • Error message - If the job failed

The response body is not stored, so log anything you need to keep from inside the endpoint.

You can also inspect a specific cron job or its execution history from the CLI:

# Show details for a specific cron job
bunx @temps-sdk/cli environments crons show -p my-app -e production --id <cron_id>

# Show execution history for a specific cron job
bunx @temps-sdk/cli environments crons executions -p my-app -e production --id <cron_id>

View Logs

Check your application logs to see cron job execution:

# View deployment logs
bunx @temps-sdk/cli runtime-logs --project my-app --environment production

# Follow logs in real time
bunx @temps-sdk/cli runtime-logs --project my-app --environment production --follow

# Filter for cron-related logs
bunx @temps-sdk/cli runtime-logs --project my-app --environment production | grep "cron"

Error handling

Retries

Cron jobs are not automatically retried. Each scheduled tick runs the endpoint exactly once — if that request fails, the failure is recorded, Temps raises a cron-failure alert through your configured notification channels (see Monitoring), and the job simply waits for its next scheduled run. There is no built-in backoff or retry attempt in between.

If you need retry behavior, implement it inside your endpoint (or have it enqueue work onto a background job queue that has its own retry policy), rather than relying on Temps to re-invoke it.

Handling Failures in Code

Return appropriate status codes so failures are recorded accurately and surfaced in notifications:

// app/api/cron/task/route.ts
export async function GET(request: Request) {
  try {
    await performTask();
    return Response.json({ success: true }, { status: 200 });
  } catch (error) {
    if (error instanceof TransientError) {
      // Recorded as a failure and reported; not automatically retried
      return Response.json({ error: error.message }, { status: 500 });
    } else {
      // Also recorded as a failure and reported
      return Response.json({ error: error.message }, { status: 400 });
    }
  }
}

No automatic retries: the status code you return only affects how the execution is recorded and reported — it does not trigger a retry. A non-2xx response of any kind (4xx or 5xx) is recorded as a failed execution.

Timeout

Temps does not currently enforce an execution timeout on cron requests. A hung or slow endpoint can run for as long as the underlying connection stays open — it is not automatically terminated after any fixed duration.

Because there's no platform-enforced cutoff, it's important to bound your own endpoint's runtime:

  • Set your own timeout/AbortController inside the handler and fail fast on slow dependencies
  • Break long-running tasks into smaller jobs
  • Use background job queues for heavy processing
  • Process data in batches

Best practices

  • Name
    Idempotency
    Description

    Design cron jobs to be idempotent—running them multiple times should produce the same result:

    // ✅ Good: Idempotent
    await db.session.deleteMany({
      where: { expiresAt: { lt: sevenDaysAgo } }
    });
    
    // ❌ Bad: Not idempotent
    await db.user.updateMany({
      data: { credits: { increment: 10 } }
    });
    
  • Name
    Use Locking
    Description

    Prevent concurrent executions with distributed locks:

    import { redis } from '@/lib/redis';
    
    const lockKey = 'cron:backup:lock';
    const acquired = await redis.set(lockKey, '1', 'EX', 3600, 'NX');
    
    if (!acquired) {
      return Response.json({ error: 'Already running' }, { status: 409 });
    }
    
    try {
      await performBackup();
    } finally {
      await redis.del(lockKey);
    }
    
  • Name
    Monitor Duration
    Description

    Temps does not enforce a timeout or alert you when a job runs long, so track duration yourself and log if it exceeds what you expect:

    const start = Date.now();
    await performTask();
    const duration = Date.now() - start;
    
    if (duration > 5 * 60 * 1000) { // adjust to your own expected duration
      console.warn(`Job took ${duration}ms`);
    }
    
  • Name
    Test Locally
    Description

    Test cron endpoints locally before deploying:

    # Set a local CRON_SECRET for testing
    export CRON_SECRET=test-secret
    
    # Invoke endpoint manually
    curl http://localhost:3000/api/cleanup \
      -H "Authorization: Bearer test-secret"
    

    In production, CRON_SECRET is auto-injected by Temps. You only need to set it manually for local testing.


Troubleshooting

  • Name
    Cron not executing
    Description

    Possible causes:

    • Invalid cron expression (use crontab.guru)
    • .temps.yaml file not in correct location (should match project directory)
    • Endpoint path doesn't match
    • Environment not deployed
    • YAML syntax error in .temps.yaml

    Solution:

    • Verify .temps.yaml is in your project directory (check project settings for directory path)
    • Check YAML syntax is valid
    • Check cron syntax with crontab.guru
    • Ensure your endpoint path matches exactly (including leading slash)
    • Redeploy your application (cron configuration happens after deployment)
    • Check deployment logs for cron configuration messages
  • Name
    401 Unauthorized errors
    Description

    Cause: Your endpoint is not reading the CRON_SECRET environment variable correctly.

    Solution:

    • CRON_SECRET is automatically injected by Temps on every deployment. You don't need to set it manually.
    • Verify your endpoint reads process.env.CRON_SECRET (Node.js) or os.environ['CRON_SECRET'] (Python)
    • Check that your auth check compares against Bearer ${process.env.CRON_SECRET}
    • Redeploy your application if the secret was not injected (older deployments before this feature may not have it)
  • Name
    Job hangs or runs longer than expected
    Description

    Cause: Temps does not enforce an execution timeout on cron requests, so a slow dependency or hung request can keep running indefinitely instead of failing fast.

    Solution:

    • Add your own timeout/AbortController inside the endpoint
    • Break long-running tasks into smaller jobs
    • Use background job queues for heavy processing
    • Process data in batches
  • Name
    Jobs running multiple times
    Description

    Cause: Multiple environments deployed with same schedule.

    Solution: This is expected behavior—each environment runs its own cron jobs. Use locking (see Best Practices) if you need to prevent concurrent execution.

Last updated

Was this page helpful?