Motia Icon

Defining Steps

Learn how to create powerful, type-safe steps in TypeScript, Python, and JavaScript with automatic validation and observability.

Defining Steps

Steps are the core building blocks of Motia - isolated, composable functions that handle specific pieces of business logic. Each step is automatically discovered, type-safe, and observable across multiple programming languages.

The Motia Step Pattern

Every step follows a simple, consistent pattern:

  1. 📁 File Naming: *.step.* or *_step.* (e.g., user-api.step.ts, process-data.step.py, data_processor_step.py)
  2. ⚙️ Configuration: Export a config object defining step behavior
  3. 🔧 Handler: Export a handler function containing business logic
  4. 🤖 Auto-Discovery: Motia automatically finds and registers your steps

🌍 Multi-Language Support

Write each step in the best language for the job - TypeScript for APIs, Python for data processing, JavaScript for quick scripts. Motia handles type safety and communication automatically.

Step Capabilities

Event-Driven Architecture

  • Subscribe to events from other steps
  • Emit events to trigger subsequent steps
  • Chain steps together into powerful workflows

Type Safety Across Languages

  • Auto-generated TypeScript definitions
  • Schema validation with Zod, Pydantic, JSDoc
  • Runtime validation for data consistency

Built-in Observability

  • Distributed tracing across all steps
  • Centralized logging with step context
  • Visual debugging in Motia Workbench

Step Configuration

Each step exports a config object that tells Motia how to handle the step. The configuration varies by step type but shares common properties:

Universal Config Properties

PropertyTypeDescriptionRequired
type'api' | 'event' | 'cron' | 'noop'The step type
namestringUnique identifier for the step
descriptionstringDocumentation for the step-
subscribesstring[]Topics this step listens to-
emitsstring[]Topics this step can emit-
flowsstring[]Flow identifiers this step belongs to-

Type-Specific Properties

Each step type has additional properties:

Configuration Examples

user-api.step.ts
import { z } from 'zod'
 
export const config = {
  type: 'api',
  name: 'user-api',
  path: '/users/:id',
  method: 'GET',
  description: 'Fetch user by ID',
  
  // Schema validation
  bodySchema: z.object({
    userId: z.string().uuid()
  }),
  
  responseSchema: {
    200: z.object({
      user: z.object({
        id: z.string(),
        name: z.string(),
        email: z.string().email()
      })
    })
  },
  
  emits: ['user.fetched'],
  flows: ['user-management']
} as const

Step Handlers

The handler function contains your step's business logic. Motia automatically calls the handler with validated input data and a context object containing powerful utilities.

Handler Signature

Every handler receives two parameters:

  1. Input Data - Validated data from the triggering event (API request, subscription, etc.)
  2. Context Object - Tools for interacting with the Motia runtime

Context Object Features

ToolDescriptionUsage
emitSend events to other stepsawait emit({ topic, data })
loggerCentralized logging with trace contextlogger.info('Processing user', { userId })
statePersistent data storage across stepsawait state.set(traceId, key, value)
traceIdUnique identifier for request tracingFlow isolation and debugging
utilsHelper utilities (dates, crypto, etc.)Various utility functions

Handler Examples with Type Safety

user-api.step.ts
import { Handlers } from 'motia'
import { z } from 'zod'
 
// Config with schema validation (from previous example)
export const config = { /* ... */ }
 
// 🎉 Handler gets full type safety from config!
export const handler: Handlers['user-api'] = async (req, { emit, logger, state, traceId }) => {
  logger.info('Processing user request', { userId: req.params.id })
  
  // req.body is automatically typed from bodySchema
  const { userId } = req.body
  
  // Simulate user fetch
  const user = await getUserById(userId)
  
  // Store in state for other steps
  await state.set(traceId, 'current-user', user)
  
  // Emit event to trigger other steps
  await emit({
    topic: 'user.fetched', // ✅ Type-checked against config.emits
    data: { user, timestamp: new Date() }
  })
  
  // Return response matching responseSchema
  return {
    status: 200,
    body: { user } // ✅ Validated against responseSchema
  }
}
 
async function getUserById(id: string) {
  // Database query or API call
  return { id, name: 'John Doe', email: 'john@example.com' }
}

Type Safety Benefits

Automatic Type Generation

Motia automatically generates TypeScript definitions based on your step configurations:

types.d.ts (Auto-generated)
declare module 'motia' {
  interface Handlers {
    'user-api': ApiRouteHandler<
      { userId: string }, // From bodySchema
      ApiResponse<200, { user: User }> // From responseSchema
    >
    'process-data': EventHandler<
      { user: User }, // From subscribes topic
      { topic: 'data.processed'; data: ProcessedUser } // From emits
    >
  }
}

Cross-Language Validation

Even in multi-language workflows, Motia ensures data consistency:

  1. TypeScript API validates input with Zod schemas
  2. Python step receives validated data via Pydantic models
  3. JavaScript step gets type hints via JSDoc annotations

🚀 Ready to Build?

Check out API Endpoints to see a complete REST API tutorial in action, or explore specific step types:

Need help? See our Community Resources for questions, examples, and discussions.