Octonode Playbook

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

ChannelDirectionCarries
stdinengine → nodeone request envelope (JSON), then EOF
stdoutnode → engineone response envelope (JSON)
stderrnode → enginelogs/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

CodeMeaningRetryable default
VALIDATION_ERRORinputs or outputs failed schema validationno
RUNTIME_ERRORthe node threw, crashed, or emitted invalid outputdepends
TIMEOUTthe node exceeded its deadline (synthesized by the engine)yes
NON_RETRYABLEa deliberate, terminal failure the node raisedno

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 by invocationId. Startup is paid once — often a 10–20× speedup for repeated calls (see octonode 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.

On this page