Motia Icon
Core Concepts

Steps

One primitive to build any backend. Simple, composable, and multi-language.

One Primitive for Any Backend

A Step is the core primitive in Motia. Instead of juggling separate frameworks for APIs, background jobs, queues, or workflows, you define everything in one place: how it runs, when it runs, where it runs, and what it does.

Every Step file contains two parts:

  • Config → defines when and how the Step runs, and gives it a unique name
  • Handler → the function that executes your business logic

Motia automatically discovers any file ending in .step.ts, .step.js, or _step.py from your src/ directory. The filename pattern tells Motia to load it, and the name in the config uniquely identifies the Step inside your system.

Flexible Organization - Steps can be placed anywhere within your src/ directory. Motia discovers them automatically regardless of how deeply nested they are.


The Simplest Example

src/hello.step.ts
import { type Handlers, type StepConfig } from 'motia'
import { z } from 'zod'
 
export const config = {
  name: 'HelloStep',
  description: 'Hello endpoint',
  triggers: [
    { type: 'http', path: '/hello', method: 'GET', responseSchema: { 200: z.object({ message: z.string() }) } },
  ],
  enqueues: [],
  flows: ['my-flow'],
} as const satisfies StepConfig
 
export const handler: Handlers<typeof config> = async (req, { logger }) => {
  logger.info('Hello endpoint called')
  return { status: 200, body: { message: 'Hello world!' } }
}

That's all you need to make a running API endpoint. Motia will auto-discover this file and wire it into your backend.


Steps Work Together: Enqueue + Queue

Steps aren't isolated. They communicate by enqueuing messages that other Steps listen for via queue triggers. This is the core of how you build backends with Motia.

Example Flow: API Step → Queue Step

src/send-message.step.ts
import { type Handlers, type StepConfig } from 'motia'
import { z } from 'zod'
 
export const config = {
  name: 'SendMessage',
  description: 'Sends a message',
  triggers: [
    { type: 'http', path: '/messages', method: 'POST' },
  ],
  enqueues: ['message.sent'],
  flows: ['messaging'],
} as const satisfies StepConfig
 
export const handler: Handlers<typeof config> = async (req, { enqueue }) => {
  await enqueue({
    topic: 'message.sent',
    data: { text: req.body.text }
  })
  return { status: 200, body: { ok: true } }
}
src/process-message.step.ts
import { type Handlers, type StepConfig } from 'motia'
import { z } from 'zod'
 
export const config = {
  name: 'ProcessMessage',
  description: 'Processes messages in background',
  triggers: [
    { type: 'queue', topic: 'message.sent', input: z.object({ text: z.string() }) },
  ],
  enqueues: ['message.processed'],
  flows: ['messaging'],
} as const satisfies StepConfig
 
export const handler: Handlers<typeof config> = async (input, { logger, enqueue }) => {
  logger.info('Processing message', input)
  await enqueue({ topic: 'message.processed', data: input })
}

With just two files, you have an API endpoint that triggers an event-driven workflow.


Triggers

Every Step has a triggers array that defines how it triggers:

TypeWhen it runsUse case
httpHTTP requestREST APIs, webhooks
queueMessage enqueuedBackground jobs, workflows
cronScheduleCleanup, reports, reminders
stateState changeReact to data changes
streamStream eventReal-time data processing

The iii development console lets you browse all registered triggers, test HTTP endpoints directly, and inspect their configuration:

Triggers view in the iii Console

HTTP Trigger

Runs when an HTTP request hits the path.

Example:

import { type Handlers, type StepConfig } from 'motia'
import { z } from 'zod'
 
export const config = {
  name: 'GetUser',
  description: 'Get user by ID',
  triggers: [
    { type: 'http', path: '/users/:id', method: 'GET' },
  ],
  enqueues: [],
  flows: ['users'],
} as const satisfies StepConfig
 
export const handler: Handlers<typeof config> = async (req, { logger }) => {
  const userId = req.pathParams.id
  logger.info('Getting user', { userId })
  return { status: 200, body: { id: userId, name: 'John' } }
}

Config:

PropertyDescription
nameUnique identifier
triggersArray with { type: 'http', path, method }
pathURL path (supports :params)
methodGET, POST, PUT, DELETE
bodySchemaValidate request body

Handler: handler(req, ctx)

  • req - Request with body, headers, pathParams, queryParams, rawBody
  • ctx - Context with logger, enqueue, state, streams, traceId, trigger, is, getData, match
  • Returns { status, body, headers? }

Context Object

Every handler receives a ctx object with these tools:

PropertyDescription
loggerStructured logging (info, warn, error)
enqueueTrigger other Steps by enqueuing messages
statePersistent key-value storage
streamsReal-time data channels for clients
traceIdUnique ID for tracing requests & workflows
triggerInfo about which trigger activated this handler
isType guards for trigger types (is.queue, is.http, is.cron)
getDataExtract data payload regardless of trigger type
matchPattern match on trigger type for multi-trigger steps

Core Functionality

State -- Persistent Data

Key-value storage shared across Steps and workflows.

const result = await state.set('settings', 'preferences', { theme: 'dark' })
const prefs = await state.get('settings', 'preferences')

state.set returns { new_value, old_value }. Use state.update for atomic updates with UpdateOp[].

Learn more about State Management

Logging -- Structured & Contextual

For debugging, monitoring, and observability.

logger.info('Processing user', { userId: '123' })

Learn more about Observability

Streams -- Real-Time Data

Push updates directly to connected clients.

await streams.chat.set('room-123', 'msg-456', { text: 'Hello!' })

Learn more about Streams

Flows -- Visualize in the iii Development Console

Group Steps together for diagram visualization in the iii development console.

Flow diagram in the iii Console

export const config = {
  name: 'CreateOrder',
  description: 'Creates a new order',
  triggers: [
    { type: 'http', path: '/orders', method: 'POST' },
  ],
  enqueues: [],
  flows: ['order-management'],
} as const satisfies StepConfig

Learn more about Flows

Infrastructure -- Configure Queue Steps

Customize timeout and retry behavior for Queue Steps.

export const config = {
  name: 'SendEmail',
  description: 'Send email with retries',
  triggers: [
    {
      type: 'queue',
      topic: 'email.requested',
      infrastructure: {
        handler: { timeout: 10 },
        queue: { maxRetries: 5, visibilityTimeout: 60 }
      }
    },
  ],
  enqueues: [],
  flows: ['email'],
} as const satisfies StepConfig

Learn more about Infrastructure


Multi-Trigger Steps

A single Step can respond to multiple trigger types. For example, a Step can be activated by both an HTTP request and a queue message:

export const config = {
  name: 'ProcessOrder',
  triggers: [
    { type: 'http', method: 'POST', path: '/orders/manual' },
    { type: 'queue', topic: 'order.created' },
    { type: 'cron', expression: '0 0 0 * * * *' },
  ],
  enqueues: ['order.processed'],
  flows: ['orders'],
} as const satisfies StepConfig

Use ctx.match() to handle each trigger type differently, or ctx.getData() to extract the data payload regardless of trigger type.

Learn more about Multi-Trigger Steps


Remember

  • Steps are just files. Export a config and handler.
  • Motia auto-discovers and connects them.
  • Combine Steps with enqueue + queue triggers to build APIs, workflows, background jobs, or entire systems.

What's Next?

Start building

Steps are all you need to know to start building. Go to the Quickstart and start building right away.

Explore examples