Motia Icon
Development Guide

State Management

Persistent Key-Value storage that works across Triggers, Steps, and Functions

State is persistent key-value storage that works across all your Triggers, Steps, and Functions. Set data in one Trigger, read it in another. Works across TypeScript, Python, and JavaScript.

How It Works

State organizes data into groups. Each group can hold multiple items with unique keys.

Think of it like folders and files:

  • groupId = A folder name (like orders, users, cache)
  • key = A file name inside that folder
  • value = The actual data
export const handler: Handlers['MyStep'] = async (input, { state }) => {
  // Store an item in a group
  await state.set('orders', 'order-123', { 
    id: 'order-123',
    status: 'pending',
    total: 99.99 
  })
  
  // Get a specific item
  const order = await state.get('orders', 'order-123')
  
  // Get all items in a group
  const allOrders = await state.getGroup('orders')
  
  // Delete a specific item
  await state.delete('orders', 'order-123')
  
  // Clear entire group
  await state.clear('orders')
}

State Methods

MethodWhat it does
state.set(groupId, key, value)Store an item in a group
state.get(groupId, key)Get a specific item (returns null if not found)
state.getGroup(groupId)Get all items in a group as an array
state.delete(groupId, key)Remove a specific item
state.clear(groupId)Remove all items in a group

Real-World Example

Let's build an order processing workflow that uses state across multiple Steps.

Step 1 - API receives order:

export const handler: Handlers['CreateOrder'] = async (req, { state, emit, logger }) => {
  const orderId = crypto.randomUUID()
  
  const order = {
    id: orderId,
    items: req.body.items,
    total: req.body.total,
    status: 'pending',
    createdAt: new Date().toISOString()
  }
  
  // Store in state
  await state.set('orders', orderId, order)
  
  logger.info('Order created', { orderId })
  
  // Trigger processing
  await emit({ 
    topic: 'order.created', 
    data: { orderId } 
  })
  
  return { status: 201, body: order }
}

Step 2 - Process payment:

export const handler: Handlers['ProcessPayment'] = async (input, { state, emit, logger }) => {
const { orderId } = input
 
// Get order from state
const order = await state.get('orders', orderId)
 
if (!order) {
  throw new Error(`Order ${orderId} not found`)
}
 
// Update status
order.status = 'paid'
await state.set('orders', orderId, order)
 
logger.info('Payment processed', { orderId })
 
await emit({ 
  topic: 'payment.completed', 
  data: { orderId } 
})
}

Step 3 - View all orders (Cron job):

export const handler: Handlers['DailyReport'] = async ({ state, logger }) => {
// Get all orders
const allOrders = await state.getGroup<Order>('orders')
 
const pending = allOrders.filter(o => o.status === 'pending')
const paid = allOrders.filter(o => o.status === 'paid')
 
logger.info('Daily order report', {
  total: allOrders.length,
  pending: pending.length,
  paid: paid.length
})
}

When to Use State

✅ Good use cases:

  • Temporary workflow data - Data that's only needed during a flow execution
  • API response caching - Cache expensive API calls that don't change often
  • Sharing data between Steps - Pass data between Steps without emitting it in events
  • Building up results - Accumulate data across multiple Steps

❌ Better alternatives:

  • Persistent user data - Use a database like Postgres or MongoDB
  • File storage - Use S3 or similar for images, PDFs, documents
  • Real-time updates - Use Motia Streams for live data to clients
  • Large datasets - Use a proper database, not state

State Adapters

State adapters control where and how state is stored. Motia provides default adapters that work out of the box, and distributed adapters for production deployments.

Default Adapter (File Storage)

No setup needed. State goes to .motia/motia.state.json.

motia.config.ts
import { config } from '@motiadev/core'
 
export default config({
  // Uses FileStateAdapter by default
  // State stored in .motia/motia.state.json
})

The default FileStateAdapter is perfect for single-instance deployments, development, and testing. No configuration needed!

Distributed Adapter (Redis)

For production deployments with multiple Motia instances, use Redis to share state across instances:

motia.config.ts
import { config } from '@motiadev/core'
import { RedisStateAdapter } from '@motiadev/adapter-redis-state'
 
export default config({
  adapters: {
    state: new RedisStateAdapter({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
    }),
  },
})

Use distributed adapters (like Redis) when running multiple Motia instances. Without them, each instance has isolated state that isn't shared.

Your Step code stays the same:

// Works with both file and Redis adapters
export const handler: Handlers['MyStep'] = async (req, { state }) => {
  await state.set('orders', 'order-123', { id: 'order-123' })
  const order = await state.get('orders', 'order-123')
  // ... rest of your code
}

The adapter handles the storage backend - your application code doesn't change.

Learn more about adapters →


Remember

  • Organize data using groupId (like orders, users, cache)
  • Each item needs a unique key within its groupId
  • Use getGroup(groupId) to retrieve all items in a group
  • State works the same across TypeScript, Python, and JavaScript
  • Clean up state when you're done with it
  • Use databases for permanent data, state for temporary workflow data

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

On this page