> ## Documentation Index
> Fetch the complete documentation index at: https://motiadev-docs-deployment-guide.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating from Motia (Node.js)

> How to move from the Motia framework to iii-sdk directly in Node.js.

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](/0-11-0/how-to/worker-rbac) for secure, real-time frontends — no REST layer needed. See [Use iii in the browser](/0-11-0/how-to/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.

<Callout type="tip">Before diving into this migration, we recommend reading the [iii documentation](/0-11-0/) to understand Workers, Functions, and Triggers. The [quickstart](/0-11-0/quickstart) and [Everything is a Worker](./everything-is-a-worker) pages are good starting points.</Callout>

***

## Step 1 — Update dependencies

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

```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
pnpm remove motia
pnpm add iii-sdk@0.11.0
pnpm add -D esbuild
```

| Before (Motia)                       | After (iii-sdk)                           |
| ------------------------------------ | ----------------------------------------- |
| `motia`                              | `iii-sdk@0.11.0`                          |
| `motia build`                        | `esbuild` via `bun run esbuild.config.ts` |
| `motia dev && bun run dist/index.js` | `bun run --watch src/main.ts`             |

***

## Step 2 — Initialize the SDK

Create `src/lib/iii.ts`. This replaces the implicit connection that Motia managed for you.

```typescript title="@/lib/iii" theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
import { Logger, registerWorker } from 'iii-sdk'

export const iii = registerWorker(process.env.III_URL ?? 'ws://localhost:49134', {
  workerName: 'api-worker',
})

export const logger = new Logger()
```

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

<Tabs>
  <Tab title="Motia">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { type Handlers, http, type StepConfig } from 'motia'
    import { z } from 'zod'

    export const config = {
      name: 'Health',
      triggers: [http('GET', '/health', { responseSchema: { 200: z.object({ ok: z.boolean() }) } })],
      enqueues: [],
    } as const satisfies StepConfig

    export const handler: Handlers<typeof config> = async (_input, _ctx) => {
      return { status: 200, body: { ok: true } }
    }
    ```
  </Tab>

  <Tab title="iii-sdk">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    const ref = iii.registerFunction('health', async () => {
      return { status_code: 200, body: { ok: true } }
    })

    iii.registerTrigger({
      type: 'http',
      function_id: ref.id,
      config: { api_path: '/health', http_method: 'GET' },
    })
    ```
  </Tab>
</Tabs>

### HTTP with middleware

<Tabs>
  <Tab title="Motia">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { type Handlers, http, type StepConfig } from 'motia'

    export const config = {
      name: 'List Tags',
      triggers: [http('GET', '/tag', { middleware: [requireAuth] })],
      enqueues: [],
    } as const satisfies StepConfig

    export const handler: Handlers<typeof config> = async (input, ctx) => {
      const { sub: userId, orgId } = input.tokenInfo
      return { status: 200, body: { tags: allTags } }
    }
    ```
  </Tab>

  <Tab title="iii-sdk">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    const ref = iii.registerFunction('list-tags', async (input) => {
      const { sub: userId, orgId } = input.tokenInfo
      return { status_code: 200, body: { tags: allTags } }
    })

    iii.registerTrigger({
      type: 'http',
      function_id: ref.id,
      config: {
        api_path: '/tag',
        http_method: 'GET',
        middleware_function_ids: ['middleware::auth'],
      },
    })
    ```

    <Callout type="info">Middleware functions must be registered separately with `registerFunction`. See [Use HTTP middleware](/0-11-0/how-to/use-http-middleware) for details.</Callout>
  </Tab>
</Tabs>

### Cron

<Tabs>
  <Tab title="Motia">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { cron, type Handlers, type StepConfig } from 'motia'

    export const config = {
      name: 'Daily Report',
      triggers: [cron('0 0 9 * * * *')],
      enqueues: [],
    } as const satisfies StepConfig

    export const handler: Handlers<typeof config> = async (_input, _ctx) => {
      await generateReport()
    }
    ```
  </Tab>

  <Tab title="iii-sdk">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    const ref = iii.registerFunction('daily-report', async () => {
      await generateReport()
    })

    iii.registerTrigger({
      type: 'cron',
      function_id: ref.id,
      config: { expression: '0 0 9 * * * *' },
    })
    ```
  </Tab>
</Tabs>

### Queue (durable subscriber)

<Tabs>
  <Tab title="Motia">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { type Handlers, queue, type StepConfig } from 'motia'

    export const config = {
      name: 'Process Order',
      triggers: [queue('order.created')],
      enqueues: [],
    } as const satisfies StepConfig

    export const handler: Handlers<typeof config> = async (input, _ctx) => {
      await processOrder(input)
    }
    ```
  </Tab>

  <Tab title="iii-sdk">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    const ref = iii.registerFunction('process-order', async (input) => {
      await processOrder(input)
    })

    iii.registerTrigger({
      type: 'durable:subscriber',
      function_id: ref.id,
      config: { topic: 'order.created' },
    })
    ```
  </Tab>
</Tabs>

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

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
import { iii } from '@/lib/iii'

iii.trigger({
  function_id: 'iii::durable::publish',
  payload: { topic: 'order.created', data: orderPayload },
})
```

### State

<Tabs>
  <Tab title="Motia">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { type Handlers, state, type StepConfig } from 'motia'

    export const config = {
      name: 'On State Change',
      triggers: [state()],
      enqueues: [],
    } as const satisfies StepConfig

    export const handler: Handlers<typeof config> = async (input, _ctx) => {
      await handleStateChange(input)
    }
    ```
  </Tab>

  <Tab title="iii-sdk">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    const ref = iii.registerFunction('on-state-change', async (input) => {
      await handleStateChange(input)
    })

    iii.registerTrigger({
      type: 'state',
      function_id: ref.id,
      config: {},
    })
    ```
  </Tab>
</Tabs>

### Stream

<Tabs>
  <Tab title="Motia">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { type Handlers, stream, type StepConfig } from 'motia'

    export const config = {
      name: 'Chat Message',
      triggers: [stream('chat', { groupId: 'room-1' })],
      enqueues: [],
    } as const satisfies StepConfig

    export const handler: Handlers<typeof config> = async (input, _ctx) => {
      await handleChatMessage(input)
    }
    ```
  </Tab>

  <Tab title="iii-sdk">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    const ref = iii.registerFunction('chat-message', async (input) => {
      await handleChatMessage(input)
    })

    iii.registerTrigger({
      type: 'stream',
      function_id: ref.id,
      config: { stream_name: 'chat', group_id: 'room-1' },
    })
    ```
  </Tab>
</Tabs>

### 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`:

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
const ref = iii.registerFunction('order-handler', async (input) => {
  await handleOrder(input)
})

iii.registerTrigger({
  type: 'http',
  function_id: ref.id,
  config: { api_path: '/orders', http_method: 'POST' },
})

iii.registerTrigger({
  type: 'durable:subscriber',
  function_id: ref.id,
  config: { topic: 'order.retry' },
})
```

### 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](/0-11-0/workers/iii-stream) and [State](/0-11-0/workers/iii-state) worker docs for the full list of operations.

<Tabs>
  <Tab title="Stream">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    const todo = await iii.trigger({
      function_id: 'stream::get',
      payload: { stream_name: 'todos', group_id: 'user-1', item_id: 'todo-1' },
    })

    await iii.trigger({
      function_id: 'stream::set',
      payload: { stream_name: 'todos', group_id: 'user-1', item_id: 'todo-1', data: { title: 'Buy milk' } },
    })

    await iii.trigger({
      function_id: 'stream::delete',
      payload: { stream_name: 'todos', group_id: 'user-1', item_id: 'todo-1' },
    })

    const items = await iii.trigger({
      function_id: 'stream::list',
      payload: { stream_name: 'todos', group_id: 'user-1' },
    })
    ```
  </Tab>

  <Tab title="State">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    const theme = await iii.trigger({
      function_id: 'state::get',
      payload: { scope: 'settings', key: 'theme' },
    })

    await iii.trigger({
      function_id: 'state::set',
      payload: { scope: 'settings', key: 'theme', value: 'dark' },
    })

    await iii.trigger({
      function_id: 'state::delete',
      payload: { scope: 'settings', key: 'theme' },
    })

    const allSettings = await iii.trigger({
      function_id: 'state::list',
      payload: { scope: 'settings' },
    })
    ```
  </Tab>
</Tabs>

<Callout type="info">Additional function IDs: `stream::update`, `stream::list_groups`, `stream::send` for streams; `state::update`, `state::list_groups` for state.</Callout>

### 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 }`.

<Tabs>
  <Tab title="Motia">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { type StreamConfig } from 'motia'

    export const config: StreamConfig = {
      name: 'todo',
      schema: todoSchema,
      baseConfig: { storageType: 'default' },

      onJoin: async (subscription, _context, authContext) => {
        return { unauthorized: false }
      },

      onLeave: async (subscription, _context, authContext) => {
        // cleanup logic
      },
    }
    ```
  </Tab>

  <Tab title="iii-sdk">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    iii.registerFunction('stream::on-join', async (event) => {
      const { stream_name, group_id, id, context } = event
      return { unauthorized: false }
    })

    iii.registerTrigger({
      type: 'stream:join',
      function_id: 'stream::on-join',
      config: {},
    })

    iii.registerFunction('stream::on-leave', async (event) => {
      const { stream_name, group_id, id, context } = event
      // cleanup logic
    })

    iii.registerTrigger({
      type: 'stream:leave',
      function_id: 'stream::on-leave',
      config: {},
    })
    ```
  </Tab>
</Tabs>

<Callout type="info">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.</Callout>

### 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.

<Tabs>
  <Tab title="Motia">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    // motia.config.ts
    import type { AuthenticateStream } from 'motia'

    export const authenticateStream: AuthenticateStream = async (req, context) => {
      return { context: { userId: 'sergio' } }
    }
    ```
  </Tab>

  <Tab title="iii-sdk">
    ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    import { iii } from '@/lib/iii'

    iii.registerFunction('stream::authenticate', async (input) => {
      const { headers, path, query_params, addr } = input
      return { context: { userId: 'sergio' } }
    })
    ```
  </Tab>
</Tabs>

The engine calls this function during WebSocket upgrade. Reference the function ID in your `config.yaml`:

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
workers:
  - name: iii-stream
    config:
      auth_function: stream::authenticate
```

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.

| Property        | Motia (`input.*`) | iii-sdk (`input.*`) |
| --------------- | ----------------- | ------------------- |
| Route params    | `params`          | `path_params`       |
| Query string    | `query`           | `query_params`      |
| JSON body       | `body`            | `body`              |
| Headers         | `headers`         | `headers`           |
| HTTP method     | `method`          | `method`            |
| Request stream  | `requestBody`     | `request_body`      |
| Response stream | `response`        | `response`          |

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
// Before (Motia)
const { folderId } = input.params || {}
const parentFolderId = input.query?.parentFolderId

// After (iii-sdk)
const { folderId } = input.path_params || {}
const parentFolderId = input.query_params?.parentFolderId
```

### `query_params` value types

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

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
function firstQueryParam(
  queryParams: Record<string, string | string[]> | undefined,
  name: string,
): string | undefined {
  const v = queryParams?.[name]
  if (v === undefined) return undefined
  return Array.isArray(v) ? v[0] : v
}
```

### Response shape

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

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
// Before (Motia)
return { status: 200, body: { tags: allTags } }
return { status: 404, body: { error: 'Not found' } }

// After (iii-sdk)
return { status_code: 200, body: { tags: allTags } }
return { status_code: 404, body: { error: 'Not found' } }
```

***

## 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`:

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
import './lib/iii'
import './handlers/health'
import './handlers/auth'
import './handlers/orders/create-order'
import './handlers/orders/list-orders'
import './handlers/reports/daily-report'
// ... import every handler file
```

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`:

```
src/steps/health.step.ts  →  src/handlers/health.ts
src/steps/auth.step.ts    →  src/handlers/auth.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`:

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
workers:
  - name: iii-exec
    config:
      exec:
        - bun run --watch src/main.ts
```

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`:

```json theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
{
  "scripts": {
    "build": "bun run esbuild.config.ts"
  }
}
```

Create `esbuild.config.ts`:

```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
import * as esbuild from 'esbuild'

esbuild.build({
  entryPoints: ['src/main.ts'],
  outfile: 'dist/index-production.js',
  bundle: true,
  platform: 'node',
  target: ['node22'],
  format: 'esm',
  minify: true,
  sourcemap: true,
  external: ['ws'],
}).catch((err) => {
  console.error(err)
  process.exit(1)
})
```

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:

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
workers:
  - name: iii-exec
    config:
      exec: 
        - bun run --enable-source-maps dist/index-production.js
```

***

## 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
