--- description: How to create HTTP endpoints in Motia globs: steps/**/*.step.ts,steps/**/*.step.js,steps/**/*_step.py alwaysApply: false --- # API Steps Guide API Steps expose HTTP endpoints that can trigger workflows and emit events. ## Creating API Steps Steps need to be created in the `steps` folder, it can be in subfolders. - Steps in TS and JS should end with `.step.ts` and `.step.js` respectively. - Steps in Python should end with `_step.py`. ## Definition Defining an API Step is done by two elements. Configuration and Handler. ### Schema Definition - **TypeScript/JavaScript**: Motia uses Zod schemas for automatic validation of request/response data - **Python**: Motia uses JSON Schema format. You can optionally use Pydantic models to generate JSON Schemas and handle manual validation in your handlers ### Configuration **TypeScript/JavaScript**: You need to export a config constant via `export const config` that is a `ApiRouteConfig` type. **Python**: You need to define a `config` dictionary with the same properties as the TypeScript `ApiRouteConfig`. ```typescript export type Emit = string | { /** * The topic name to emit to. */ topic: string; /** * Optional label for the emission, could be used for documentation or UI. */ label?: string; /** * This is purely for documentation purposes, * it doesn't affect the execution of the step. * * In Workbench, it will render differently based on this value. */ conditional?: boolean; } export interface QueryParam { /** * The name of the query parameter */ name: string /** * The description of the query parameter */ description: string } export interface ApiRouteConfig { /** * Should always be api */ type: 'api' /** * A unique name for this API step, used internally and for linking handlers. */ name: string /** * Optional human-readable description. */ description?: string /** * The URL path for this API endpoint (e.g., '/users/:id'). */ path: string /** * The HTTP method for this route. * POST, GET, PUT, DELETE, PATCH, OPTIONS, HEAD */ method: ApiRouteMethod /** * Topics this API step can emit events to. * Important note: All emits in the handler need to be listed here. */ emits: Emit[] /** * Optional: Topics that are virtually emitted, perhaps for documentation or lineage, * but not strictly required for execution. * * In Motia Workbench, they will show up as gray connections to other steps. */ virtualEmits?: Emit[] /** * Optional: Virtually subscribed topics. * * Used by API steps when we want to chain different HTTP requests * that could happen sequentially */ virtualSubscribes?: string[] /** * Flows are used to group multiple steps to be visible in diagrams in Workbench */ flows?: string[] /** * List of middlewares that will be executed BEFORE the handler is called */ middleware?: ApiMiddleware[] /** * Defined with Zod library, can be a ZodObject OR a ZodArray. * * Note: This is not validated automatically, you need to validate it in the handler. */ bodySchema?: ZodInput /** * Defined with Zod library, can be a ZodObject OR a ZodArray * * The key (number) is the HTTP status code this endpoint can return and * for each HTTP Status Code, you need to define a Zod schema that defines the response body */ responseSchema?: Record /** * Mostly for documentation purposes, it will show up in Endpoints section in Workbench */ queryParams?: QueryParam[] /** * Files to include in the step bundle. * Needs to be relative to the step file. */ includeFiles?: string[] } ``` ### Handler The handler is a function that is exported via `export const handler` that is a `ApiRouteHandler` type. #### Type Definition from Motia ```typescript export interface ApiRequest { /** * Key-value pairs of path parameters (e.g., from '/users/:id'). */ pathParams: Record /** * Key-value pairs of query string parameters. Values can be string or array of strings. */ queryParams: Record /** * The parsed request body (typically an object if JSON, but can vary). */ body: TBody /** * Key-value pairs of request headers. Values can be string or array of strings. */ headers: Record } export type ApiRouteHandler< /** * The type defined by config['bodySchema'] */ TRequestBody = unknown, /** * The type defined by config['responseSchema'] */ TResponseBody extends ApiResponse = ApiResponse, /** * The type defined by config['emits'] which is dynamic depending * on the topic handlers (Event Steps) */ TEmitData = never, > = (req: ApiRequest, ctx: FlowContext) => Promise ``` ### Handler definition **TypeScript/JavaScript:** ```typescript export const handler: Handlers['CreateResource'] = async (req, { emit, logger, state, streams }) => { // Implementation } ``` **Python:** ```python async def handler(req, context): # req: dictionary containing pathParams, queryParams, body, headers # context: object containing emit, logger, state, streams, trace_id pass ``` ### Examples #### TypeScript Example ```typescript import { ApiRouteConfig, Handlers } from 'motia'; import { z } from 'zod'; const bodySchema = z.object({ title: z.string().min(1, "Title cannot be empty"), description: z.string().optional(), category: z.string().min(1, "Category is required"), metadata: z.record(z.any()).optional() }) export const config: ApiRouteConfig = { type: 'api', name: 'CreateResource', path: '/resources', method: 'POST', emits: ['send-email'], flows: ['resource-management'], bodySchema, responseSchema: { 201: z.object({ id: z.string(), title: z.string(), category: z.string() }), 400: z.object({ error: z.string() }) } }; export const handler: Handlers['CreateResource'] = async (req, { emit, logger }) => { try { const { title, description, category, metadata } = bodySchema.parse(req.body); // Use the logger for structured logging. It's good practice to log key events or data. logger.info('Attempting to create resource', { title, category }); /** * Create files to manage service calls. * * Let's try to use Domain Driven Design to create files to manage service calls. * Steps are the entry points, they're the Controllers on the DDD architecture. */ const result = await service.createResource(resourceData); /** * This is how we emit events to trigger Event Steps. * Only use emits if the task can take a while to complete. * * Examples of long tasks are: * - LLM Calls * - Processing big files, like images, videos, audio, etc. * - Sending emails * * Other applicable examples are tasks that are likely to fail, examples: * - Webhook call to external systems * * API Calls that are okay to fail gracefully can be done without emits. */ await emit({ /** * 'topic' must be one of the topics listed in config['emits']. * do not emit to topics that are not defined in Steps */ topic: 'send-email', /** * 'data' is the payload of the event message. * make sure the data used is compliant with the Event Step input schema */ data: { /** * The data to send to the Event Step. */ resource: result, /** * The user to send the email to. */ user } }); logger.info('Resource created successfully', { resourceId, title, category }); // Return a response object for the HTTP request. return { status: 201, // CREATED (specified in config['responseSchema']) /** * 'body' is the JSON response body sent back to the client. */ body: { id: result.id, title: result.title, category: result.category, status: 'active' } }; } catch (error) { /** * For one single step project, it is fine to * handle ZodErrors here, on multiple steps projects, * it is highly recommended to handle them as a middleware * (defined in config['middleware']) */ if (error instanceof ZodError) { logger.error('Resource creation failed', { error: error.message }); return { status: 400, body: { error: 'Validation failed' } }; } logger.error('Resource creation failed', { error: error.message }); return { status: 500, body: { error: 'Creation failed' } }; } }; ``` #### Python Example ```python from pydantic import BaseModel, Field from typing import Optional, Dict, Any class ResourceData(BaseModel): title: str = Field(..., min_length=1, description="Title cannot be empty") description: Optional[str] = None category: str = Field(..., min_length=1, description="Category is required") metadata: Optional[Dict[str, Any]] = None class ResourceResponse(BaseModel): id: str title: str category: str status: str class ErrorResponse(BaseModel): error: str config = { "type": "api", "name": "CreateResource", "path": "/resources", "method": "POST", "emits": ["send-email"], "flows": ["resource-management"], "bodySchema": ResourceData.model_json_schema(), "responseSchema": { 201: ResourceResponse.model_json_schema(), 400: ErrorResponse.model_json_schema() } } async def handler(req, context): try: body = req.get("body", {}) # Optional: Validate input manually using Pydantic (Motia doesn't do this automatically) resource_data = ResourceData(**body) context.logger.info("Attempting to create resource", { "title": resource_data.title, "category": resource_data.category }) # Process the resource creation result = await service.create_resource({ "title": resource_data.title, "description": resource_data.description, "category": resource_data.category, "metadata": resource_data.metadata }) # Emit event to trigger other steps await context.emit({ "topic": "send-email", "data": { "resource": result, "user_id": "example-user" } }) context.logger.info("Resource created successfully", { "resource_id": result.get("id"), "title": result.get("title"), "category": result.get("category") }) return { "status": 201, "body": { "id": result.get("id"), "title": result.get("title"), "category": result.get("category"), "status": "active" } } except ValidationError as e: context.logger.error("Resource creation failed - Pydantic validation error", {"error": str(e)}) return { "status": 400, "body": {"error": "Validation failed"} } except Exception as e: context.logger.error("Resource creation failed", {"error": str(e)}) return { "status": 500, "body": {"error": "Creation failed"} } ```