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

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions