Skip to main content
Motia was a higher-level framework built on top of iii-sdk. It handled file scanning, middleware wiring, trigger registration, and production bundling automatically. These conveniences came at a cost: they hid iii’s three core primitives — Workers, Functions, and Triggers — behind opaque abstractions that limited what you could build. By moving to iii-sdk directly, you unlock the full power of the engine:
  • Add new workers that register their own functions and triggers, enabling multi-worker orchestration across services, languages, and runtimes.
  • Connect from the browser using iii-browser-sdk with Worker RBAC for secure, real-time frontends — no REST layer needed. See Use iii in the browser.
  • Treat Motia as one worker among many instead of a standalone monolith. Your existing Motia code becomes just another worker in a larger iii deployment.
  • Understand the primitives directly. Working with registerFunction, registerTrigger, and registerWorker builds a mental model that transfers across all iii SDKs and documentation.
Before diving into this migration, we recommend reading the iii documentation to understand Workers, Functions, and Triggers. The quickstart and Everything is a Worker pages are good starting points.

Step 1 — Update dependencies

Remove motia and add iii-sdk. If you need a production bundler, add esbuild as a dev dependency.

Step 2 — Initialize the SDK

Create src/lib/iii.ts. This replaces the implicit connection that Motia managed for you.
@/lib/iii
Every import { logger } from 'motia' in your codebase changes to import { logger } from '@/lib/iii'.

Step 3 — Migrate handlers

Motia auto-registered functions and triggers from exported config objects. With iii-sdk you call registerFunction and registerTrigger directly.

HTTP

HTTP with middleware

Cron

Queue (durable subscriber)

To publish to a topic, use iii.trigger instead of Motia’s enqueue:

State

Stream

Multiple triggers on one function

A function can have multiple triggers. This was possible in Motia via the triggers array, and maps directly to multiple registerTrigger calls sharing the same function_id:

Stream and State operations

In Motia, you used the Stream class and stateManager to read and write data. Under the hood, these called iii.trigger() with built-in function IDs (stream::get, state::set, etc.). With iii-sdk, you call iii.trigger() directly. See the Stream and State worker docs for the full list of operations.
Additional function IDs: stream::update, stream::list_groups, stream::send for streams; state::update, state::list_groups for state.

Stream lifecycle hooks (onJoin / onLeave)

In Motia, *.stream.ts files could define onJoin and onLeave callbacks inside the StreamConfig. The framework registered a single shared function for each hook and dispatched by stream name internally. With iii-sdk, you register these as regular functions with stream:join and stream:leave triggers. The handler receives a StreamJoinLeaveEvent with { stream_name, group_id, id, context }.
If you had multiple streams with different onJoin / onLeave logic, dispatch by event.stream_name inside the handler or register separate function IDs per stream.

Stream authentication (authenticateStream)

In Motia, motia.config.ts exported an authenticateStream function that ran during WebSocket upgrade. With iii-sdk, you register a function directly and reference its ID in the engine’s stream module config.
The engine calls this function during WebSocket upgrade. Reference the function ID in your config.yaml:
Key differences:
  • Motia used queryParams (camelCase); iii-sdk uses query_params (snake_case).
  • No trigger registration is needed for auth — it is config-driven via auth_function.
  • The motia.config.ts file is no longer needed; delete it.

Step 4 — Update request and response shapes

Request properties

Motia wrapped iii-sdk’s HTTP request into friendlier property names. With iii-sdk you receive the raw shape.

query_params value types

In iii-sdk, query_params values are string | string[]. Use a helper to safely extract the first value:

Response shape

iii-sdk uses status_code instead of status. This applies to every handler return and middleware response.

Step 5 — Set up the entry point

Motia discovered .step.ts files automatically. With iii-sdk, you create a single entry point that imports all handler files as side effects. Create src/main.ts:
Each import executes the registerFunction and registerTrigger calls at the module level, registering the function and its triggers with the engine.

File renaming

Rename all *.step.ts files to *.ts:
The directory structure is yours to decide — handlers/, routes/, rest/, or any layout that fits your project.

Step 6 — Development workflow

Replace Motia’s dev command with a direct bun watcher. In your config.yaml, configure the worker under iii-exec:
Run the engine, and it will start your worker process with file watching built in.

Step 7 — Production build

Motia had motia build. Replace it with a plain esbuild config. Add to package.json:
Create esbuild.config.ts:
Key decisions:
  • bundle: true — all application code and npm dependencies are bundled into a single file.
  • external: ['ws'] — the ws package has native addons and must remain in node_modules at runtime.
  • sourcemap: true — run production with bun run --enable-source-maps dist/index-production.js so stack traces map back to TypeScript source.
  • platform: 'node' — Node.js built-ins (fs, path, crypto, etc.) are treated as external.
  • format: 'esm' — matches "type": "module" in your package.json.
Update your production config to run the bundled output:

Migration checklist

  • Remove motia from dependencies, add iii-sdk@0.11.0
  • Add esbuild as a dev dependency
  • Create src/lib/iii.ts with registerWorker and Logger
  • Rename all *.step.ts files to *.ts
  • Replace all import { ... } from 'motia' with iii-sdk imports
  • Convert every config + handler export to registerFunction + registerTrigger calls
  • Replace status with status_code in every handler return
  • Replace params with path_params and query with query_params
  • Replace requestBody with request_body in streaming handlers
  • Replace Stream and stateManager usage with iii.trigger() calls or custom wrappers
  • Migrate stream onJoin / onLeave hooks to registerFunction + registerTrigger (stream:join / stream:leave)
  • Migrate authenticateStream from motia.config.ts to a registered function and set auth_function in config.yaml
  • Delete motia.config.ts
  • Create src/main.ts with side-effect imports for every handler
  • Replace motia dev with bun run --watch src/main.ts in your dev config
  • Replace motia build with an esbuild config
  • Test all endpoints, cron jobs, queue subscribers, and stream handlers