Skip to main content

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.

Build docs developers (and LLMs) love