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
- Add
.temps.yamlto your project directory (repository root or subdirectory for monorepos) - Commit and push to Git
- Deploy your application
- After deployment completes, Temps reads the
.temps.yamlfile - Cron jobs are configured automatically for that environment
- 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
Create a .temps.yaml file in your project's root directory (or the project's subdirectory in a monorepo).
- 2
Add a cron entry with path: "/api/cleanup", schedule: "0 * * * *", and an optional name: "Hourly Cleanup".
- 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
Commit and push .temps.yaml to Git, then deploy your application.
- 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.yamlfile - 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:
- A
CRON_SECRETenvironment variable is injected into your container on every deployment - The cron scheduler sends an
Authorizationheader 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.yamlfile - 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:
- Go to Project Settings → Cron Jobs
- See all configured cron jobs
- 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/
AbortControllerinside 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_SECRETis 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.yamlfile 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.yamlis 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_SECRETenvironment variable correctly.Solution:
CRON_SECRETis 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) oros.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/
AbortControllerinside the endpoint - Break long-running tasks into smaller jobs
- Use background job queues for heavy processing
- Process data in batches
- Add your own timeout/
- 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.