Skip to main content

Motia API Workflows: Steps & Events 2026

·APIScout Team
motiaapi-designevent-drivenworkflowstypescriptai-agents
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.

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.

Comments

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.