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.
Testing Motia Workflows
Motia's event-driven model makes testing more structured than testing traditional Express routes. Each Step is independently testable because it has a clear interface: it receives data, performs side effects, and emits events to topics.
Unit testing individual Steps: test the Step handler function directly by mocking the emit, state, and ctx arguments:
// test/create-order.test.ts
import { describe, it, expect, vi } from 'vitest';
// Import the handler function directly (not the full Step definition)
const handler = async ({ body, emit, state, ctx }) => {
const orderId = 'test-order-id';
await state.set(`order:${orderId}`, { ...body, orderId, status: 'pending' });
await emit('order.created', { orderId, customerId: body.customerId });
return { orderId, status: 'pending' };
};
describe('create-order step', () => {
it('emits order.created with orderId', async () => {
const emit = vi.fn();
const state = { set: vi.fn(), get: vi.fn() };
await handler({
body: { customerId: 'cust-123', items: [{ productId: 'prod-1', quantity: 2 }] },
emit,
state,
ctx: { traceId: 'trace-001' },
});
expect(emit).toHaveBeenCalledWith('order.created', {
orderId: 'test-order-id',
customerId: 'cust-123',
});
expect(state.set).toHaveBeenCalled();
});
});
Integration testing the full workflow: Motia's test utilities let you spin up the full application, trigger an API Step, and assert the events emitted by downstream Steps. Use the Motia Workbench's built-in trace capture for integration tests — trigger an event, wait for the workflow to complete, and assert the final state:
// test/order-workflow.integration.test.ts
import { MotiaTestClient } from '@motiadev/testing';
const client = new MotiaTestClient({ baseUrl: 'http://localhost:3000' });
it('processes an order end-to-end', async () => {
const { orderId } = await client.post('/orders', {
customerId: 'cust-123',
items: [{ productId: 'prod-1', quantity: 1 }],
});
// Wait for async workflow to complete
await client.waitForTopic('payment.succeeded', { orderId }, { timeout: 5000 });
const order = await client.get(`/orders/${orderId}`);
expect(order.status).toBe('paid');
});
The key testing insight: because Motia topics are explicit named contracts, you can assert on what was emitted to each topic rather than relying on observable side effects. Failed payment flows, saga compensations, and AI risk analysis results are all observable via topic emissions.
Production Deployment
Motia applications deploy like standard Node.js servers — the event loop and topic routing run in-process, not as external message broker infrastructure. This simplicity is a key feature for teams that want event-driven architecture without Kafka or RabbitMQ overhead.
Docker deployment:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main.js"]
For persistent state in production, Motia's default in-memory state store is replaced with Redis or Postgres:
// motia.config.ts
import { defineConfig } from '@motiadev/core';
export default defineConfig({
state: {
adapter: 'redis',
url: process.env.REDIS_URL,
},
});
Railway, Fly.io, and Render all work out of the box — Motia's architecture is identical to any stateless Node.js server for the HTTP layer, with Redis providing the shared state. The topic-routing engine runs in the application process, so there's no separate message broker to manage in simple deployments. For high-throughput production workloads (thousands of events/second), Motia supports pluggable brokers (Kafka, RabbitMQ) as the transport for topic messages while keeping the same Step API.
Observability: Motia ships with a Workbench UI (accessible at /workbench in development) that shows every workflow trace, Step execution, and topic emission in real time. In production, forward traces to Datadog or OpenTelemetry — Motia exports standard OTEL spans so your existing observability stack works without changes.
When to Use Motia vs Alternatives
Motia's event-driven model has a real learning curve — the mental shift from "write a function, it runs and returns" to "write a Step, it emits events, other Steps react" takes time. When does that investment pay off?
Motia is the right choice when your backend workflows have more than 2 sequential steps, involve parallel processing that currently requires Promise.all boilerplate, mix AI workloads (Python) with HTTP logic (TypeScript), need end-to-end traceability for debugging production issues, or will grow with new steps as the product evolves.
Traditional Express + queue (BullMQ, Inngest, Trigger.dev) remains better when you need mature ecosystem tooling and existing team familiarity outweighs architecture benefits, your async jobs are simple and well-defined (one function, one job), you need fine-grained queue control (priorities, delay, batch processing), or your team is small and the cognitive overhead of a new paradigm costs more than it saves.
Inngest and Trigger.dev are the closest alternatives in the TypeScript async workflow space. Both take a function-as-unit-of-work model rather than Motia's topic-subscription model. The difference: Inngest/Trigger.dev workflows are defined as connected function chains; Motia workflows emerge from Steps subscribing to topics. Motia's decoupling means steps can be added, removed, or resubscribed without modifying other steps — better for evolving systems but harder to visualize the full flow.
Methodology
Sources: Motia official documentation (motia.dev), Motia GitHub repository (MotiaDev/motia v1.x), JavaScript Rising Stars 2025 report, ThinkThroo analysis (January 2026). Motia version referenced: 1.x stable as of March 2026. Code examples use the @motiadev/core TypeScript API; the Python motia package mirrors the same primitives. The multi-language (TypeScript + Python coexistence) feature requires the Motia runtime v1.2+. State adapter support for Redis and Postgres is production-stable; Kafka and RabbitMQ transport adapters are in public beta as of March 2026. The @motiadev/testing package for integration testing is in active development — check the Motia changelog for stability updates before pinning it in production CI. JavaScript Rising Stars 2025 ranking was for the backend frameworks category, not overall JavaScript projects; Motia ranked first in that category ahead of Hono, ElysiaJS, and other newer frameworks.
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.