Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 275 additions & 41 deletions frameworks/motia/HANDLER_MIGRATION_GUIDE.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,24 @@ All operations in the array are applied atomically — no data loss, no race con
## Usage in State

```typescript
await ctx.state.update<Order>('orders', orderId, [
import { stateManager } from 'motia'

await stateManager.update<Order>('orders', orderId, [
{ type: 'increment', path: 'completedSteps', by: 1 },
{ type: 'set', path: 'status', value: 'shipped' },
{ type: 'decrement', path: 'retries', by: 1 },
{ type: 'remove', path: 'tempData' },
])
```

Returns `{ new_value, old_value }` — the same return type as `state.set()`.
Returns `{ new_value, old_value }` — the same return type as `stateManager.set()`.

### Python

```python
await context.state.update("orders", order_id, [
from motia import state_manager

await state_manager.update("orders", order_id, [
{"type": "increment", "path": "completedSteps", "by": 1},
{"type": "set", "path": "status", "value": "shipped"},
{"type": "decrement", "path": "retries", "by": 1},
Expand All @@ -76,8 +80,10 @@ await context.state.update("orders", order_id, [
The same `UpdateOp` types work on stream data:

```typescript
export const handler: Handlers<typeof config> = async (input, { streams }) => {
await streams.deployment.update('data', deploymentId, [
import { deploymentStream } from './deployment.stream'

export const handler: Handlers<typeof config> = async (input) => {
await deploymentStream.update('data', deploymentId, [
{ type: 'increment', path: 'completedSteps', by: 1 },
{ type: 'set', path: 'status', value: 'progress' },
])
Expand All @@ -93,7 +99,9 @@ Stream updates are also atomic and trigger stream events that connected clients
The `merge` operation performs a shallow merge of an object into the existing value:

```typescript
await ctx.state.update('users', userId, [
import { stateManager } from 'motia'

await stateManager.update('users', userId, [
{
type: 'merge',
path: 'preferences',
Expand All @@ -105,7 +113,9 @@ await ctx.state.update('users', userId, [
If `path` is omitted, the merge is applied to the root object:

```typescript
await ctx.state.update('users', userId, [
import { stateManager } from 'motia'

await stateManager.update('users', userId, [
{
type: 'merge',
value: { lastLogin: new Date().toISOString(), loginCount: 5 },
Expand All @@ -120,7 +130,9 @@ await ctx.state.update('users', userId, [
### Counter Tracking

```typescript
await ctx.state.update('metrics', 'api-calls', [
import { stateManager } from 'motia'

await stateManager.update('metrics', 'api-calls', [
{ type: 'increment', path: 'total', by: 1 },
{ type: 'increment', path: `endpoints.${endpoint}`, by: 1 },
{ type: 'set', path: 'lastCall', value: new Date().toISOString() },
Expand All @@ -130,7 +142,9 @@ await ctx.state.update('metrics', 'api-calls', [
### Status Transitions

```typescript
await ctx.state.update('orders', orderId, [
import { stateManager } from 'motia'

await stateManager.update('orders', orderId, [
{ type: 'set', path: 'status', value: 'completed' },
{ type: 'set', path: 'completedAt', value: new Date().toISOString() },
{ type: 'remove', path: 'processingData' },
Expand All @@ -140,7 +154,9 @@ await ctx.state.update('orders', orderId, [
### Parallel Step Completion

```typescript
await ctx.state.update('tasks', taskId, [
import { stateManager } from 'motia'

await stateManager.update('tasks', taskId, [
{ type: 'increment', path: 'completedSteps', by: 1 },
{ type: 'merge', path: 'results', value: { [stepName]: result } },
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Each ExecModule watches its own file patterns and manages its own SDK process in
A TypeScript HTTP endpoint triggers a Python ML processing step:

```typescript title="steps/submit-review.step.ts"
import type { Handlers, StepConfig } from 'motia'
import { type Handlers, type StepConfig, enqueue } from 'motia'
import { z } from 'zod'

export const config = {
Expand All @@ -80,13 +80,15 @@ export const config = {
flows: ['review-pipeline'],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async ({ request }, { enqueue }) => {
export const handler: Handlers<typeof config> = async ({ request }) => {
await enqueue({ topic: 'review.submitted', data: { text: request.body.text } })
return { status: 202, body: { status: 'processing' } }
}
```

```python title="steps/analyze_review_step.py"
from motia import state_manager, enqueue

config = {
"name": "AnalyzeReview",
"description": "Runs sentiment analysis on the review",
Expand All @@ -101,13 +103,13 @@ async def handler(input, ctx):
text = input.get("text", "")
sentiment = analyze_sentiment(text)

await ctx.state.set("reviews", ctx.trace_id, {
await state_manager.set("reviews", ctx.trace_id, {
"text": text,
"sentiment": sentiment,
"analyzed": True
})

await ctx.enqueue({
await enqueue({
"topic": "review.analyzed",
"data": {"traceId": ctx.trace_id, "sentiment": sentiment}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A single Step can respond to multiple trigger types. This is useful when the sam

```typescript
import type { Handlers, StepConfig } from 'motia'
import { logger, enqueue, stateManager } from 'motia'
import { z } from 'zod'

const orderSchema = z.object({ orderId: z.string(), amount: z.number() })
Expand All @@ -28,26 +29,26 @@ export const config = {
export const handler: Handlers<typeof config> = async (input, ctx) => {
return ctx.match({
http: async ({ request }) => {
await processOrder(request.body, ctx)
await processOrder(request.body)
return { status: 200, body: { success: true } }
},
queue: async (data) => {
const payload = ctx.getData()
await processOrder(payload, ctx)
await processOrder(payload)
},
cron: async () => {
ctx.logger.info('Running scheduled order processing')
const pendingOrders = await ctx.state.list('pending-orders')
logger.info('Running scheduled order processing')
const pendingOrders = await stateManager.list('pending-orders')
for (const order of pendingOrders) {
await processOrder(order, ctx)
await processOrder(order)
}
},
})
}

async function processOrder(order: any, ctx: any) {
ctx.logger.info('Processing order', { orderId: order.orderId })
await ctx.enqueue({ topic: 'order.processed', data: order })
async function processOrder(order: any) {
logger.info('Processing order', { orderId: order.orderId })
await enqueue({ topic: 'order.processed', data: order })
}
```

Expand All @@ -70,10 +71,10 @@ return ctx.match({
},
queue: async (data) => {
const payload = ctx.getData()
ctx.logger.info('From queue', payload)
logger.info('From queue', payload)
},
cron: async () => {
ctx.logger.info('From cron')
logger.info('From cron')
},
})
```
Expand Down Expand Up @@ -102,7 +103,7 @@ When you do not care about the trigger type and just need the data payload:

```typescript
const data = ctx.getData()
ctx.logger.info('Processing', data)
logger.info('Processing', data)
```

## Best Practices
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The `step()` helper provides a streamlined way to define multi-trigger Steps wit
## Basic Usage

```typescript
import { http, queue, step } from 'motia'
import { http, queue, step, logger, enqueue } from 'motia'
import { z } from 'zod'

const orderSchema = z.object({
Expand All @@ -31,13 +31,13 @@ export const { config, handler } = step(stepConfig, async (input, ctx) => {

return ctx.match({
http: async (request) => {
ctx.logger.info('Manual order', { body: request.body })
await ctx.enqueue({ topic: 'notification', data: request.body })
logger.info('Manual order', { body: request.body })
await enqueue({ topic: 'notification', data: request.body })
return { status: 200, body: { success: true } }
},
queue: async (queueInput) => {
ctx.logger.info('Processing from queue', { data })
await ctx.enqueue({ topic: 'notification', data })
logger.info('Processing from queue', { data })
await enqueue({ topic: 'notification', data })
},
})
})
Expand All @@ -57,7 +57,7 @@ Extracts the data payload from the input regardless of which trigger activated t

```typescript
const data = ctx.getData()
ctx.logger.info('Processing data', data)
logger.info('Processing data', data)
```

## ctx.match()
Expand All @@ -70,19 +70,19 @@ return ctx.match({
return { status: 200, body: { ok: true } }
},
queue: async (data) => {
ctx.logger.info('From queue')
logger.info('From queue')
},
cron: async () => {
ctx.logger.info('From cron')
logger.info('From cron')
},
state: async (stateEvent) => {
ctx.logger.info('State changed', stateEvent)
logger.info('State changed', stateEvent)
},
stream: async (streamEvent) => {
ctx.logger.info('Stream event', streamEvent)
logger.info('Stream event', streamEvent)
},
default: async (input) => {
ctx.logger.warn('Unknown trigger type')
logger.warn('Unknown trigger type')
},
})
```
Expand Down
Loading
Loading