# How the iii Engine's WebSocket Protocol Enables Cross-Language Function Invocation Between Python, TypeScript, and Rust

> Discover how the iii engine's WebSocket protocol facilitates cross-language function invocation between Python, TypeScript, and Rust. Achieve seamless polyglot interoperability with language-agnostic message frames.

- Repository: [iii/iii](https://github.com/iii-hq/iii)
- Tags: internals
- Published: 2026-05-28

---

**The iii engine uses a unified JSON-over-WebSocket protocol defined in [`engine/src/protocol.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/protocol.rs) to route function calls between workers written in Python, TypeScript, and Rust, enabling seamless polyglot interoperability through language-agnostic message frames.**

The iii engine (iii-hq/iii) implements a polyglot function-as-a-service architecture where workers in different languages communicate through a single, shared WebSocket protocol. By standardizing on JSON message frames defined by a central `Message` enum, the engine enables **cross-language function invocation** without language-specific serialization layers or adapters. This design allows a Python worker to execute functions registered by TypeScript, Rust services to trigger Node.js handlers, and any combination thereof—all through identical wire formats.

## Protocol Architecture and the Message Enum

At the heart of the iii engine's cross-language capability lies a single source of truth defined in [`engine/src/protocol.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/protocol.rs). This file contains the `Message` enum, which specifies every frame type traversing the WebSocket connection: `WorkerRegistered`, `RegisterFunction`, `InvokeFunction`, `InvocationResult`, and `InvocationError`.

Because all three SDKs (Python, TypeScript/Node, and Rust) serialize and deserialize these identical JSON structures, the engine acts purely as a router. When a worker connects, it performs a handshake (`WorkerRegistered`), after which the engine can push `InvokeFunction` frames to any connected worker and receive `InvocationResult` frames in return. The payload remains untouched JSON as it travels between language runtimes.

## Worker Registration and Handshake

Before cross-language invocation can occur, workers must register their exposed functions with the engine. The SDKs open a WebSocket connection to the engine port and send `RegisterFunction` messages for each capability they offer.

In [`engine/src/engine/mod.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/engine/mod.rs), the WebSocket server handles the upgrade from HTTP, tracks connected workers, and stores the mapping between `function_id` strings and their hosting workers. Each worker receives a unique `worker_id` upon successful registration.

**Python registration** ([`sdk/packages/python/iii/src/iii/iii.py`](https://github.com/iii-hq/iii/blob/main/sdk/packages/python/iii/src/iii/iii.py)):

```python
from iii import register_worker

iii = register_worker("ws://localhost:49134")

@iii.register_function("hello::greet")
def greet(name: str) -> str:
    return f"Hello, {name}!"

```

**TypeScript registration** ([`sdk/packages/node/iii/src/iii.ts`](https://github.com/iii-hq/iii/blob/main/sdk/packages/node/iii/src/iii.ts)):

```typescript
import { registerWorker } from "iii-sdk";

const iii = registerWorker("ws://localhost:49134");

iii.registerFunction("analytics::track", async (data) => {
  console.log("Processing event:", data);
  return { status: "recorded" };
});

```

**Rust registration** ([`sdk/packages/rust/iii/src/lib.rs`](https://github.com/iii-hq/iii/blob/main/sdk/packages/rust/iii/src/lib.rs)):

```rust
use iii_sdk::{register_worker, RegisterFunction};

let iii = register_worker("ws://localhost:49134");

iii.register_function("compute::factorial", |n: u64| {
    (1..=n).product::<u64>()
}).await?;

```

## Cross-Language Invocation Flows

Once functions are registered, the engine routes invocations based solely on the `function_id`, ignoring the implementation language. The protocol supports two invocation patterns defined in [`engine/src/protocol.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/protocol.rs).

### Synchronous Invocation with Result Awaiting

For synchronous calls, the engine generates a unique `invocation_id`, sends an `InvokeFunction` frame to the target worker, and blocks until receiving a matching `InvocationResult` or `InvocationError` frame.

**TypeScript invoking Python**:

```typescript
const result = await iii.invokeFunction({
  function_id: "hello::greet",
  data: "World"
});
// Returns: "Hello, World!"

```

The resulting JSON frame carries the original `invocation_id`, allowing the TypeScript SDK to correlate the response with the pending promise.

### Fire-and-Forget Asynchronous Calls

For high-throughput scenarios, workers can invoke functions without awaiting responses by omitting the `invocation_id` field entirely.

**Rust invoking a metrics handler** (potentially Python or TypeScript):

```rust
iii.invoke_async(InvokeFunction {
    function_id: "metrics::track".into(),
    data: serde_json::json!({
        "event": "click",
        "label": "signup"
    }),
    // No invocation_id → fire-and-forget
}).await;

```

The engine forwards the message to the appropriate worker (registered in any language) and immediately returns without awaiting a response, maximizing throughput for telemetry and logging workloads.

## Why Cross-Language Interoperability Works

The iii engine achieves seamless polyglot function calls through four architectural principles:

- **Single source of truth**: The `Message` enum in [`engine/src/protocol.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/protocol.rs) serves as the authoritative schema. Each SDK implements thin wrappers around this definition, ensuring all workers speak the same wire protocol.

- **JSON payloads**: By standardizing on JSON for all data transfer, the engine avoids language-specific serialization formats. Primitive types, objects, arrays, and binary data (base64-encoded) travel unchanged between Python, TypeScript, and Rust runtimes.

- **Language-agnostic routing**: The engine in [`engine/src/engine/mod.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/engine/mod.rs) handles only connection management and frame routing. It inspects `function_id` and `invocation_id` fields but never deserializes the `data` payload, maintaining strict isolation between worker implementations.

- **Typed SDK abstractions**: While the wire format is uniform, each SDK provides idiomatic interfaces—`register_worker()` in all three languages hide WebSocket management, frame serialization (`serde_json` in Rust, Python's `json` module, JavaScript's `JSON.stringify`), and promise handling.

## Summary

- The iii engine defines a unified `Message` enum in [`engine/src/protocol.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/protocol.rs) that all SDKs use for WebSocket communication.
- Workers register functions via `RegisterFunction` messages, and the engine stores these mappings in [`engine/src/engine/mod.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/engine/mod.rs).
- **Cross-language function invocation** occurs when the engine routes `InvokeFunction` frames to workers based on `function_id`, regardless of implementation language.
- Synchronous calls use `invocation_id` to correlate requests and responses, while `invoke_async` provides fire-and-forget semantics.
- JSON payloads ensure data remains interpretable across Python, TypeScript, and Rust without translation layers.

## Frequently Asked Questions

### How does the iii engine route function calls between different programming languages?

The engine maintains a registry mapping `function_id` strings to active WebSocket connections. When a worker calls `invokeFunction`, the engine looks up the target `function_id`, retrieves the associated worker's connection from its internal state (managed in [`engine/src/engine/mod.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/engine/mod.rs)), and forwards the JSON frame unchanged. Because the payload format is identical across all SDKs, the receiving worker deserializes the message using its native JSON library and executes the handler.

### What happens if a cross-language function invocation fails?

When a registered handler throws an exception or returns an error, the target worker's SDK catches the failure and serializes an `InvocationError` frame containing the error message and stack trace. The engine routes this frame back to the original caller using the `invocation_id`. If the calling SDK is TypeScript, the promise rejects; if Python, it raises an exception; if Rust, it returns an `Err` variant—each SDK translates the protocol-level error into idiomatic language constructs.

### Can a Python worker synchronously invoke a Rust function and receive complex data structures?

Yes. Synchronous invocation works identically regardless of language direction. A Python worker can call `iii.invoke_function()` targeting a Rust-registered `function_id`, passing JSON-serializable dictionaries. The Rust handler receives the data as `serde_json::Value` or a typed struct, processes it, and returns a serializable response. The iii engine routes the `InvocationResult` frame back to Python, where the SDK decodes the JSON into native Python objects (dicts, lists, primitives).

### Does the iii WebSocket protocol support binary data transfer between languages?

Yes, though JSON is the primary format, binary data can be transferred by encoding it as base64 strings within the JSON payload. Since all three SDKs (Python's `base64`, JavaScript's `Buffer`, and Rust's base64 crates) handle this encoding consistently, workers can exchange images, files, or serialized protobuf messages across language boundaries while remaining compliant with the `Message` enum protocol defined in [`engine/src/protocol.rs`](https://github.com/iii-hq/iii/blob/main/engine/src/protocol.rs).