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 packaging 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 register_function, register_trigger, and register_worker 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.
For the Node / TypeScript migration, see Migrating from Motia (Node.js).

Step 1 — Update dependencies

Remove motia and add iii-sdk.
The Python package is distributed as iii-sdk on PyPI but imported as iii (e.g., from iii import register_worker).

Step 2 — Initialize the SDK

Create src/lib/iii_client.py. This replaces the implicit connection that Motia managed for you.
Every from motia import logger in your codebase changes to from src.lib.iii_client import logger.

Step 3 — Register functions and triggers directly

Motia auto-registered functions and triggers from exported config objects. With iii-sdk Python, you call iii.register_function and iii.register_trigger directly at module scope — there’s no helper to build. Each handler module registers its function and trigger(s) when imported. This mirrors the Motia config + handler structure without the implicit discovery. Continue to Step 4 to see the handler patterns.
iii-sdk Python supports engine-native HTTP middleware via middleware_function_ids. See Use HTTP middleware for the full pattern.

Step 4 — Migrate handlers

HTTP

HTTP with middleware

iii-sdk Python uses engine-native middleware registered as regular functions with a middleware:: prefix. See Use HTTP middleware for the full pattern.

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 be driven by multiple triggers. This was possible in Motia via the triggers array, and maps directly to multiple register_trigger calls for the same function_id:

Stream and State operations

In Motia, you used the Stream class and stateManager singleton 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 files could define onJoin and onLeave callbacks inside the stream config. 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 dict with stream_name, group_id, id, and 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.py (or motia.config.ts for JS projects) exported an authentication 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.py file is no longer needed; delete it.

Step 5 — 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. Motia Python’s ApiRequest already exposed path_params, query_params, body, headers, method. When switching to iii-sdk, handlers receive those fields in a plain dict instead of a Pydantic model, so attribute access (request.path_params) becomes dict-key access (data["path_params"]).

query_params value types

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

Response shape

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

Step 6 — Set up the entry point

Motia discovered *_step.py files automatically. With iii-sdk Python, create src/main.py that imports every handler module as a side effect.
Each import executes the register_function and register_trigger calls at the module level, registering the function and its triggers with the engine.

File renaming

Rename all *_step.py files to remove the _step suffix:
The directory structure is yours to decide — handlers/, routes/, rest/, or any layout that fits your project.

Step 7 — Development workflow

The iii-exec worker has a built-in file watcher. In your config.yaml, declare watch globs alongside the exec command — no extra dependencies, no wrapper process:
Any change matching a watch glob restarts the exec pipeline.

Step 8 — Production build

Python has no direct esbuild equivalent. The recommended production path is a virtualenv (or container image) with pinned dependencies. Add to pyproject.toml:
Build and run:
For containerized deploys, a minimal Dockerfile:
Update your production config to run the installed module (omit watch in production):
Unlike esbuild, you do not bundle Python sources — the runtime imports modules directly. Ship your source tree (or a wheel) rather than a single file.

Migration checklist

  • Remove motia from requirements.txt / pyproject.toml, add iii-sdk==0.11.0
  • Create src/lib/iii_client.py with register_worker and Logger
  • Rename all *_step.py files to drop the _step suffix
  • Replace all from motia import ... with from iii import ... (and from src.lib.iii_client import ... for iii / logger)
  • Convert every config dict + handler function to iii.register_function(...) + iii.register_trigger(...) calls
  • Replace ApiResponse(status=...) with plain dicts using statusCode (e.g., {"statusCode": 200, "body": ...})
  • Replace request.path_params / request.query_params attribute access with dict-key access on the raw payload (data["path_params"], data.get("query_params"))
  • Replace requestBody with request_body in streaming handlers
  • Replace Stream and stateManager usage with iii.trigger() calls or custom wrappers
  • Migrate stream on_join / on_leave hooks to register_function + register_trigger (stream:join / stream:leave)
  • Migrate authenticate_stream from motia_config.py to a registered function and set auth_function in config.yaml
  • Delete motia_config.py
  • Create src/main.py with side-effect imports for every handler module
  • Replace motia dev with an iii-exec worker using watch + exec in your config.yaml
  • Replace motia build with pip install . (and a Dockerfile for containerized deploys)
  • Test all endpoints, cron jobs, queue subscribers, and stream handlers