-
Notifications
You must be signed in to change notification settings - Fork 0
python to rust migration
The travel SPA's playbooks were first authored against the retired
Python NoETL runtime. The production runtime is now the Rust
noetl-server-rust + noetl-worker-rust stack, which parses every
playbook against a strict typed schema
(orchestrate-core/src/playbook.rs)
and validates structure before it will execute. Several Python-era
shapes the Python engine accepted leniently are rejected by Rust.
This page is the running list of those drifts and the corrected shapes. It exists because each one is a silent trap: the playbook registers fine (registration is permissive), then fails at execute time. Validate every migrated playbook by executing it, not just registering it.
The tool.kind value is a closed enum. Accepted kinds:
http, postgres, duckdb, ducklake, python, workbook,
playbook, playbooks, secrets, iterator, container,
script, snowflake, transfer, snowflake_transfer, gcs,
gateway, nats, shell, artifact, noop, task_sequence,
rhai, subscription, wasm. There is no catch-all — an
unknown kind makes the step's tool: block match neither variant
of the untagged ToolDefinition enum, producing:
400 Bad Request - {"error":"workflow[N]: data did not match any variant
of untagged enum ToolDefinition at line L column C","status":400}
The line/column point at the offending step's mapping.
The Python agent tool shape is not a Rust kind. Convert each
MCP / sub-playbook call:
# Python (rejected by Rust)
tool:
kind: agent
framework: noetl
entrypoint: automation/agents/mcp/firestore
payload: { method: tools/call, tool: get_doc, arguments: {...} }
# Rust (accepted)
tool:
kind: playbook
path: automation/agents/mcp/firestore # entrypoint → path
payload: { method: tools/call, tool: get_doc, arguments: {...} }-
entrypoint:→path:— the value is unchanged; it already equals the child playbook'smetadata.path. - drop
framework: noetl(Python-only). - keep
payload:— the Rustplaybooktool forwards it to the child as workload input (same{method, tool, arguments, …}contract the MCP playbooks read).
Caveat — the
playbooktool does not return the child's result. See §5.
The Rust runtime's validate_playbook rejects any workflow that
has no step literally named start:
422 Unprocessable Entity - {"error":"Workflow must have a step named 'start'","status":422}
The Python engine treated the first declared step as the entry
point; Rust requires an explicit start. Add a noop entry step
that routes to the real first step:
workflow:
- step: start
tool: { kind: noop }
next:
spec: { mode: exclusive }
arcs:
- step: normalize_input # the original first stepThis validation runs after parse, so a ToolDefinition error
(§1) masks it — fix the kinds first, then this surfaces.
The Rust http tool reads a JSON request body from json:. The
Python-era data: key is silently ignored, so the request goes out
with an empty body. This bit the gateway session-validate and
authorization playbooks (noetl/ai-meta#133 / #134):
# Python
tool: { kind: http, method: POST, url: "...", data: { foo: "bar" } }
# Rust
tool: { kind: http, method: POST, url: "...", json: { foo: "bar" } }Same rule for callback payloads posted back into the runtime.
Python steps could pull values out of an implicit context object
(context.get("x")). The Rust runtime renders a step's templates
against an explicitly-declared input: block; values a step needs
must be bound there (then referenced by name in code:). The same
fix replaced context.get(...) with input: + a pick in the auth
playbooks. Also watch accessor depth on prior-step results — e.g.
.command_0.rows collapsed to .rows during that migration.
The Rust playbook tool
(noetl-tools/src/tools/playbook.rs)
has two modes, and neither returns the child playbook's result
data:
-
async (no
return_step): returns{status: "started", execution_id, path, async: true}. -
blocking (
return_stepset): pollsGET /api/executions/{id}/statusand returns the execution status payload (status,current_step,progress,is_cancelled) — still not the child's output.
So a downstream reference like {{ call_google_places }} resolves
to a status envelope, not the Google Places results. For a playbook
that consumes its MCP children's outputs (itinerary-planner
feeds call_google_places / call_duffel_offers /
call_amadeus_hotels into normalize_tool_response, and
load_slot_state.data into extract_turn), this is a functional
blocker, not just a cosmetic one.
This needs a runtime capability that does not exist yet (a synchronous, result-returning sub-playbook invocation). Tracked as gated issue noetl/ai-meta#136. Do not work around it by inlining MCP logic or round-tripping results through Firestore — that changes the architecture.
A python step returns a result dict. The Rust worker wraps it,
so a field the step set at the top of result (e.g.
result["first_tool"]) is exposed downstream at
{{ <step>.context.data.first_tool }}, not
{{ <step>.context.first_tool }}. itinerary-planner's routing
when: conditions read the shallower path and resolve null,
so the tool-dispatch arcs never fire. Audit every
{{ <step>.context.<field> }} reference against the actual
result envelope. Tracked:
noetl/ai-meta#135.
# register (permissive — does NOT catch the drifts above)
noetl --host=localhost --port=8082 catalog register playbooks/<name>.yaml
# execute (strict — this is what surfaces the parse + validation errors)
noetl --host=localhost --port=8082 exec "<catalog/path>" --runtime distributed \
--set <key>=<value>
# then read the event trace for per-step pass/fail
curl -s "http://localhost:8082/api/executions/<exec_id>" | jq '.events'Register the child MCP playbooks too — a parent kind: playbook
step 404s if the child isn't in the catalog.
- Playbook: itinerary-planner — the worked example all of the above surfaced on.
-
Auth and session — where the
json:/input:drifts first bit. - Business data via playbooks
- noetl/travel#60 (itinerary-planner migration umbrella),
noetl/ops#209 (MCP dep
startsteps).
Travel SPA
Architecture
- Architecture
- Widget contract
- Business data via playbooks
- Playbook: itinerary-planner
- Playbook: calendar/list
- Python → Rust migration
- Travel-domain SLM
- Travel SLM journey
- Training the Travel SLM
Integration
Operations
See also
- noetl wiki (app)
- ops wiki (deploy)
- Ephemeral Blueprints