Steps are the fundamental building blocks in Motia. Each step represents a unit of work that can be triggered by various events, execute business logic, and interact with other steps through queues.
What is a step?
A step consists of two parts:
Configuration : Defines the step’s name, triggers, and output queues
Handler : The async function that executes when the step is triggered
Creating a step
Motia provides two syntaxes for defining steps:
TypeScript
JavaScript
Python
import { step , http } from 'motia'
// Inline syntax
export default step ({
name: 'process-order' ,
triggers: [ http ( 'POST' , '/orders' )] ,
enqueues: [ 'order-confirmation' ]
}, async ( input , ctx ) => {
// Handler logic
return { status: 200 , body: { success: true } }
} )
// Builder syntax
export default step ({
name: 'process-order' ,
triggers: [ http ( 'POST' , '/orders' )] ,
enqueues: [ 'order-confirmation' ]
})
. handle ( async ( input , ctx ) => {
return { status: 200 , body: { success: true } }
})
import { step , http } from 'motia'
// Inline syntax
export default step ({
name: 'process-order' ,
triggers: [ http ( 'POST' , '/orders' )] ,
enqueues: [ 'order-confirmation' ]
}, async ( input , ctx ) => {
// Handler logic
return { status: 200 , body: { success: true } }
} )
// Builder syntax
export default step ({
name: 'process-order' ,
triggers: [ http ( 'POST' , '/orders' )] ,
enqueues: [ 'order-confirmation' ]
})
. handle ( async ( input , ctx ) => {
return { status: 200 , body: { success: true } }
})
from motia import step, http
# Inline syntax
default = step({
'name' : 'process-order' ,
'triggers' : [http( 'POST' , '/orders' )],
'enqueues' : [ 'order-confirmation' ]
}, async def handler( input , ctx):
# Handler logic
return { 'status' : 200 , 'body' : { 'success' : True }}
)
# Builder syntax
default = step({
'name' : 'process-order' ,
'triggers' : [http( 'POST' , '/orders' )],
'enqueues' : [ 'order-confirmation' ]
}).handle( async def handler( input , ctx):
return { 'status' : 200 , 'body' : { 'success' : True }}
)
Step configuration
The step configuration object defines the step’s behavior and connections:
Required fields
Unique identifier for the step
Array of triggers that can invoke this step. See Triggers for details.
Optional fields
Human-readable description of what the step does
Queue topics this step can publish to. Use with ctx.enqueue()
Flow names this step belongs to, for logical grouping
Additional files to bundle with the step deployment
Configure handler resources and queue behavior:
handler.ram: Memory in MB (default: 128)
handler.cpu: CPU units (optional)
handler.timeout: Timeout in seconds (default: 30)
queue.type: “fifo” or “standard” (default: “standard”)
queue.maxRetries: Retry attempts (default: 3)
queue.visibilityTimeout: Visibility timeout in seconds (default: 30)
Step handler signature
Handlers receive two parameters:
type StepHandler < TInput , TEnqueueData > = (
input : TriggerInput < TInput >,
ctx : FlowContext < TEnqueueData , TriggerInput < TInput >>
) => Promise < ApiResponse | void >
from typing import Awaitable, Callable, Any
StepHandler = Callable[[Any, FlowContext], Awaitable[Any]]
The input type depends on the trigger:
HTTP triggers : MotiaHttpArgs with request/response objects
Queue triggers : The queue message data
Cron triggers : undefined (no input)
State triggers : StateTriggerInput with old/new values
Stream triggers : StreamTriggerInput with event data
Context parameter
The ctx parameter provides access to Motia’s runtime features. See FlowContext for full details.
Multi-trigger steps
Steps can respond to multiple trigger types:
import { step , http , queue , cron } from 'motia'
import { z } from 'zod'
const schema = z . object ({
orderId: z . string (),
status: z . string ()
})
export default step ({
name: 'update-order' ,
triggers: [
http ( 'POST' , '/orders/:id' , { bodySchema: schema }),
queue ( 'order-updates' , { input: schema }),
cron ( '0 * * * *' ) // Hourly check
]
}, async ( input , ctx ) => {
return ctx . match ({
http : async ( req ) => {
const data = req . request . body
await updateOrder ( data )
return { status: 200 , body: { updated: true } }
},
queue : async ( data ) => {
await updateOrder ( data )
},
cron : async () => {
await checkPendingOrders ()
}
})
} )
async function updateOrder ( data : z . infer < typeof schema >) {
// Shared logic for HTTP and queue
}
from motia import step, http, queue, cron
default = step({
'name' : 'update-order' ,
'triggers' : [
http( 'POST' , '/orders/ {id} ' ),
queue( 'order-updates' ),
cron( '0 * * * *' ) # Hourly check
]
}, async def handler( input , ctx):
return await ctx.match({
'http' : lambda req : update_via_http(req),
'queue' : lambda data : update_via_queue(data),
'cron' : lambda : check_pending_orders()
})
)
async def update_via_http ( req ):
data = req.request.body
await update_order(data)
return { 'status' : 200 , 'body' : { 'updated' : True }}
async def update_via_queue ( data ):
await update_order(data)
async def check_pending_orders ():
# Periodic check logic
pass
Use ctx.match() to handle different trigger types with type-safe branching. See Handler patterns for more details.
Type inference
Motia automatically infers types from your configuration:
import { step , http , queue } from 'motia'
import { z } from 'zod'
const orderSchema = z . object ({
id: z . string (),
amount: z . number ()
})
export default step ({
name: 'process-payment' ,
triggers: [
http ( 'POST' , '/payments' , { bodySchema: orderSchema }),
queue ( 'payment-queue' , { input: orderSchema })
] ,
enqueues: [ 'payment-confirmed' ]
}, async ( input , ctx ) => {
// input is typed as: MotiaHttpArgs<Order> | Order
// ctx.enqueue is typed to accept: { topic: 'payment-confirmed', data: unknown }
const data = ctx . getData () // Extracts Order from both trigger types
await ctx . enqueue ({
topic: 'payment-confirmed' ,
data: { orderId: data . id , success: true }
})
} )
Step lifecycle
Trigger fires : HTTP request, queue message, cron schedule, state change, or stream event
Input validation : Schema validation if defined in trigger config
Handler execution : Your async handler function runs
Response handling :
HTTP: Return ApiResponse object
Queue/Cron: Return nothing or throw error for retry
State/Stream: Return any value
Error handling : Errors trigger retries based on infrastructure config
Best practices
Single responsibility Keep each step focused on one task. Use multiple steps connected by queues for complex workflows.
Idempotency Design handlers to be safely retryable. Use idempotency keys for external API calls.
Type safety Define schemas for inputs and outputs to catch errors at build time.
Error handling Let transient errors throw for automatic retry. Handle permanent failures explicitly.
Next steps
Triggers Learn about the five trigger types
Context API Explore FlowContext capabilities
Handlers Handler patterns and best practices
State management Working with persistent state