Skip to content

bbdev Motia v0.17.x to 1.0-RC Migration #1

@shirohasuki

Description

@shirohasuki

Current State Summary

The bbdev project is a Python-only Motia application at bbdev/api/ with:

  • 28 step files across 7 flows: verilator, compiler, firesim, marshal, sardine, palladium, workload
  • Every flow follows the same API + Event step pair pattern:
    • API step (*_api_step.py): receives HTTP POST, calls context.emit(), then polls wait_for_result() in a loop
    • Event step (*_event_step.py): subscribes to a topic, runs a shell command via stream_run_logger(), writes result to state via check_result()
  • 1 TypeScript config file: motia.config.ts (uses defineConfig with plugins)
  • Dependencies: motia@0.17.14-beta.196 + 8 @motiadev/* plugin packages
  • Utility modules: event_common.py, stream_run.py, path.py, port.py, search_workload.py
  • Services layer: services/ directory with tool classes (not Motia steps, just helper code)

Migration Plan

Phase 1: Project Infrastructure

Task Details
Create config.yaml Define modules: RestApiModule, StateModule, QueueModule, PubSubModule, ExecModule (Python-only, using uv run motia dev --dir steps)
Create pyproject.toml Add motia[otel], iii-sdk, pydantic>=2.0 as dependencies
Handle motia.config.ts No stream auth is used — delete entirely
Handle package.json Python-only project — delete (along with pnpm-lock.yaml, pnpm-workspace.yaml)
Delete old artifacts Remove .motia/ directory, types.d.ts
Install iii engine From https://iii.dev

Phase 2: Migrate 14 API Steps (HTTP triggers)

All API steps follow an identical pattern. The transformation is mechanical:

Before:

config = {
    "type": "api",
    "name": "Verilator Clean",
    "path": "/verilator/clean",
    "method": "POST",
    "emits": ["verilator.clean"],
    "flows": ["verilator"],
}

async def handler(req, context):
    body = req.get("body") or {}
    await context.emit({"topic": "verilator.clean", "data": {...}})
    while True:
        result = await wait_for_result(context)
        if result is not None:
            return result
        await asyncio.sleep(1)

After:

from motia import ApiRequest, ApiResponse, FlowContext, http

config = {
    "name": "Verilator Clean",
    "description": "clean build directory",
    "flows": ["verilator"],
    "triggers": [http("POST", "/verilator/clean")],
    "enqueues": ["verilator.clean"],
}

async def handler(request: ApiRequest, ctx: FlowContext) -> ApiResponse:
    body = request.body or {}
    await ctx.enqueue({"topic": "verilator.clean", "data": {...}})
    while True:
        result = await wait_for_result(ctx)
        if result is not None:
            return ApiResponse(status=result["status"], body=result["body"])
        await asyncio.sleep(1)

Key changes:

  • "type": "api" removed, "method" + "path" move into http() trigger
  • "emits" becomes "enqueues"
  • context.emit() becomes ctx.enqueue()
  • req.get("body") becomes request.body
  • Return dict becomes ApiResponse(status=..., body=...)

Files (14 total):

  • verilator/: 01_clean_api, 02_verilog_api, 03_build_api, 04_sim_api, 04_cosim_api, 05_run_api
  • compiler/01_build_api
  • firesim/: 01_buildbitstream_api, 02_infrasetup_api, 03_runworkload_api
  • marshal/: 01_build_api, 02_launch_api
  • sardine/01_run_api
  • palladium/01_verilog_api
  • workload/01_buidl_api

Phase 3: Migrate 14 Event Steps (Queue triggers)

Also mechanical:

Before:

config = {
    "type": "event",
    "name": "make clean",
    "subscribes": ["verilator.run", "verilator.clean"],
    "emits": ["verilator.verilog"],
    "flows": ["verilator"],
}

async def handler(data, context):
    ...
    await context.emit({"topic": "verilator.verilog", "data": {...}})

After:

from motia import FlowContext, queue

config = {
    "name": "make clean",
    "description": "clean build directory",
    "flows": ["verilator"],
    "triggers": [
        queue("verilator.run"),
        queue("verilator.clean"),
    ],
    "enqueues": ["verilator.verilog"],
}

async def handler(input_data: dict, ctx: FlowContext) -> None:
    ...
    await ctx.enqueue({"topic": "verilator.verilog", "data": {...}})

Key changes:

  • "type": "event" removed
  • "subscribes": [...] becomes triggers: [queue(...)] (multiple topics = multiple queue() calls)
  • "emits" becomes "enqueues"
  • context.emit() becomes ctx.enqueue()
  • Handler params (data, context) become (input_data: dict, ctx: FlowContext)

Files (14 total):

  • verilator/: 01_clean_event, 02_verilog_event, 03_build_event, 04_sim_event, 04_cosim_event
  • compiler/01_build_event
  • firesim/: 01_buildbitstream_event, 02_infrasetup_event, 03_runworkload_event
  • marshal/: 01_build_event, 02_launch_event
  • sardine/01_run_event
  • palladium/01_verilog_event
  • workload/01_build_event

Phase 4: Update Utility Modules

File Changes
utils/event_common.py context.statectx.state, context.trace_idctx.trace_id, context.loggerctx.logger. These are shared helpers used by all steps — parameter naming must match new convention.
utils/stream_run.py No changes needed — pure Python subprocess utility, no Motia API usage
utils/path.py No changes needed
utils/port.py No changes needed
utils/search_workload.py No changes needed
services/* No changes needed — no Motia API usage
types.d.ts Delete — auto-generated by old Motia, no longer used

Phase 5: Cleanup

  • Delete motia.config.ts, package.json, pnpm-lock.yaml, pnpm-workspace.yaml
  • Delete types.d.ts
  • Delete .motia/ directory
  • Evaluate whether flake.nix needs updates for iii engine and new commands

Change Summary

Change Count Type
"type": "api"http() trigger 14 files Mechanical
"type": "event"queue() trigger 14 files Mechanical
context.emit()ctx.enqueue() ~20 calls Find-and-replace
"emits""enqueues" 28 configs Find-and-replace
req.get("body")request.body 14 API handlers Mechanical
Return dict → ApiResponse(...) 14 API handlers Mechanical
context.*ctx.* in utils 2 files Find-and-replace
New config.yaml 1 file Create
New pyproject.toml 1 file Create
Delete TS/Node files 4-5 files Delete

Risks and Considerations

  1. wait_for_result() polling pattern: API steps poll state in a while True loop with asyncio.sleep(1). This should still work with new Motia, but needs verification on whether the new state API still wraps values in a data field (old Motia state.get() returns {"data": actual_value}). If the wrapping is removed, the result["data"] unpacking logic in wait_for_result() needs adjustment.

  2. sys.path hacks: Event steps manually add utils_path to sys.path. The new Python runtime's directory handling may affect this — needs testing.

  3. iii engine system dependency: The new architecture requires the iii Rust engine. This needs to be incorporated into the Nix flake.

  4. State parameter semantics: context.state.set(trace_id, key, value) maps to ctx.state.set(group, id, value) — parameter positions are identical, semantics map 1:1 (trace_id → group, key → id). Low risk.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions