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
HTTP method: GET, POST, PUT, PATCH, DELETE
URL path for the endpoint (e.g., /pets, /orders/:id)
Optional Fields
Zod schema for request body validation. Request will be rejected if validation fails.
Map of status codes to Zod schemas for response validation:responseSchema: {
200: z.object({ success: z.boolean() }),
404: z.object({ error: z.string() }),
}
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>
}>
JSON Response
Return a JSON object:
return {
status: 200,
body: { message: 'Success', orderId: '123' },
}
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
Default request timeout in milliseconds
concurrency_request_limit
Maximum concurrent requests
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}