Skip to main content
State triggers allow you to execute workflows when state values change, enabling reactive patterns and event-driven architectures based on state mutations.

Basic usage

import { step, state } from 'motia'

export const config = step({
  name: 'on-user-update',
  triggers: [state()],
})

export const handler = async (input, ctx) => {
  const { group_id, item_id, old_value, new_value } = input
  
  ctx.logger.info('State changed', {
    group: group_id,
    item: item_id,
    from: old_value,
    to: new_value,
  })
}

Input structure

State triggers receive an input object with the following structure:
type
'state'
required
Always 'state' for state triggers
group_id
string
required
The group identifier (scope) where the state change occurred
item_id
string
required
The item identifier (key) that changed
old_value
T | undefined
The previous value before the change. undefined for new items
new_value
T | undefined
The new value after the change. undefined for deleted items

Accessing state

Read and write state within handlers:
import { step, state } from 'motia'

export const config = step({
  name: 'track-changes',
  triggers: [state()],
})

export const handler = async (input, ctx) => {
  const { group_id, item_id, new_value } = input
  
  // Read current state
  const current = await ctx.state.get(group_id, item_id)
  
  // Update state
  await ctx.state.set(group_id, `${item_id}_history`, {
    timestamp: Date.now(),
    value: new_value,
  })
  
  ctx.logger.info('Change tracked')
}

Conditional triggers

Filter state changes with conditions:
import { step, state } from 'motia'

export const config = step({
  name: 'on-status-active',
  triggers: [
    state((input, ctx) => {
      const { new_value } = input
      return new_value?.status === 'active'
    }),
  ],
})

export const handler = async (input, ctx) => {
  ctx.logger.info('User activated', {
    userId: input.item_id,
  })
}

Detecting change types

Differentiate between creates, updates, and deletes:
export const handler = async (input, ctx) => {
  const { old_value, new_value, item_id } = input
  
  if (!old_value && new_value) {
    // Create
    ctx.logger.info('Item created', { item_id })
  } else if (old_value && new_value) {
    // Update
    ctx.logger.info('Item updated', { item_id })
  } else if (old_value && !new_value) {
    // Delete
    ctx.logger.info('Item deleted', { item_id })
  }
}

State operations

Set state

await ctx.state.set('users', 'user-123', {
  name: 'John Doe',
  status: 'active',
})

Get state

const user = await ctx.state.get('users', 'user-123')

Update state

await ctx.state.update('users', 'user-123', [
  { type: 'set', path: 'status', value: 'inactive' },
  { type: 'set', path: 'lastLogin', value: Date.now() },
])

Delete state

await ctx.state.delete('users', 'user-123')

List items

const users = await ctx.state.list('users')

Clear group

await ctx.state.clear('users')

Configuration options

condition
function
Optional function (input, ctx) => boolean to filter which state changes trigger the workflow:
  • input.type - Always 'state'
  • input.group_id - The group identifier
  • input.item_id - The item identifier
  • input.old_value - Previous value
  • input.new_value - New value

Use cases

User lifecycle events

React to user status changes:
import { step, state } from 'motia'

export const config = step({
  name: 'user-lifecycle',
  triggers: [
    state((input) => input.group_id === 'users'),
  ],
  enqueues: ['send-email'],
})

export const handler = async (input, ctx) => {
  const { item_id, old_value, new_value } = input
  
  // New user registration
  if (!old_value && new_value) {
    await ctx.enqueue({
      topic: 'send-email',
      data: {
        to: new_value.email,
        template: 'welcome',
      },
    })
  }
  
  // Status change
  if (old_value?.status !== new_value?.status) {
    ctx.logger.info('User status changed', {
      userId: item_id,
      from: old_value?.status,
      to: new_value?.status,
    })
  }
}

Audit trail

Maintain change history:
import { step, state } from 'motia'

export const config = step({
  name: 'audit-trail',
  triggers: [state()],
})

export const handler = async (input, ctx) => {
  const { group_id, item_id, old_value, new_value } = input
  
  // Store audit record
  await ctx.state.set('audit', `${group_id}_${item_id}_${Date.now()}`, {
    timestamp: Date.now(),
    group: group_id,
    item: item_id,
    oldValue: old_value,
    newValue: new_value,
    traceId: ctx.traceId,
  })
}

Derived state

Compute derived values:
import { step, state } from 'motia'

export const config = step({
  name: 'update-aggregates',
  triggers: [
    state((input) => input.group_id === 'orders'),
  ],
})

export const handler = async (input, ctx) => {
  const { new_value } = input
  
  if (!new_value) return
  
  // Update customer total
  const customerId = new_value.customerId
  const orders = await ctx.state.list('orders')
  const customerOrders = orders.filter(
    (o) => o.customerId === customerId
  )
  const total = customerOrders.reduce((sum, o) => sum + o.total, 0)
  
  await ctx.state.set('customer-totals', customerId, {
    orderCount: customerOrders.length,
    total,
  })
}

Cascading updates

Propagate changes:
import { step, state } from 'motia'

export const config = step({
  name: 'cascade-delete',
  triggers: [
    state((input) => {
      return input.group_id === 'users' && !input.new_value
    }),
  ],
})

export const handler = async (input, ctx) => {
  const userId = input.item_id
  
  // Delete related data
  const sessions = await ctx.state.list('sessions')
  for (const session of sessions) {
    if (session.userId === userId) {
      await ctx.state.delete('sessions', session.id)
    }
  }
  
  ctx.logger.info('User data cleaned up', { userId })
}

Build docs developers (and LLMs) love