Runlane
Recipes

Scheduled tasks

Generate and email a weekday analytics report on a cron schedule.

Runlane schedules are defined beside a task. Maintenance turns each due occurrence into a normal queued run.

Send a weekday report

The Runlane schedule and steps below are real. The marked metric, analysis, and email calls are pseudocode for your application and providers:

src/tasks/daily-report.ts
import { task } from '@runlane/core'
import * as z from 'zod'

const metricsSchema = z.object({ values: z.array(z.object({ name: z.string(), value: z.number() })) })
const reportSchema = z.object({
  anomalies: z.array(z.string()),
  insights: z.array(z.string()),
  recommendations: z.array(z.string()),
})
const dailyReportResultSchema = z.object({ insights: z.number().int(), sent: z.boolean() })

export const dailyReport = task({
  id: 'reports.daily',
  schema: z.object({ accountId: z.string() }),
  output: dailyReportResultSchema,
  schedule: {
    id: 'reports.weekdays.account-123',
    cron: '0 9 * * 1-5',
    payload: { accountId: 'account-123' },
    timeZone: 'America/New_York',
  },
  async run({ accountId }, context) {
    const metrics = await context.step.run('load-metrics', { output: metricsSchema }, async () => {
      //////////////////////////////////
      // MAKE YOUR API CALLS HERE
      //////////////////////////////////
      const result = await loadPreviousDayMetrics({ accountId, signal: context.signal })

      return metricsSchema.parse(result)
    })

    const report = await context.step.run('analyze-metrics', { output: reportSchema }, async () => {
      //////////////////////////////////
      // MAKE YOUR API CALLS HERE
      //////////////////////////////////
      const result = await analyzeMetrics({ metrics, signal: context.signal })

      return reportSchema.parse(result)
    })

    await context.step.run('send-report', { output: z.object({ sent: z.literal(true) }) }, async ({ token }) => {
      //////////////////////////////////
      // MAKE YOUR API CALLS HERE
      //////////////////////////////////
      await sendDailyReport({ idempotencyKey: token, metrics, report, signal: context.signal })

      return { sent: true }
    })

    return { insights: report.insights.length, sent: true }
  },
})

This schedule runs at 9:00 AM on weekdays in America/New_York. The task loads the previous day’s metrics, generates structured findings, sends the report once through a durable step, and returns a small result.

A schedule accepts exactly one cadence:

  • runAt for one occurrence;
  • every for a fixed interval;
  • cron for cron occurrences.

Know the boundary

Schedules are static task registration, not a dynamic CRUD service. For customer-created schedules, store them in your application and use an application scheduler to call trigger().

Maintenance creates the run; a worker executes it. Running only one of those processes is not enough.

Before production

  • Give maintenance a clear deployment owner.
  • Use stable schedule and provider idempotency keys.
  • Test a due occurrence with a controlled clock.
  • Confirm overlapping maintenance calls do not create duplicate occurrences.
  • Monitor schedules that are due but not materialized.

Read the complete schedule tasks guide and maintenance guide.

On this page