Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/MotiaDev/motia/llms.txt

Use this file to discover all available pages before exploring further.

Overview

State triggers enable reactive workflows that execute when state values change. Perfect for implementing audit logs, cascading updates, and real-time reactions to data changes.

Basic Configuration

Define a state trigger in your step config:
import type { Handlers, StateTriggerInput, StepConfig } from 'motia'
import type { Order } from './types'

export const config = {
  name: 'OrderCompleted',
  triggers: [
    {
      type: 'state',
      condition: (input: StateTriggerInput<Order>) => {
        return (
          input.group_id === 'orders' &&
          !!input.new_value &&
          input.new_value.status === 'completed'
        )
      },
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (input, ctx) => {
  const order = input.new_value as Order
  
  ctx.logger.info('Order completed', { orderId: order.id })
  
  await ctx.enqueue({
    topic: 'email.send',
    data: {
      to: order.email,
      template: 'order-complete',
      order,
    },
  })
}

Configuration Options

Required Fields

type
string
required
Must be "state"
condition
function
required
Function that determines if the trigger should fire:
condition: (input: StateTriggerInput<T>) => boolean

Optional Fields

scope
string
Specific state scope to monitor (if omitted, monitors all scopes)
key
string
Specific state key to monitor (if omitted, monitors all keys in scope)
condition_function_id
string
Reference to a separate condition function (advanced usage)

Handler Signature

State handlers receive the trigger input and context:
type StateTriggerInput<T> = {
  group_id: string      // State scope (e.g., 'orders', 'users')
  item_id: string       // State key
  old_value: T | null   // Previous value (null if new)
  new_value: T | null   // New value (null if deleted)
}

type StateHandler<T> = (
  input: StateTriggerInput<T>,
  ctx: HandlerContext
) => Promise<void>

Condition Function

The condition function receives the state change event and returns true to execute the handler:
condition: (input: StateTriggerInput<Order>) => {
  // Check scope
  if (input.group_id !== 'orders') return false
  
  // Check if value exists
  if (!input.new_value) return false
  
  // Check specific field
  if (input.new_value.status !== 'shipped') return false
  
  return true
}

Common Patterns

Completion Detection

Trigger when a process completes:
import type { StateTriggerInput } from 'motia'
import type { ParallelMerge } from './types'

export const config = {
  name: 'ParallelMergeComplete',
  triggers: [
    {
      type: 'state',
      condition: (input: StateTriggerInput<ParallelMerge>) => {
        return (
          input.group_id === 'merges' &&
          !!input.new_value &&
          input.new_value.totalSteps === input.new_value.completedSteps
        )
      },
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (input, ctx) => {
  const result = input.new_value as ParallelMerge
  const traceId = input.item_id
  
  ctx.logger.info('All parallel steps completed', {
    traceId,
    totalSteps: result.totalSteps,
    duration: Date.now() - result.startedAt,
  })
  
  // Trigger next workflow
  await ctx.enqueue({
    topic: 'workflow.complete',
    data: { traceId, result },
  })
}

Value Change Detection

React to specific field changes:
type User = {
  id: string
  email: string
  verified: boolean
  createdAt: string
}

export const config = {
  name: 'UserVerified',
  triggers: [
    {
      type: 'state',
      condition: (input: StateTriggerInput<User>) => {
        return (
          input.group_id === 'users' &&
          input.old_value?.verified === false &&
          input.new_value?.verified === true
        )
      },
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (input, ctx) => {
  const user = input.new_value as User
  
  await ctx.enqueue({
    topic: 'email.welcome',
    data: { userId: user.id, email: user.email },
  })
}

Threshold Monitoring

Trigger when values cross thresholds:
type Metrics = {
  errorRate: number
  requestCount: number
  timestamp: string
}

export const config = {
  name: 'HighErrorRate',
  triggers: [
    {
      type: 'state',
      condition: (input: StateTriggerInput<Metrics>) => {
        return (
          input.group_id === 'metrics' &&
          !!input.new_value &&
          input.new_value.errorRate > 0.05 // 5% threshold
        )
      },
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (input, ctx) => {
  const metrics = input.new_value as Metrics
  
  await ctx.enqueue({
    topic: 'alert.send',
    data: {
      severity: 'high',
      message: `Error rate at ${(metrics.errorRate * 100).toFixed(2)}%`,
      metrics,
    },
  })
}

Audit Logging

Log all changes to specific entities:
export const config = {
  name: 'OrderAuditLog',
  triggers: [
    {
      type: 'state',
      condition: (input: StateTriggerInput<Order>) => {
        return input.group_id === 'orders'
      },
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (input, ctx) => {
  const auditEntry = {
    timestamp: new Date().toISOString(),
    groupId: input.group_id,
    itemId: input.item_id,
    oldValue: input.old_value,
    newValue: input.new_value,
    operation: !input.old_value ? 'create' : 
                !input.new_value ? 'delete' : 'update',
  }
  
  await ctx.state.set('audit-log', crypto.randomUUID(), auditEntry)
}

Deletion Detection

React to state deletions:
export const config = {
  name: 'SessionExpired',
  triggers: [
    {
      type: 'state',
      condition: (input: StateTriggerInput<Session>) => {
        return (
          input.group_id === 'sessions' &&
          input.new_value === null // Item was deleted
        )
      },
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (input, ctx) => {
  ctx.logger.info('Session expired', { sessionId: input.item_id })
  
  await ctx.enqueue({
    topic: 'analytics.session-end',
    data: {
      sessionId: input.item_id,
      session: input.old_value,
    },
  })
}

Scoped Monitoring

Monitor specific state scopes:
export const config = {
  name: 'InventoryLow',
  triggers: [
    {
      type: 'state',
      scope: 'inventory', // Only monitor 'inventory' scope
      condition: (input: StateTriggerInput<InventoryItem>) => {
        return (
          !!input.new_value &&
          input.new_value.quantity < input.new_value.reorderThreshold
        )
      },
    },
  ],
} as const satisfies StepConfig

Key Monitoring

Monitor a specific state key:
export const config = {
  name: 'ConfigChanged',
  triggers: [
    {
      type: 'state',
      scope: 'system',
      key: 'config', // Only monitor 'system.config'
      condition: (input) => {
        return input.new_value !== null
      },
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (input, ctx) => {
  ctx.logger.info('System config changed', {
    oldValue: input.old_value,
    newValue: input.new_value,
  })
  
  // Reload configuration
  await reloadSystemConfig(input.new_value)
}

Cascading Updates

Trigger related state updates:
export const config = {
  name: 'UpdateUserStats',
  triggers: [
    {
      type: 'state',
      condition: (input: StateTriggerInput<Order>) => {
        return (
          input.group_id === 'orders' &&
          !!input.new_value &&
          !input.old_value // New order created
        )
      },
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (input, ctx) => {
  const order = input.new_value as Order
  
  // Update user statistics
  await ctx.state.update('user-stats', order.userId, [
    { type: 'increment', path: 'totalOrders', by: 1 },
    { type: 'increment', path: 'totalSpent', by: order.amount },
  ])
}

Module Configuration

Configure the state module in motia.config.json:
{
  "modules": {
    "state": {
      "adapter": {
        "type": "redis",
        "config": {
          "url": "redis://localhost:6379"
        }
      }
    }
  }
}

Supported Adapters

  • kv_store - Local key-value store (development only)
  • redis - Redis-backed state (production)

Best Practices

Avoid infinite loops: Don’t modify the same state that triggered the handler, or use careful conditions to prevent cascading triggers.
Type safety: Use TypeScript types for StateTriggerInput<T> to get autocomplete for old_value and new_value fields.
Performance: State triggers fire synchronously with state changes. Keep handlers lightweight or enqueue heavy work to background queues.

Debugging

Log trigger inputs to understand state changes:
export const handler: Handlers<typeof config> = async (input, ctx) => {
  ctx.logger.info('State trigger fired', {
    groupId: input.group_id,
    itemId: input.item_id,
    oldValue: input.old_value,
    newValue: input.new_value,
  })
  
  // Handler logic
}
State triggers are eventually consistent - there may be a small delay between state changes and trigger execution.