Octonode Playbook

Native Nodes

Canonical Octonode repository documentation.

Native Nodes

Native nodes are Octonode's built-in palette — an n8n-style library of pre-written nodes covering math, logic, flow control, data transforms, text/string manipulation, date & time, and free-form code.

Unlike marketplace plugins (which are installed as self-contained folders), native nodes are materialized directly into your project: running octonode add math.add writes a real, fully-editable TypeScript source file into nodes/ and registers a deterministic CommonJS runtime artifact in .octonode. You edit nodes/add_1.ts; Octonode rebuilds and executes nodes/add_1.cjs, so source and runtime remain distinct in either module mode.

Adding a native node

Native nodes and plugins are different: built-ins live in packages/native/src/catalog/; installable plugins follow plugins.md. To register a new built-in:

  1. Add one NativeNodeSpec to the matching catalog file. Declare its palette metadata, input/output JSON Schemas, and a TypeScript template using tsNodeFile from catalog/scaffold.ts.
  2. For a new category file, import its exported spec array into catalog/index.ts and spread it into CATALOG. Existing category files are already registered there.
  3. Use params only for values baked into generated source. A parameterized node is described after generation; a static node uses its catalog signature directly.
  4. Add one runnable test covering materialization and the node's non-trivial behavior. If the node changes Studio rendering, add/update the component story and visual baseline.
  5. Add the node to the catalog table below, then run yarn gen, yarn build, and the targeted test (plus the Storybook visual gate for Studio changes).

Generated node source must keep stdout reserved for the IPC envelope; log only to stderr.

Octonode integrations (3 nodes)

These native nodes use the active Octonode API, so the same node works with the local filesystem store and hosted Cloudflare workspace records. Set OCTONODE_API_URL when the API is not at http://127.0.0.1:8787/api.

IDOperations
octonode.data-tablesList/get/create/update/delete tables and rows; read-only SQL query
octonode.variablesList, set, and delete variables
octonode.metadataList plugins, native nodes, templates, projects, and workflows

The Catalog

Math (31 nodes)

The 13 binary operators take two operands a and b via incoming edges and emit one result. The generated code inlines the operator symbol — no function call, no indirection.

IDSymbolDescription
math.add+Addition
math.subtractSubtraction
math.multiply×Multiplication
math.divide÷Division (float)
math.floor-divide//Floor division
math.modulo%Modulo
math.power**Exponentiation
math.equals=Equality
math.not-equalsInequality
math.greater>Greater-than
math.less<Less-than
math.greater-equalsGreater-or-equal
math.less-equalsLess-or-equal

The 10 function nodes are unary (input a) or binary (a, b) and emit one numeric result:

IDSymbolDescription
math.abs|x|Absolute value
math.negate−xNegation
math.roundRound to nearest integer (half away from zero)
math.floor⌊x⌋Round down
math.ceil⌈x⌉Round up
math.sqrtSquare root
math.squareSquare (a · a)
math.signsgnSign (−1, 0, 1)
math.minminMinimum of a, b
math.maxmaxMaximum of a, b

The 8 advanced nodes work over a list (values) or take params:

IDSymbolDescription
math.sumΣSum a list of numbers
math.productMultiply a list together
math.averageArithmetic mean of a list
math.medianMMiddle value of a list
math.round-to≈nRound a to params.decimals places
math.clamp[ ]Constrain a to [min, max]
math.random?Random number in [min, max)
math.to-fixed0.0Format a with fixed decimals → string

Logic (6 nodes)

Boolean combinators and null/empty checks.

IDSymbolDescription
logic.anda && b
logic.ora || b
logic.not¬!a
logic.xora XOR b
logic.coalesce??First non-null of a, b
logic.is-emptyTrue for null / "" / [] / {}

Generated TypeScript excerpt for math.add:

// nodes/add_1.ts (compiled to nodes/add_1.cjs)
const { defineNode, start } = require("@octonode/sdk");
const node = defineNode({
  id: "add-1",
  inputs:  { type: "object", properties: { a: { type: "number" }, b: { type: "number" } } },
  outputs: { type: "object", properties: { result: { type: "number" } } },
  async run({ inputs }) {
    const { a, b } = inputs;
    return { result: a + b };   // the + symbol, not a function call
  },
});
start(node);

Flow (11 nodes)

IDDescription
flow.startOutput-only manual trigger: defines the workflow input signature from params.inputs and passes it through
flow.webhookOutput-only HTTP trigger for active workflows; payload schema comes from params.outputs
flow.ifEmits { true: { value } } or { false: { value } } — only the taken branch key is present
flow.switchRoutes value to one of N named output keys based on key (falls back to default); emits only the matching key. Declares each case + a default output port, so octonode validate warns when the default branch is left unwired
flow.filterPasses { passed: { value } } or { dropped: { value } }
flow.mergeMerges two upstream objects a and b into { merged: { ...a, ...b } }
flow.promiseJoins Promise branches using all, allSettled, any, race, settled, then, catch, or finally semantics
flow.waitDelays for delay_ms ms then emits { value } unchanged
flow.stop-errorAlways returns status: "error" with the incoming message — use as a guard
flow.loopMap Over Items — applies a 1:1 per-item transform to items (edit the transform in code). Not n8n's batching/sub-branch loop; the DAG engine has no sub-graph execution model
flow.workflow-refRuns another workflow as a single node (workflow-in-workflow). Studio lists existing workflows directly in the node picker and labels the canvas node with its target. A runtime OCTONODE_REF_STACK env guard throws on cycles. Requires a .octonode config file in the project root (file-mode projects only).

Console (1 node)

IDDescription
console.logLogs message to the run output (stderr, so it never collides with the node IPC channel on stdout) and passes message through unchanged. params.level (log/info/warn/error) prefixes the line

Data (24 nodes)

IDDescription
data.set-fieldsMerge params.fields into item
data.rename-keysRename keys in item according to a map
data.sortSort items by params.key, ascending or descending
data.limitKeep the first params.count items
data.remove-duplicatesDeduplicate items by params.key
data.split-outUnwrap item.items into a flat items array
data.aggregateCollect items[].value into a values array
data.summarizeSum/count/min/max/avg items[].field into result
data.filter-itemsKeep items where params.key equals params.value
data.flattenFlatten a list of lists by one level
data.group-byGroup items into buckets keyed by params.key
data.map-fieldSet a constant params.value on every item's params.key
data.mapRun a synchronous callback for every item
data.filterKeep items accepted by a synchronous callback
data.find / data.find-lastReturn explicit { found, value } first/last-match results
data.find-index / data.find-last-indexReturn the matching index or -1
data.flat-mapMap items and flatten one level
data.for-eachRun a side effect and return the resulting items
data.some / data.everyTest callback predicates
data.reduce / data.reduce-rightFold items with optional initial values

Text (13 nodes)

String manipulation nodes — most take text and emit result.

IDDescription
text.uppercaseConvert text to UPPERCASE
text.lowercaseConvert text to lowercase
text.trimStrip leading/trailing whitespace
text.lengthCharacter count of text → numeric result
text.replaceReplace every params.find with params.replace
text.splitSplit text on params.separatoritems array
text.joinJoin an items array with params.separatorresult
text.concatConcatenate two strings a + b
text.substringSlice text from params.start for params.length
text.containsWhether text contains params.search → boolean
text.templateFill a {{placeholder}} template from data
text.regex-extractFirst params.pattern match in text
text.padPad text to params.length with params.char

Date & Time (5 nodes)

Timestamps are epoch milliseconds — pure integer arithmetic with zero dependencies. parse follows an explicit UTC contract: an ISO 8601 string with no timezone offset is interpreted as UTC.

IDSymbolDescription
datetime.nowCurrent time as epoch ms
datetime.addAdd params.ms to a timestamp
datetime.diffΔta − b between two timestamps (ms)
datetime.format📅Format an epoch-ms timestamp as a UTC ISO 8601 string
datetime.parse→msParse an ISO 8601 string to epoch ms (no-offset = UTC)

Code (2 nodes)

IDDescription
code.customFree-form node — provide a function body via --params '{"body":"..."}'
code.source-functionCompiled whole-function adapter used when source contains runtime syntax outside the semantic graph grammar

The body receives inputs and must return an outputs object:

octonode add code.custom --params '{"body":"return { doubled: inputs.n * 2 };"}'

For the full TypeScript Handbook/JavaScript runtime matrix and JSON boundary, see TypeScript and JavaScript coverage.


CLI

# Add a node with defaults
octonode add math.add

# Specify language and workflow
octonode add flow.if --lang typescript --workflow main

# Pass params (e.g. switch cases, or a code body)
octonode add flow.switch --params '{"cases":["high","med","low"]}'
octonode add code.custom --params '{"body":"return { out: inputs.value * 2 };"}'

# List the full catalog
octonode add --list

Instance IDs are auto-incremented: the first math.add becomes add-1, the second add-2, and so on. Collision detection is safe across concurrent adds.

defaults.language

Set a project-wide default language so --lang can be omitted:

# .octonode.yaml
defaults:
  language: typescript

API Endpoints

MethodPathDescription
GET/api/native-nodesList catalog metadata (templates stripped)
POST/api/native-nodes/:id/materializeMaterialize a node; body: { language?, workflowId?, params?, position? }
POST/api/webhooks/:workflowIdTrigger an active workflow whose root is flow.webhook (SSE run stream)
GET/api/nodes/:id/sourceRead a node's source file ({ path, language, content, mtime })
PUT/api/nodes/:id/sourceWrite a node's source; body: { content, baseMtime } — returns { ok: true } or { ok: false, conflict: true, content } on 409
GET/api/nodes/:id/source/watchSSE stream of source-file change events for live editing

Branching Semantics

An edge from: { node, output: K } fires only when the node's outputs contain key K. A non-root node whose incoming edges all resolved inactive is skipped with reason no-active-branch; the skip propagates, but a fan-in node with at least one active incoming edge still runs.

This means flow.if (which emits either { true: ... } or { false: ... }) automatically prunes the untaken branch without any special-casing in the scheduler. The taken branch runs; the other is skipped with reason: "no-active-branch". Branch-skips do not count as failures and do not affect the run's status.


Studio

In edit mode the left palette shows all catalog entries grouped by category (Math / Flow / Data / Code). Drag a node onto the canvas or click Add to materialize it into the project and place it visually.

Existing workflows appear under Workflows as nodes in the node picker. Selecting one materializes flow.workflow-ref with its id, and the canvas renders a distinct workflow-call card so nested orchestration remains visible without opening generated code.

Math nodes display their operator symbol (e.g. +, ÷) as the node glyph on the canvas.

The live code panel opens when a native node is selected. It:

  • Shows the generated source file in a read-only-by-default editor.
  • Watches for external changes via the SSE /source/watch endpoint and refreshes automatically.
  • Displays a conflict banner when a write is rejected due to a concurrent edit (stale baseMtime).

On this page