Motia API Workflows: Steps & Events 2026
Motia API Workflows: Steps & Events 2026
TL;DR
Motia is the #1 backend framework in JavaScript Rising Stars 2025 because it solves a real problem: modern APIs aren't just HTTP endpoints — they're orchestrations of HTTP calls, events, background jobs, and AI agents. Motia's Step primitive unifies all of these into one model. Instead of wiring together Express + BullMQ + node-cron + Kafka, you write Steps that emit topics, and Motia connects them automatically. This article shows how to design event-driven API workflows with Motia in 2026.
Key Takeaways
- Motia's core primitive is the Step — every API endpoint, event handler, and scheduled job is a Step
- Steps communicate via topics (pub/sub) — decoupled producers and consumers with automatic routing
- Unified state store — shared key-value store accessible from every Step, automatically traced
- Multi-language: TypeScript and Python Steps coexist — write your LLM workflows in Python, your REST endpoints in TypeScript
- Built-in observability: every workflow is automatically traced end-to-end from HTTP request to final state change
- API design insight: Motia changes how you think about API boundaries — instead of designing endpoints, you design events
The Event-Driven API Design Pattern
Traditional REST API design thinks in endpoints:
POST /orders → create an order
GET /orders/:id → get order status
POST /orders/:id/pay → pay for an order
Event-driven API design thinks in events:
order.created → triggers payment processing
payment.succeeded → triggers fulfillment
fulfillment.shipped → triggers notification
The REST endpoints still exist — they're the entry points. But the business logic is driven by events, not by HTTP calls. This separation has a significant architectural advantage: HTTP clients don't need to know about the internal workflow. A client calls POST /orders and gets an order ID back. Everything that happens next (payment, fulfillment, notification) is internal workflow triggered by events.
Motia makes this pattern first-class.
Designing a Motia API Workflow
Let's design a payment processing workflow with Motia. This is a common, realistic use case: a REST endpoint receives an order, and a series of internal steps process it asynchronously.
Step 1: The Entry Point (API Step)
// src/steps/create-order.step.ts
import { defineStep } from '@motiadev/core';
import { z } from 'zod';
export default defineStep({
type: 'api',
path: '/orders',
method: 'POST',
// Zod schema validates incoming request body
bodySchema: z.object({
items: z.array(z.object({
productId: z.string(),
quantity: z.number().positive(),
})),
customerId: z.string(),
}),
async handler({ body, emit, state, ctx }) {
// Generate a correlation ID for tracing
const orderId = crypto.randomUUID();
const traceId = ctx.traceId;
// Persist initial state
await state.set(`order:${orderId}`, {
...body,
orderId,
status: 'pending',
createdAt: new Date().toISOString(),
});
// Emit to trigger downstream processing — non-blocking
await emit('order.created', { orderId, customerId: body.customerId });
// Respond immediately — processing happens asynchronously
return {
orderId,
status: 'pending',
traceId, // Client can use this to query trace later
};
},
});
The client gets an immediate 202 Accepted-style response with an orderId. The rest of the workflow proceeds asynchronously via events.
Step 2: Price Calculation (Event Step)
// src/steps/calculate-price.step.ts
import { defineStep } from '@motiadev/core';
export default defineStep({
type: 'event',
subscribes: ['order.created'],
async handler({ data, emit, state }) {
const order = await state.get(`order:${data.orderId}`);
// Fetch prices for each item
const itemPrices = await Promise.all(
order.items.map(async (item) => ({
...item,
unitPrice: await getProductPrice(item.productId),
subtotal: (await getProductPrice(item.productId)) * item.quantity,
}))
);
const total = itemPrices.reduce((sum, item) => sum + item.subtotal, 0);
// Update state with pricing
await state.set(`order:${data.orderId}`, {
...order,
items: itemPrices,
total,
status: 'priced',
});
// Emit with price information
await emit('order.priced', {
orderId: data.orderId,
total,
customerId: order.customerId,
});
},
});
Step 3: Payment (Event Step)
// src/steps/process-payment.step.ts
import { defineStep } from '@motiadev/core';
export default defineStep({
type: 'event',
subscribes: ['order.priced'],
async handler({ data, emit, state }) {
const order = await state.get(`order:${data.orderId}`);
try {
const charge = await stripe.paymentIntents.create({
amount: Math.round(data.total * 100), // cents
currency: 'usd',
customer: data.customerId,
confirm: true,
});
await state.set(`order:${data.orderId}`, {
...order,
status: 'paid',
paymentIntentId: charge.id,
});
await emit('payment.succeeded', {
orderId: data.orderId,
paymentIntentId: charge.id,
});
} catch (err) {
await state.set(`order:${data.orderId}`, {
...order,
status: 'payment_failed',
error: err.message,
});
await emit('payment.failed', {
orderId: data.orderId,
error: err.message,
});
}
},
});
Step 4: AI Risk Analysis (Python Step)
# src/steps/risk-analysis.step.py
from motia import define_step
from anthropic import Anthropic
client = Anthropic()
@define_step(
type="event",
subscribes=["order.priced"]
)
async def handler(data, emit, state):
"""
Run AI risk analysis in parallel with payment processing.
Both this step and process-payment.step.ts subscribe to order.priced —
Motia fans out to both simultaneously.
"""
order = await state.get(f"order:{data['orderId']}")
response = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=512,
messages=[{
"role": "user",
"content": f"""
Analyze this order for fraud risk. Return JSON with:
- risk_score (0.0 to 1.0)
- risk_factors (array of strings)
- recommendation (allow/review/block)
Order: {order}
"""
}]
)
import json
analysis = json.loads(response.content[0].text)
await state.set(f"order:{data['orderId']}:risk", analysis)
if analysis['recommendation'] == 'block':
await emit('order.flagged', {
'orderId': data['orderId'],
'risk_score': analysis['risk_score'],
'factors': analysis['risk_factors']
})
Note that risk-analysis.step.py and process-payment.step.ts both subscribe to order.priced. Motia fans out to both simultaneously — the payment processes and the AI analysis runs in parallel, without any explicit coordination code.
Querying Order Status (API Step)
The client originally received just an orderId. They need a way to check status:
// src/steps/get-order.step.ts
import { defineStep } from '@motiadev/core';
export default defineStep({
type: 'api',
path: '/orders/:orderId',
method: 'GET',
async handler({ params, state }) {
const order = await state.get(`order:${params.orderId}`);
const risk = await state.get(`order:${params.orderId}:risk`);
if (!order) {
return { error: 'Order not found', status: 404 };
}
return {
...order,
riskAnalysis: risk || { status: 'pending' },
};
},
});
No database queries, no joins — just reads from the unified state store that every Step shares.
Motia's Event-Driven Workflow vs Traditional Approaches
| Design pattern | Traditional | Motia |
|---|---|---|
| HTTP handler + async job | Express route → Bull queue → worker | API Step → emit topic → Event Step |
| Shared state | Manual Redis client in every service | Unified state store, auto-traced |
| Parallel processing | Promise.all or manual fan-out | Multiple Steps subscribing same topic |
| AI integration | Separate Python service + HTTP call | Python Step in same app |
| Observability | OpenTelemetry setup + Grafana | Built-in Workbench traces |
| Error handling | Retry logic in each worker | Error emission as first-class events |
Advanced Pattern: Saga with Compensating Events
Motia's event model maps naturally to the Saga pattern for distributed transactions:
// src/steps/compensate-failed-payment.step.ts
import { defineStep } from '@motiadev/core';
export default defineStep({
type: 'event',
subscribes: ['payment.failed'],
async handler({ data, emit, state }) {
const order = await state.get(`order:${data.orderId}`);
// Compensating action — release any reserved inventory
await emit('inventory.release', {
orderId: data.orderId,
items: order.items,
});
// Notify customer
await emit('notification.send', {
customerId: order.customerId,
type: 'payment_failed',
orderId: data.orderId,
});
},
});
Each step in a failed workflow emits compensating events rather than rolling back directly. The result is a fully observable, auditable saga without saga orchestrator overhead.
Scheduled API Health Checks (Cron Step)
// src/steps/api-health-check.step.ts
import { defineStep } from '@motiadev/core';
export default defineStep({
type: 'cron',
expression: '*/5 * * * *', // Every 5 minutes
async handler({ emit, state }) {
const endpoints = await state.get('monitored-endpoints') || [];
const results = await Promise.all(
endpoints.map(async (endpoint) => {
const start = Date.now();
try {
const res = await fetch(endpoint.url, { signal: AbortSignal.timeout(5000) });
return {
url: endpoint.url,
status: res.status,
latency: Date.now() - start,
healthy: res.ok,
};
} catch (err) {
return { url: endpoint.url, healthy: false, error: err.message };
}
})
);
const unhealthy = results.filter((r) => !r.healthy);
if (unhealthy.length > 0) {
await emit('api.degraded', { endpoints: unhealthy, timestamp: Date.now() });
}
await state.set('health-check-results', { results, checkedAt: Date.now() });
},
});
This cron Step runs API health checks every 5 minutes and emits api.degraded when endpoints are unhealthy — triggering whatever alerting and remediation Steps you've connected to that topic.
Recommendations for API Designers
Design for events, not responses: Motia pushes you to think about what happens after the response — not just what you return. This is a better mental model for any non-trivial backend.
Use topics as contracts: Your Step's emitted topics are its public API contract. Other Steps subscribe to them. Changing a topic name is a breaking change.
Lean on parallel fanout: When multiple things need to happen after an event (risk analysis + payment + notification), subscribe multiple Steps to the same topic. Motia handles the concurrency.
Python for AI, TypeScript for APIs: The multi-language support isn't a gimmick — keeping AI logic in Python (where the ecosystem is mature) while writing HTTP handlers in TypeScript (where the tooling is excellent) is a genuinely powerful pattern.
Methodology
- Sources: Motia official docs (motia.dev), Motia GitHub (MotiaDev/motia), JS Rising Stars 2025, ThinkThroo Motia analysis, Medium engineering breakdown
- Date: March 2026
Understanding the spec layer on top of workflows? See Arazzo 1.0: Multi-Step API Workflows Spec 2026 — how to document Motia-style workflows in machine-readable YAML.
Real-time APIs alongside event workflows: SSE vs WebSockets vs Long Polling 2026.
API authentication for Motia endpoints: API Authentication Guide: Keys, OAuth & JWT 2026.