Call NodeTool nodes as typed async functions from sandbox code, one importable module per node namespace, with streaming and plain JavaScript control flow
Scanned 9/1/2026
Install to Claude Code
npx -y skills add nodetool-ai/nodetool --skill sandbox-flow --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Sandbox Flow?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/nodetool-ai-sandbox-flow)More formats (shields.io, HTML) on the badges page.
---
name: sandbox-flow
description: Call NodeTool nodes as typed async functions from sandbox code, one importable module per node namespace, with streaming and plain JavaScript control flow
---
# Calling nodes from the sandbox
Specifier: `@nodetool-ai/sandbox-flow`. Each node namespace is its own module —
`@nodetool-ai/sandbox-flow/nodetool.text`,
`@nodetool-ai/sandbox-flow/lib.audio`, and so on for all 69. Every node type is
a generated async function whose name and inputs come from the node's own
metadata, so a type this pack does not export has no import to resolve.
This is not the graph DSL. Nothing is built, saved, or scheduled: the call runs
the node and resolves to its outputs. `await` is the edge, a variable is the
wire, `Promise.all` is the fan-out, and `if`/`for`/`try` are themselves.
```js
import { concat } from "@nodetool-ai/sandbox-flow/nodetool.text";
const r = await concat({ a: "hello ", b: "world" });
return r.output;
```
## Two imports, both required
1. `import { … } from "@nodetool-ai/sandbox-flow/<namespace>";` — the nodes.
2. `import "@nodetool-ai/sandbox-nodetool/flow";` — the capability module.
The second one is not decoration. The host mounts a capability module by
reading the **body's** static imports, and this pack's guest code calls into
`@nodetool-ai/sandbox-nodetool/flow`. Without the body-side import the facade
is never mounted and the pack's own import of it is refused by name.
```js
import "@nodetool-ai/sandbox-nodetool/flow";
import { concat } from "@nodetool-ai/sandbox-flow/nodetool.text";
```
## Streaming
A node that streams its output carries a `.stream` member; a one-shot node does
not, so the surface tells you which is which. Awaiting the plain call on a
streaming node still works — it drains the node and returns the last value per
slot.
```js
import "@nodetool-ai/sandbox-nodetool/flow";
import { agent } from "@nodetool-ai/sandbox-flow/nodetool.agents";
let text = "";
for await (const chunk of agent.stream({ objective: "Summarize the file" })) {
text += chunk.chunk ?? "";
if (text.length > 2000) break; // closes the stream; the node stops
}
return text;
```
Breaking early releases the stream on the host. Read it to the end, or break —
both are fine; abandoning the loop object without either is what leaks.
A node written against the streaming-input contract streams `{slot, value}`
instead of partial outputs, one item per emission, and its inputs accept an
array wherever a single value goes:
```js
import { take } from "@nodetool-ai/sandbox-flow/nodetool.control";
const kept = [];
for await (const { slot, value } of take.stream({ input_item: [1, 2, 3], n: 2 })) {
if (slot === "output") kept.push(value);
}
```
Arrays are the whole of streaming input in this version. An async iterable on
an input handle is not accepted.
## Errors
A node that fails rejects the call with its own error. There is no verdict
machinery and no per-node error stream — your `try`/`catch` is the supervisor,
and retry, fallback, and timeout are yours to write:
```js
import "@nodetool-ai/sandbox-nodetool/flow";
import { textToSpeech } from "@nodetool-ai/sandbox-flow/gemini.audio";
try {
return await textToSpeech({ text: draft });
} catch (error) {
return { error: String(error) };
}
```
## Fan-out
Concurrency is `Promise.all`, and it is real: the calls run at once.
```js
const summaries = await Promise.all(
documents.map((document) => summarizer({ text: document }))
);
```
Nothing throttles this. A hundred items is a hundred concurrent model calls —
batch them yourself when that matters.
## The untyped root
`import { callNode } from "@nodetool-ai/sandbox-flow"` takes a node type as a
string and checks nothing until the host answers. Use it when the type is
decided at run time; otherwise import the namespace, where a wrong name is an
import error rather than a failed call.
## Gotchas
- **Inputs are values, not handles.** There is nothing to wire and no
`.output()` — the call returns the outputs record.
- **No graph comes out of this.** Nothing opens in the editor, validates, or
replays. When the artifact matters, build a graph with
`@nodetool-ai/sandbox-dsl` instead.
- **A missing property is the node's default**, exactly as in a graph run.
- **Names follow the node class**: `nodetool.text.Concat` is `concat`,
`nodetool.constant.Integer` is `integer`. Reserved words take a trailing
underscore (`if_`).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!