Skip to main content
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:
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 } }
})

Step configuration

The step configuration object defines the step’s behavior and connections:

Required fields

name
string
required
Unique identifier for the step
triggers
TriggerConfig[]
required
Array of triggers that can invoke this step. See Triggers for details.

Optional fields

description
string
Human-readable description of what the step does
enqueues
string[]
Queue topics this step can publish to. Use with ctx.enqueue()
flows
string[]
Flow names this step belongs to, for logical grouping
includeFiles
string[]
Additional files to bundle with the step deployment
infrastructure
InfrastructureConfig
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>

Input parameter

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
}
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

  1. Trigger fires: HTTP request, queue message, cron schedule, state change, or stream event
  2. Input validation: Schema validation if defined in trigger config
  3. Handler execution: Your async handler function runs
  4. Response handling:
    • HTTP: Return ApiResponse object
    • Queue/Cron: Return nothing or throw error for retry
    • State/Stream: Return any value
  5. 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

Build docs developers (and LLMs) love