Octonode IPC Protocol (v1)
Canonical Octonode repository documentation.
Octonode IPC Protocol (v1)
Octonode runs every node as an isolated child process. The engine and a node
exchange exactly one JSON envelope per request: the engine writes a request to
the node's stdin, the node writes a response to its stdout. stderr is
reserved for human logs and never parsed.
This document is the contract. Any program that honors it is a valid Octonode node, in any language.
Channels & framing
| Channel | Direction | Carries |
|---|---|---|
| stdin | engine → node | one request envelope (JSON), then EOF |
| stdout | node → engine | one response envelope (JSON) |
| stderr | node → engine | logs/diagnostics (free-form, ignored by the engine) |
- The request is a single JSON value, terminated by a newline and EOF.
- The response must be one JSON envelope and nothing else. Any stdout log or extra line is a protocol error; nodes must send all logging to stderr.
- Every envelope carries
"octonode": "1"(the protocol version).
Request envelopes (engine → node)
invoke
{
"octonode": "1",
"type": "invoke",
"invocationId": "f1e2…",
"node": "greet",
"inputs": { "name": "Ada" },
"context": { "config": {}, "attempt": 1, "env": "prod" }
}describe
Asks the node to self-report its manifest instead of executing. This is the
universal discovery mechanism behind octonode scan (Phase 3).
{ "octonode": "1", "type": "describe", "invocationId": "f1e2…" }Response envelopes (node → engine)
result (ok)
{ "octonode": "1", "type": "result", "invocationId": "f1e2…",
"status": "ok", "outputs": { "greeting": "Hello, Ada" } }result (error)
{ "octonode": "1", "type": "result", "invocationId": "f1e2…",
"status": "error",
"error": { "code": "VALIDATION_ERROR", "message": "…", "retryable": false } }manifest (response to describe)
{ "octonode": "1", "type": "manifest", "invocationId": "f1e2…",
"status": "ok",
"manifest": {
"id": "greet",
"language": "typescript",
"inputs": { "type": "object", "properties": { "name": { "type": "string" } } },
"outputs": { "type": "object", "properties": { "greeting": { "type": "string" } }, "required": ["greeting"] },
"icon": "wave"
} }Error codes
| Code | Meaning | Retryable default |
|---|---|---|
VALIDATION_ERROR | inputs or outputs failed schema validation | no |
RUNTIME_ERROR | the node threw, crashed, or emitted invalid output | depends |
TIMEOUT | the node exceeded its deadline (synthesized by the engine) | yes |
NON_RETRYABLE | a deliberate, terminal failure the node raised | no |
retryable on the error object — not the code — is authoritative for retry
decisions in the DAG executor (Phase 4).
Transport modes
The same envelopes work in two modes, so a runner that loops over stdin supports both with one code path:
- One-shot (default): the engine spawns a process, writes one request line, closes stdin, and reads one response. Maximum isolation; pays interpreter startup per call.
- Worker (opt-in, via
createNodeWorker): the engine keeps the process alive and streams many newline-delimited requests, each answered by a response line, multiplexed byinvocationId. Startup is paid once — often a 10–20× speedup for repeated calls (seeoctonode bench). Requires a looping runner. The supported TypeScript SDK is worker-capable; archived SDK fixtures remain protocol references only.
Engine-side guarantees
The engine never lets a misbehaving node throw inside the orchestrator. It synthesizes a structured error envelope when:
- the process fails to spawn →
RUNTIME_ERROR(retryable), - the process exceeds its deadline →
TIMEOUT(the engine kills the process tree), - the process exits without output →
RUNTIME_ERROR, - stdout is not valid JSON or doesn't match the envelope schema →
RUNTIME_ERROR.
This is why a node in any language is safe to run: the worst case is a typed error.