Skip to main content

Overview

HTTP triggers allow your steps to respond to HTTP requests. Define REST API endpoints with full control over methods, paths, request validation, and response formats.

Basic Configuration

Define an HTTP trigger in your step config:
import type { Handlers, StepConfig } from 'motia'
import { z } from 'zod'

export const config = {
  name: 'CreatePet',
  triggers: [
    {
      type: 'http',
      method: 'POST',
      path: '/pets',
      bodySchema: z.object({
        name: z.string(),
        photoUrl: z.string(),
      }),
      responseSchema: {
        200: z.object({
          id: z.string(),
          name: z.string(),
          createdAt: z.string(),
        }),
      },
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (request, ctx) => {
  const { name, photoUrl } = request.body
  
  const pet = await ctx.state.set('pets', crypto.randomUUID(), {
    name,
    photoUrl,
    createdAt: new Date().toISOString(),
  })

  return {
    status: 200,
    body: pet,
  }
}

Configuration Options

Required Fields

type
string
required
Must be "http"
method
string
required
HTTP method: GET, POST, PUT, PATCH, DELETE
path
string
required
URL path for the endpoint (e.g., /pets, /orders/:id)

Optional Fields

bodySchema
ZodSchema
Zod schema for request body validation. Request will be rejected if validation fails.
responseSchema
object
Map of status codes to Zod schemas for response validation:
responseSchema: {
  200: z.object({ success: z.boolean() }),
  404: z.object({ error: z.string() }),
}
condition
function
Conditional function to determine if the handler should execute:
condition: (input, ctx) => {
  return input.body.verified === true
}

Handler Signature

HTTP handlers receive the request object and context:
type HttpHandler = (
  request: {
    body: T,
    query: Record<string, string>,
    params: Record<string, string>,
    headers: Record<string, string>,
    requestBody: { stream: AsyncIterable<Uint8Array> },
  },
  ctx: HandlerContext
) => Promise<{
  status: number
  body?: any
  headers?: Record<string, string>
}>

Response Formats

JSON Response

Return a JSON object:
return {
  status: 200,
  body: { message: 'Success', orderId: '123' },
}

Custom Headers

Include custom response headers:
return {
  status: 201,
  headers: { 'X-Request-ID': ctx.traceId },
  body: { created: true },
}

Server-Sent Events (SSE)

Stream events to the client:
export const handler: Handlers<typeof config> = async ({ request, response }, ctx) => {
  response.status(200)
  response.headers({
    'content-type': 'text/event-stream',
    'cache-control': 'no-cache',
    'connection': 'keep-alive',
  })

  for (const item of items) {
    response.stream.write(`event: item\ndata: ${JSON.stringify(item)}\n\n`)
    await sleep(1000)
  }

  response.stream.write(`event: done\ndata: {}\n\n`)
  response.close()
}

Path Parameters

Define dynamic route segments:
{
  type: 'http',
  method: 'GET',
  path: '/pets/:petId',
}
Access parameters in handler:
export const handler: Handlers<typeof config> = async (request, ctx) => {
  const { petId } = request.params
  const pet = await ctx.state.get('pets', petId)
  
  if (!pet) {
    return { status: 404, body: { error: 'Pet not found' } }
  }
  
  return { status: 200, body: pet }
}

Query Parameters

Access query string parameters:
export const handler: Handlers<typeof config> = async (request, ctx) => {
  const { limit, offset } = request.query
  const pets = await ctx.state.list('pets')
  
  return {
    status: 200,
    body: pets.slice(Number(offset) || 0, Number(limit) || 10),
  }
}

Request Body Validation

Use Zod schemas for automatic validation:
import { z } from 'zod'

{
  type: 'http',
  method: 'POST',
  path: '/orders',
  bodySchema: z.object({
    petId: z.string().uuid(),
    quantity: z.number().int().positive(),
    email: z.string().email(),
  }),
}
Invalid requests receive automatic 400 responses.

Array Schemas

Handle arrays in request and response:
import { jsonSchema } from 'motia'
import { z } from 'zod'

{
  type: 'http',
  method: 'POST',
  path: '/batch',
  bodySchema: jsonSchema(
    z.array(z.object({
      name: z.string(),
      value: z.number(),
    }))
  ),
  responseSchema: {
    200: jsonSchema(z.array(z.object({
      id: z.string(),
      processed: z.boolean(),
    }))),
  },
}

Multi-Trigger Example

Combine HTTP with other trigger types:
export const config = {
  name: 'ProcessOrder',
  triggers: [
    {
      type: 'http',
      method: 'POST',
      path: '/orders/manual',
      bodySchema: z.object({
        amount: z.number(),
        description: z.string(),
      }),
      condition: (input) => input.body.amount > 100,
    },
    {
      type: 'queue',
      topic: 'order.created',
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, ctx) => {
  return ctx.match({
    http: async ({ request }) => {
      // Handle HTTP-specific logic
      return { status: 200, body: { orderId: '123' } }
    },
    queue: async (queueInput) => {
      // Handle queue-specific logic
    },
  })
}

Module Configuration

Configure the REST API module in motia.config.json:
{
  "modules": {
    "rest_api": {
      "port": 3111,
      "host": "0.0.0.0",
      "default_timeout": 30000,
      "concurrency_request_limit": 1024,
      "cors": {
        "allowed_origins": ["*"],
        "allowed_methods": ["GET", "POST", "PUT", "DELETE"]
      }
    }
  }
}

Module Options

port
number
default:"3111"
Port for the HTTP server
host
string
default:"0.0.0.0"
Host address to bind to
default_timeout
number
default:"30000"
Default request timeout in milliseconds
concurrency_request_limit
number
default:"1024"
Maximum concurrent requests
cors
object
CORS configuration with allowed_origins and allowed_methods arrays

Common Patterns

Enqueue Background Tasks

export const handler: Handlers<typeof config> = async (request, ctx) => {
  await ctx.enqueue({
    topic: 'process-order',
    data: { orderId: '123' },
  })
  
  return {
    status: 202,
    body: { message: 'Processing started' },
  }
}

Update State

export const handler: Handlers<typeof config> = async (request, ctx) => {
  await ctx.state.set('orders', request.body.id, {
    ...request.body,
    status: 'pending',
  })
  
  return { status: 200, body: { success: true } }
}

Error Handling

export const handler: Handlers<typeof config> = async (request, ctx) => {
  try {
    const result = await processOrder(request.body)
    return { status: 200, body: result }
  } catch (error) {
    ctx.logger.error('Order processing failed', { error })
    return {
      status: 500,
      body: { error: 'Internal server error' },
    }
  }
}
HTTP triggers run on port 3111 by default. Your endpoints are available at http://localhost:3111{path}

Build docs developers (and LLMs) love