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
Step configuration object defining the step’s behavior Human-readable description of what the step does
Array of trigger configurations that invoke this step. See Triggers Topics this step can enqueue to. Each can be a string or object with topic, label, and conditional properties
Flow names this step belongs to for organization and filtering
Additional files to include in the deployment bundle
Handler function that processes trigger inputs. If omitted, returns a builder with a .handle() method
Return type
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 } ` },
})
}
)