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-sdkwith 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, andregister_workerbuilds 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
Removemotia and add iii-sdk.
| Before (Motia) | After (iii-sdk) |
|---|---|
motia (pip) | iii-sdk==0.11.0 (pip) |
motia build | python -m build or a container image |
motia dev | python -m src.main with a file watcher |
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
Createsrc/lib/iii_client.py. This replaces the implicit connection that Motia managed for you.
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 exportedconfig 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
- Motia
- iii-sdk
HTTP with middleware
- Motia
- iii-sdk
iii-sdk Python uses engine-native middleware registered as regular functions with a middleware:: prefix. See Use HTTP middleware for the full pattern.Cron
- Motia
- iii-sdk
Queue (durable subscriber)
- Motia
- iii-sdk
iii.trigger instead of Motia’s enqueue:
State
- Motia
- iii-sdk
Stream
- Motia
- iii-sdk
Multiple triggers on one function
A function can be driven by multiple triggers. This was possible in Motia via thetriggers array, and maps directly to multiple register_trigger calls for the same function_id:
Stream and State operations
In Motia, you used theStream 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.
- Stream
- State
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.
- Motia
- iii-sdk
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.
- Motia
- iii-sdk
config.yaml:
- Motia used
queryParams(camelCase); iii-sdk usesquery_params(snake_case). - No trigger registration is needed for auth — it is config-driven via
auth_function. - The
motia_config.pyfile is no longer needed; delete it.
Step 5 — Update request and response shapes
Request properties
Motia wrappediii-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 |
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.
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:
handlers/, routes/, rest/, or any layout that fits your project.
Step 7 — Development workflow
Theiii-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:
watch glob restarts the exec pipeline.
Step 8 — Production build
Python has no directesbuild equivalent. The recommended production path is a virtualenv (or container image) with pinned dependencies.
Add to pyproject.toml:
Dockerfile:
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
motiafromrequirements.txt/pyproject.toml, addiii-sdk==0.11.0 - Create
src/lib/iii_client.pywithregister_workerandLogger - Rename all
*_step.pyfiles to drop the_stepsuffix - Replace all
from motia import ...withfrom iii import ...(andfrom src.lib.iii_client import ...foriii/logger) - Convert every
configdict +handlerfunction toiii.register_function(...)+iii.register_trigger(...)calls - Replace
ApiResponse(status=...)with plain dicts usingstatusCode(e.g.,{"statusCode": 200, "body": ...}) - Replace
request.path_params/request.query_paramsattribute access with dict-key access on the raw payload (data["path_params"],data.get("query_params")) - Replace
requestBodywithrequest_bodyin streaming handlers - Replace
StreamandstateManagerusage withiii.trigger()calls or custom wrappers - Migrate stream
on_join/on_leavehooks toregister_function+register_trigger(stream:join/stream:leave) - Migrate
authenticate_streamfrommotia_config.pyto a registered function and setauth_functioninconfig.yaml - Delete
motia_config.py - Create
src/main.pywith side-effect imports for every handler module - Replace
motia devwith aniii-execworker usingwatch+execin yourconfig.yaml - Replace
motia buildwithpip install .(and aDockerfilefor containerized deploys) - Test all endpoints, cron jobs, queue subscribers, and stream handlers