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
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.