Skip to main content

step()

The step() function is the primary way to define workflow steps in Motia. It provides type-safe configuration and handler definition with full TypeScript inference.

Signature

function step<TConfig extends StepConfig>(
  config: TConfig,
  handler: Handlers<TConfig>
): StepDefinition<TConfig>

function step<TConfig extends StepConfig>(
  config: TConfig
): StepBuilder<TConfig>

Parameters

config
StepConfig
required
Step configuration object defining the step’s behavior
handler
Handlers<TConfig>
Handler function that processes trigger inputs. If omitted, returns a builder with a .handle() method

Return type

config
TConfig
The step configuration
handler
Handlers<TConfig>
The handler function

Usage

Direct definition

Define a step with config and handler in one call:
import { step, queue } from 'motia'
import { z } from 'zod'

const orderSchema = z.object({
  email: z.string(),
  quantity: z.number(),
  petId: z.string(),
})

export const { config, handler } = step(
  {
    name: 'ProcessFoodOrder',
    description: 'Process incoming food orders',
    triggers: [queue('process-food-order', { input: orderSchema })],
    enqueues: ['notification'],
  },
  async (input, ctx) => {
    ctx.logger.info('Processing order', { input })
    
    const order = await createOrder(input)
    await ctx.state.set('orders', order.id, order)
    
    await ctx.enqueue({
      topic: 'notification',
      data: { email: input.email, orderId: order.id },
    })
  }
)

Builder pattern

Separate config from handler using the builder:
import { step, http } from 'motia'
import { z } from 'zod'

const stepConfig = {
  name: 'CreatePet',
  triggers: [
    http('POST', '/pets', {
      bodySchema: z.object({
        name: z.string(),
        photoUrl: z.string(),
      }),
      responseSchema: {
        200: z.object({ id: z.string(), name: z.string() }),
      },
    }),
  ],
}

export const { config, handler } = step(stepConfig).handle(async (request, ctx) => {
  const pet = await createPet(request.body)
  
  return {
    status: 200,
    body: pet,
  }
})

Multiple triggers

Steps can respond to multiple trigger types:
import { step, http, queue } from 'motia'
import { z } from 'zod'

const dataSchema = z.object({
  userId: z.string(),
  action: z.string(),
})

export const { config, handler } = step(
  {
    name: 'ProcessAction',
    triggers: [
      queue('user-actions', { input: dataSchema }),
      http('POST', '/actions', { bodySchema: dataSchema }),
    ],
  },
  async (input, ctx) => {
    // Use getData() to get the payload regardless of trigger type
    const data = ctx.getData()
    
    await processAction(data)
    
    // Return response only for HTTP triggers
    return ctx.match({
      http: async () => ({ status: 200, body: { success: true } }),
    })
  }
)

Cron triggers

Scheduled tasks using cron expressions:
import { step, cron } from 'motia'

export const { config, handler } = step(
  {
    name: 'DailyCleanup',
    description: 'Clean up old records daily',
    triggers: [cron('0 0 * * *')], // Every day at midnight
  },
  async (_input, ctx) => {
    ctx.logger.info('Running daily cleanup')
    await cleanupOldRecords()
  }
)

State triggers

React to state changes:
import { step, state } from 'motia'
import type { StateTriggerInput } from 'motia'

export const { config, handler } = step(
  {
    name: 'OnOrderUpdate',
    triggers: [
      state((input: StateTriggerInput<Order>) => {
        return input.group_id === 'orders' && input.new_value?.status === 'shipped'
      }),
    ],
  },
  async (input, ctx) => {
    ctx.logger.info('Order shipped', {
      orderId: input.item_id,
      newValue: input.new_value,
    })
  }
)

Type inference

The step() function provides full TypeScript inference for:
  • Handler input types based on trigger schemas
  • Context enqueue data types based on configured topics
  • Response types for HTTP triggers
  • Trigger-specific type guards with ctx.is and ctx.match
// Input type is inferred from bodySchema
const { config, handler } = step(
  {
    name: 'Example',
    triggers: [
      http('POST', '/example', {
        bodySchema: z.object({ name: z.string() }),
      }),
    ],
    enqueues: ['notifications'],
  },
  async (request, ctx) => {
    // TypeScript knows request.body has { name: string }
    const name = request.body.name
    
    // TypeScript knows enqueue requires notifications topic
    await ctx.enqueue({
      topic: 'notifications', // type-checked
      data: { message: `Hello ${name}` },
    })
  }
)

Build docs developers (and LLMs) love