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
Cron expression defining the schedule (see Cron Syntax)
Optional Fields
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.