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
Function that determines if the trigger should fire:condition: (input: StateTriggerInput<T>) => boolean
Optional Fields
Specific state scope to monitor (if omitted, monitors all scopes)
Specific state key to monitor (if omitted, monitors all keys in scope)
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.