Skip to main content

Overview

Cron triggers enable scheduled, recurring task execution using standard cron expressions. Motia handles distributed locking automatically to ensure jobs run exactly once across multiple instances.

Basic Configuration

Define a cron trigger in your step config:
import type { Handlers, StepConfig } from 'motia'

export const config = {
  name: 'DailyReport',
  triggers: [
    {
      type: 'cron',
      expression: '0 0 9 * * * *', // Every day at 9:00 AM
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, ctx) => {
  ctx.logger.info('Generating daily report')
  
  const data = await ctx.state.list('analytics')
  const report = generateReport(data)
  
  await ctx.enqueue({
    topic: 'email.send',
    data: { report },
  })
}

Configuration Options

Required Fields

type
string
required
Must be "cron"
expression
string
required
Cron expression defining the schedule (see Cron Syntax)

Optional Fields

condition
function
Conditional function to determine if the handler should execute:
condition: (input, ctx) => {
  // Only run on weekdays
  const day = new Date().getDay()
  return day >= 1 && day <= 5
}

Handler Signature

Cron handlers receive an empty input and context:
type CronHandler = (
  input: {},
  ctx: HandlerContext
) => Promise<void>

Cron Syntax

Motia uses 7-field cron expressions:
┌───────────── second (0-59)
│ ┌───────────── minute (0-59)
│ │ ┌───────────── hour (0-23)
│ │ │ ┌───────────── day of month (1-31)
│ │ │ │ ┌───────────── month (1-12)
│ │ │ │ │ ┌───────────── day of week (0-6) (Sunday=0)
│ │ │ │ │ │ ┌───────────── year (optional)
│ │ │ │ │ │ │
* * * * * * *

Common Examples

Every minute:
expression: '0 * * * * * *'
Every 5 minutes:
expression: '0 */5 * * * * *'
Every hour at minute 30:
expression: '0 30 * * * * *'
Every day at 9:00 AM:
expression: '0 0 9 * * * *'
Every weekday at 8:00 AM:
expression: '0 0 8 * * 1-5 *'
First day of every month at midnight:
expression: '0 0 0 1 * * *'
Every Monday at 10:00 AM:
expression: '0 0 10 * * 1 *'
Every second (for testing):
expression: '* * * * * * *'
Every 10 seconds:
expression: '*/10 * * * * * *'

Special Characters

  • * - Any value
  • , - Value list separator (e.g., 1,3,5)
  • - - Range of values (e.g., 1-5)
  • / - Step values (e.g., */5 = every 5)

Distributed Locking

Motia automatically handles distributed locking to ensure cron jobs run exactly once, even when multiple instances are running:
export const config = {
  name: 'BillingJob',
  triggers: [
    {
      type: 'cron',
      expression: '0 0 0 1 * * *', // First of month
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, ctx) => {
  // Only one instance will execute this
  ctx.logger.info('Processing monthly billing')
  
  const users = await ctx.state.list('users')
  for (const user of users) {
    await processBilling(user)
  }
}
How it works:
  • Before execution, Motia attempts to acquire a distributed lock
  • Only the instance that acquires the lock executes the handler
  • Lock is automatically released after execution
  • Other instances skip execution silently

Conditional Execution

Use conditions to add runtime logic:
export const config = {
  name: 'BusinessHoursTask',
  triggers: [
    {
      type: 'cron',
      expression: '0 0 * * * * *', // Every hour
      condition: (input, ctx) => {
        const hour = new Date().getHours()
        // Only run during business hours (9 AM - 5 PM)
        return hour >= 9 && hour < 17
      },
    },
  ],
} as const satisfies StepConfig

State Auditing Pattern

Common pattern for checking and acting on state:
import type { Handlers, StepConfig } from 'motia'
import type { Order } from './types'

export const config = {
  name: 'StateAuditJob',
  triggers: [
    {
      type: 'cron',
      expression: '0 0/5 * * * * *', // Every 5 minutes
    },
  ],
  enqueues: ['notification'],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, ctx) => {
  const orders = await ctx.state.list<Order>('orders')
  
  for (const order of orders) {
    const currentDate = new Date()
    const shipDate = new Date(order.shipDate)
    
    if (!order.complete && currentDate > shipDate) {
      ctx.logger.warn('Order overdue', {
        orderId: order.id,
        shipDate: order.shipDate,
      })
      
      await ctx.enqueue({
        topic: 'notification',
        data: {
          email: order.email,
          templateId: 'order-overdue',
          templateData: {
            orderId: order.id,
            shipDate: order.shipDate,
          },
        },
      })
    }
  }
}

Multi-Trigger with Cron

Combine cron with other trigger types:
export const config = {
  name: 'ProcessOrders',
  triggers: [
    {
      type: 'queue',
      topic: 'order.created',
      condition: (input) => input.amount > 1000,
    },
    {
      type: 'http',
      method: 'POST',
      path: '/orders/manual',
    },
    {
      type: 'cron',
      expression: '* * * * *', // Every minute
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, ctx) => {
  return ctx.match({
    http: async ({ request }) => {
      // Handle manual API request
      return { status: 200, body: { processed: true } }
    },
    queue: async (queueInput) => {
      // Handle high-value orders from queue
    },
    cron: async () => {
      // Process pending orders batch
      const pending = await ctx.state.list('pending-orders')
      
      for (const order of pending) {
        await ctx.enqueue({
          topic: 'order.processed',
          data: { orderId: order.id },
        })
      }
      
      ctx.logger.info('Batch complete', {
        count: pending.length,
      })
    },
  })
}

Module Configuration

Configure the cron module in motia.config.json:
{
  "modules": {
    "cron": {
      "adapter": {
        "type": "redis",
        "config": {
          "url": "redis://localhost:6379"
        }
      }
    }
  }
}

Supported Adapters

  • kv - Local key-value store (development only)
  • redis - Redis-backed distributed locking (production)
Use the redis adapter in production to ensure distributed locking works across multiple instances.

Common Patterns

Data Cleanup

export const config = {
  name: 'CleanupExpiredSessions',
  triggers: [
    {
      type: 'cron',
      expression: '0 0 * * * * *', // Every hour
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, ctx) => {
  const sessions = await ctx.state.list('sessions')
  const now = Date.now()
  
  for (const session of sessions) {
    if (session.expiresAt < now) {
      await ctx.state.delete('sessions', session.id)
      ctx.logger.info('Deleted expired session', { id: session.id })
    }
  }
}

Report Generation

export const config = {
  name: 'WeeklyReport',
  triggers: [
    {
      type: 'cron',
      expression: '0 0 9 * * 1 *', // Mondays at 9 AM
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, ctx) => {
  const metrics = await gatherWeeklyMetrics()
  
  await ctx.enqueue({
    topic: 'email.send',
    data: {
      to: 'team@company.com',
      subject: 'Weekly Report',
      body: formatReport(metrics),
    },
  })
}

Health Checks

export const config = {
  name: 'HealthCheck',
  triggers: [
    {
      type: 'cron',
      expression: '0 */1 * * * * *', // Every minute
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, ctx) => {
  const health = await checkSystemHealth()
  
  if (!health.ok) {
    await ctx.enqueue({
      topic: 'alert.send',
      data: { message: 'System unhealthy', details: health },
    })
  }
}
Test cron expressions using every second (* * * * * * *) during development, then adjust to production schedule.
Cron handlers should be idempotent - safe to run multiple times with the same result. While distributed locking prevents concurrent execution, network issues or crashes could cause re-execution.

Build docs developers (and LLMs) love