Docs

Script Nodes & Scripts

Run your own JavaScript inside a flow: the shape of a script file, the platform resources it can import, the sandbox rules, and worked examples.

What a Script Node Runs

A Script node runs one JavaScript file from your deployed project — in a locked-down sandbox.

A Script node runs a .js file you wrote in your project. Use it for the work the other nodes can't express: shape the flow's data, call an external API, drive agents from code, or run custom validation. The file runs in a locked-down sandbox with no access to the machine it runs on — no filesystem, no ambient network — and a fresh sandbox is created for each run and torn down afterwards. The only things a script can reach are the dictionary it's given and the platform resources it explicitly imports.

On the canvas a Script node has three dots: In (from the previous node), Out (runs on a clean return, carrying your return value), and Error (runs if the script throws). Wire the Error dot for anything that can fail — a throw with no Error edge just ends that branch.

The Shape of a Script

Every script exports one function, and it has a specific name.

A script file must export a function named exec that takes one argument, props. It is a named exportexport const exec = … — not a default export. It may be synchronous or async, and its return value becomes the node's result, so it must be plain data (no functions or class instances).

the required shape

export const exec = (props) => {
    // your code — read/write the dictionary, call resources, compute
    return { ok: true };          // the node's result; must be plain, copyable data
};

// async works too — the platform waits for the result:
export const exec = async (props) => {
    const value = await somethingAsync();
    return value;
};
Named exec — not export default
The runner looks for a named exec export. A file that uses export default — or exports no exec at all — fails the node to its Error edge.

Inputs: props

The Script node's Key/Value Pairs arrive as the exec argument.

A Script node has the same Key/Value Pairs editor as an Assignment node (up to 10 pairs; keys up to 100 and values up to 200 characters). Each pair's value is resolved with the usual ${…} rules — only ${dictionary.*} and ${session.*} — and the pairs are handed to exec as the props object. As everywhere, a plain typed value arrives as text; a whole-string placeholder preserves its type.

pairs in, props out

Key/Value Pairs on the Script node:
    city      =  Sofia
    userName  =  ${dictionary.name}

// inside the script:
export const exec = (props) => {
    props.city;      // "Sofia"  — a plain value arrives as text (coerce numbers yourself)
    props.userName;  // dictionary.name's value, with its type preserved
};

Reading & Writing the Stores

The dictionary is a ready-to-use global; the session comes through a resource.

The current flow's dictionary is available directly as a dictionary global — no import needed. It is a writable copy of the flow's scratchpad: read it, mutate it, and on a clean return your changes are committed back. The session (the whole-run, flat store) is reached through the system/session resource.

dictionary global + session resource

import Session from 'system/session';

export const exec = (props) => {
    const items = dictionary.items ?? [];              // dictionary — a bare global, no import
    dictionary.total = items.length;                   // a write; commits on a clean return

    Session.setSessionValue('lastStep', 'validated');  // whole-run, flat values only
    const step = Session.getSessionValue('lastStep');  // (or: Session.lastStep)

    return { ok: true, step };
};
On a clean return
Your dictionary changes, any session writes, and any queued agent writes are all committed; the flow continues out Out with your return value.
On a throw
The flow leaves the Error edge; dictionary changes and queued agent writes are discarded (all-or-nothing — nothing is half-written), but session writes made before the throw persist, so an error-handling node can still read them.

Platform Resources

The nine modules a script may import — everything a script can reach beyond the dictionary.

A script imports a resource by its module name. Both a default import of the whole object and named imports of individual methods work.

two import styles — both valid

import Log from 'system/log';        // the whole resource
Log.info('hello');

import { info } from 'system/log';   // or one method by name
info('hello');

There are nine resources (note it is system/log, singular):

system/chat
Stream an interim status to the end user — a live "what I'm doing now" message (writeOut, writeThinking, progress and streaming helpers). It is not the final answer; the node's return / the agent's response is.
system/log
Leave a trace in the platform log — error, warning, info, debug. Not shown to the end user.
system/session
Read and write the whole-run shared store — flat, simple values only. Use it to carry a small value between flows and nodes in the run (methods, or direct property access like Session.userId).
system/dictionary
Read another flow's dictionary (read-only), given its flow id. The current flow's dictionary is the bare dictionary global — this resource is only for cross-flow reads.
system/agent
Read and write the agent conversation — prompts and responses, organised into named channels. Its methods let a script send an agent a message, read an agent's latest answer, and drain a channel. This is how a script drives agents and how a tool replies to its caller.
system/settings
Read the agent's Json Settings (read-only) — get(field), getMany(...), has(field). This is config a human set on the agent's runtime settings; the partner-facing front settings are never visible here.
system/cookies
Read the request's browser cookies (read-only) — get(name), all(), has(name). Useful to read a token the user's browser sent (e.g. an OAuth cookie) so a script can call a third-party API on their behalf.
system/http
Make an outbound HTTP call — the sandbox has no network of its own, so this is the only way out. get, getJson, post, postJson, request. Every method returns { ok, status, body }.
system/tools
Small host utilities — generateUUID() (the sandbox has no crypto) and getTimestamp().
Only these nine, and only system/log is singular
Any other import — an npm package like lodash, an absolute path, or a non-existent module such as system/logs (plural) — is rejected and fails the node to its Error edge.

The Sandbox Rules

What a script can and cannot do — and why that makes partner code safe to run.

  • No filesystem, no ambient network. A script cannot read files or open its own network connection. The only way out is an system/http call, which is what makes it safe to run partner-authored code at all.
  • No fetch, no crypto. These globals are not present. Use system/http for outbound calls and system/tools for a UUID or timestamp.
  • Only what you are given. A script sees the dictionary global and the resources it imports — nothing else on the host is reachable.
  • One outbound read at a time. Outbound system/http calls and system/agent reads must run one after another — await each. Firing two at once (for example inside a Promise.all) fails; a single response that is too large fails too.
no filesystemno ambient networkfresh sandbox per runawait one outbound read at a time

Importing One Script Into Another

Split logic into your own helper files, and reuse code across a project.

A script can import your own files by relative path — ./helpers.js in the same folder, ../lib/format.js in a sibling folder. A helper file is a normal module: it exports whatever functions or constants you like and does not need an exec — only the file a node runs (or a registered tool) needs that. Bare package names and absolute paths are not allowed; only system/* resources and your relative files.

your own helper modules

// scripts/init.js  — the file a Script node runs
import { clean }  from './helpers.js';       // same folder
import { format } from '../lib/format.js';   // a sibling folder

export const exec = (props) => {
    return format(clean(props.raw));
};

// scripts/helpers.js  — a helper: no exec needed
export const clean = (s) => (s ?? '').trim();
A registered tool lives under tools/
A script that an agent can call as a tool is registered in your project and lives under a tools/ folder; a plain helper you import with ./ or ../ is just your own module. See the worked tool example below.

Worked Examples

Three scripts you will write again and again.

1 — Shape the flow's data. Read the dictionary, compute, write back. Changes commit when the script returns cleanly.

scripts/summarize.js

export const exec = (props) => {
    const items = dictionary.items ?? [];   // read the current flow's scratchpad
    dictionary.count = items.length;         // write — persists on a clean return
    dictionary.first = items[0] ?? null;
    return { ok: true };
};

2 — Call an external API. Every system/http method returns { ok, status, body }, so check ok for an error status. A transport or policy failure (a network error, a timeout, a blocked address) throws, so wrap the call in try/catch and wire the node's Error dot.

scripts/fetch-profile.js

import Http from 'system/http';

export const exec = async (props) => {
    try {
        const res = await Http.getJson('https://api.example.com/users/' + props.userId);
        if (!res.ok) { return { ok: false, status: res.status }; }
        dictionary.profile = res.body;       // committed on a clean return
        return { ok: true };
    } catch (err) {
        return { ok: false, error: err.message };
    }
};

3 — Reply as a tool. When an agent calls a tool, the tool reads the call from toolOptions, does its work, and pushes the result back to the calling agent through system/agent. The reply must be stamped with the caller's session so the agent recognises it as the answer to its call.

scripts/tools/lookup.js

import Agent from 'system/agent';
import Tools from 'system/tools';

export const exec = () => {
    const call    = toolOptions.toolContext;                     // the call the model made
    const agentId = call.agentId;                                // which agent to answer
    const session = toolOptions.agentNode.flowContext.sessionId; // stamp with the caller's session
    const input   = (call.options.context ?? '').trim();

    Agent.setRootAgentPrompt(agentId, {
        prompt: 'Looked up: ' + input,
        sessionID: session,
        requestID: Tools.generateUUID()
    });
    return { ok: true };
};
An un-stamped tool reply is invisible
A tool's reply must carry the caller's sessionID. Without it, the agent — which resolves its trigger scoped to its own session — never sees the answer and reports No tool response, even though the tool ran and fetched its data. This is the single most common tool bug.

Good to Know

  • Script props are text. A value typed as 123 arrives as the string "123" — convert it yourself (Number(props.x)) or pass a placeholder that already holds a number.
  • Wire the Error dot. A throw discards the dictionary and agent writes and leaves the Error edge — connect it to a handler so a failure is a designed outcome, not a dead branch.
  • Return copyable data. The return value is copied out of the sandbox to become the node's result, so return plain objects, arrays, and values — not functions or class instances.
  • Scripts read runtime settings only. system/settings exposes the agent's Json Settings; a value placed on the partner-facing front settings is simply invisible to the code that expected it.

Data & Placeholders

The dictionary and session a script reads and writes.

Agent Settings

The Json Settings your scripts read.

Node Reference

Every node's fields and handles.

Previous

Data & Placeholders

Next

Building Tools

Keen Agents 2026

Documentation

Release 15