Motia Icon

Background Jobs

Learn how to create async background jobs and scheduled tasks with Motia

What You'll Build

A pet management system with background jobs that handle:

  • Event Step - Async job that sets feeding reminders when pets are created
  • Cron Step - Scheduled job that runs daily to clean up deleted pets

workbench

Getting Started

Clone the example repository:

git clone https://github.com/MotiaDev/build-your-first-app.git
cd build-your-first-app
git checkout background-jobs

Install dependencies:

npm install

Start the Workbench:

npm run dev

Your Workbench will be available at http://localhost:3000.


Project Structure

package.json
requirements.txt
types.d.ts

Files like features.json and tutorial/tutorial.tsx are only for the interactive tutorial and are not part of Motia's project structure.

All code examples in this guide are available in the build-your-first-app repository.

You can follow this guide to learn how to build background jobs with Motia step by step, or you can clone the repository and dive into our Interactive Tutorial to learn by doing directly in the Workbench.

interactive-tutorial


Understanding Background Jobs

Background jobs let you handle time-consuming tasks without blocking your API responses. When a user creates a pet, they get an immediate response while tasks like sending emails or processing data happen in the background.

Motia provides two types of background jobs:

  • Event Steps - Triggered by events from your API endpoints
  • Cron Steps - Run on a schedule (like daily cleanup tasks)

Creating Your First Event Step

Let's create a background job that sets feeding reminders when a pet is created. First, we need to emit an event from our API endpoint.

Step 1: Emit Events from API

View on GitHub:

steps/typescript/create-pet.step.ts
import { ApiRouteConfig, Handlers } from 'motia'
import { z } from 'zod'
import { TSStore } from './ts-store'
 
const createPetSchema = z.object({
  name: z.string().min(1, 'Name is required').trim(),
  species: z.enum(['dog', 'cat', 'bird', 'other']),
  ageMonths: z.number().int().min(0, 'Age must be a positive number')
})
 
export const config: ApiRouteConfig = {
  type: 'api',
  name: 'TsCreatePet',
  path: '/ts/pets',
  method: 'POST',
  // Declare what events this endpoint can emit
  emits: ['ts.feeding.reminder.enqueued'],
  flows: ['TsPetManagement'],
  bodySchema: createPetSchema
}
 
export const handler: Handlers['TsCreatePet'] = async (req, { emit, logger }) => {
  try {
    const validatedData = createPetSchema.parse(req.body)
    
    const pet = TSStore.create({ 
      name: validatedData.name, 
      species: validatedData.species, 
      ageMonths: validatedData.ageMonths
    })
    
    if (logger) {
      logger.info('🐾 Pet created', { 
        petId: pet.id, 
        name: pet.name, 
        species: pet.species, 
        status: pet.status 
      })
    }
    
    // Emit event to trigger background job
    if (emit) {
      await emit({
        topic: 'ts.feeding.reminder.enqueued',
        data: {
          petId: pet.id,
          enqueuedAt: Date.now()
        }
      })
    }
 
    return { status: 201, body: pet }
  } catch (error) {
    if (error instanceof z.ZodError) {
      return { 
        status: 400, 
        body: { 
          message: 'Validation error', 
          errors: error.errors 
        } 
      }
    }
    
    if (logger) {
      logger.error('❌ Pet creation failed', { 
        error: error instanceof Error ? error.message : 'Unknown error' 
      })
    }
    
    return { 
      status: 500, 
      body: { message: 'Internal server error' } 
    }
  }
}

The API endpoint now emits an event after creating a pet. The response returns immediately while the background job processes asynchronously.


Step 2: Create the Event Step

Now let's create the background job that listens for this event and sets feeding reminders.

View on GitHub:

steps/typescript/set-next-feeding-reminder.job.step.ts
import { EventConfig, Handlers } from 'motia'
import { TSStore } from './ts-store'
 
export const config = {
  type: 'event',
  name: 'TsSetNextFeedingReminder',
  description: 'Background job that sets next feeding reminder and adds welcome notes',
  // Subscribe to the event emitted by CreatePet
  subscribes: ['ts.feeding.reminder.enqueued'],
  emits: [],
  flows: ['TsPetManagement']
}
 
export const handler: Handlers['TsSetNextFeedingReminder'] = async (input, { emit, logger }) => {
  const { petId, enqueuedAt } = input
 
  if (logger) {
    logger.info('🔄 Setting next feeding reminder', { petId, enqueuedAt })
  }
 
  try {
    // Calculate next feeding time (24 hours from now)
    const nextFeedingAt = Date.now() + (24 * 60 * 60 * 1000)
    
    // Fill in non-critical details
    const updates = {
      notes: 'Welcome to our pet store! We\'ll take great care of this pet.',
      nextFeedingAt: nextFeedingAt
    }
 
    const updatedPet = TSStore.update(petId, updates)
    
    if (!updatedPet) {
      if (logger) {
        logger.error('❌ Failed to set feeding reminder - pet not found', { petId })
      }
      return
    }
 
    if (logger) {
      logger.info('✅ Next feeding reminder set', { 
        petId, 
        notes: updatedPet.notes?.substring(0, 50) + '...',
        nextFeedingAt: new Date(nextFeedingAt).toISOString()
      })
    }
 
    // Feeding reminder scheduled successfully
 
  } catch (error: any) {
    if (logger) {
      logger.error('❌ Feeding reminder job error', { petId, error: error.message })
    }
  }
}

How Event Steps Work

Event Steps have a few key differences from API Steps:

  • type is set to 'event' instead of 'api'
  • subscribes lists the events this job listens for
  • handler receives the event data as the first argument

When you create a pet, the API returns immediately. The background job picks up the event and processes it asynchronously.


Testing Your Background Job

Create a pet and watch the background job execute:

# Create a pet
curl -X POST http://localhost:3000/pets \
  -H "Content-Type: application/json" \
  -d '{"name": "Max", "species": "dog", "ageMonths": 24}'

Check the logs in Workbench to see both the API call and the background job execution:

background-job-logs

You'll see:

  1. "Pet created" log from the API endpoint
  2. "Setting next feeding reminder" log from the background job
  3. "Next feeding reminder set" log when the job completes

Creating a Scheduled Cron Job

Now let's create a cron job that runs daily to clean up soft-deleted pets. This demonstrates how to handle scheduled maintenance tasks.

View on GitHub:

steps/typescript/deletion-reaper.cron.step.ts
import { CronConfig, Handlers } from 'motia'
import { TSStore } from './ts-store'
 
export const config: CronConfig = {
  type: 'cron',
  name: 'TsDeletionReaper',
  description: 'Daily job that permanently removes pets scheduled for deletion',
  cron: '0 2 * * *', // Daily at 2:00 AM
  emits: [],
  flows: ['TsPetManagement']
}
 
export const handler: Handlers['TsDeletionReaper'] = async ({ emit, logger }) => {
  if (logger) {
    logger.info('🔄 Deletion Reaper started - scanning for pets to purge')
  }
 
  try {
    const petsToReap = TSStore.findDeletedPetsReadyToPurge()
    
    if (petsToReap.length === 0) {
      if (logger) {
        logger.info('✅ Deletion Reaper completed - no pets to purge')
      }
      
      // No emit - no subscribers for ts.reaper.completed
      return
    }
 
    let purgedCount = 0
    
    for (const pet of petsToReap) {
      const success = TSStore.remove(pet.id)
      
      if (success) {
        purgedCount++
        
        if (logger) {
          logger.info('💀 Pet permanently purged', { 
            petId: pet.id, 
            name: pet.name,
            deletedAt: new Date(pet.deletedAt!).toISOString(),
            purgeAt: new Date(pet.purgeAt!).toISOString()
          })
        }
 
        // No emit - no subscribers for ts.pet.purged
      } else {
        if (logger) {
          logger.warn('⚠️ Failed to purge pet', { petId: pet.id, name: pet.name })
        }
      }
    }
 
    if (logger) {
      logger.info('✅ Deletion Reaper completed', { 
        totalScanned: petsToReap.length,
        purgedCount,
        failedCount: petsToReap.length - purgedCount
      })
    }
 
    // No emit - no subscribers for ts.reaper.completed
 
  } catch (error: any) {
    if (logger) {
      logger.error('❌ Deletion Reaper error', { error: error.message })
    }
  }
}

Understanding Cron Steps

Cron Steps run on a schedule defined by a cron expression:

  • type is set to 'cron'
  • cron defines when the job runs (e.g., '0 2 * * *' = daily at 2 AM)
  • handler receives only the context (no input data like Event Steps)

Common cron patterns:

  • '*/5 * * * *' - Every 5 minutes
  • '0 * * * *' - Every hour
  • '0 0 * * *' - Daily at midnight
  • '0 9 * * 1' - Every Monday at 9 AM

Monitoring Background Jobs

Workbench provides tools to monitor your background jobs:

Tracing

See the complete execution flow from API call to background job:

tracing

Each trace shows:

  • When the API endpoint was called
  • When events were emitted
  • When background jobs started and completed
  • Total processing time

🎉 Congratulations! You've successfully created background jobs with Motia. Your pet store now handles async tasks efficiently without blocking API responses.


What's Next?

You now have a complete backend system with API endpoints and background jobs! But there's more power in Motia when you combine everything into workflows.

In the next guide, we'll build complete workflow orchestrations that connect multiple Steps together:

  • Queue-Based Job Processing - SetNextFeedingReminder triggered by pet creation, processing asynchronously without blocking API responses
  • Scheduled Maintenance Tasks - Deletion Reaper running daily at 2 AM to permanently remove soft-deleted pets past their purge date
  • Pet Lifecycle Orchestration - Staff-driven workflow managing pet status transitions from creation through quarantine, health checks, and adoption
  • Event-Driven State Management - Centralized orchestrator ensuring consistent pet status changes with automatic progressions and staff decision points

Let's continue building by creating workflows that orchestrate your APIs and background jobs into powerful, event-driven systems.

Need help? See our Community Resources for questions, examples, and discussions.