Skip to main content

Motia API Workflows: Steps & Events 2026

·APIScout Team
Share:

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 patternTraditionalMotia
HTTP handler + async jobExpress route → Bull queue → workerAPI Step → emit topic → Event Step
Shared stateManual Redis client in every serviceUnified state store, auto-traced
Parallel processingPromise.all or manual fan-outMultiple Steps subscribing same topic
AI integrationSeparate Python service + HTTP callPython Step in same app
ObservabilityOpenTelemetry setup + GrafanaBuilt-in Workbench traces
Error handlingRetry logic in each workerError 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.

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.