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.
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.
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 export — export 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; };
exec export. A file that uses export default — or exports no exec at all — fails the node to its Error edge.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 };
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 }; };
dictionary changes, any session writes, and any queued agent writes are all committed; the flow continues out Out with your return value.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.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/chatwriteOut, writeThinking, progress and streaming helpers). It is not the final answer; the node's return / the agent's response is.system/logerror, warning, info, debug. Not shown to the end user.system/sessionSession.userId).system/dictionarydictionary global — this resource is only for cross-flow reads.system/agentsystem/settingsget(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/cookiesget(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/httpget, getJson, post, postJson, request. Every method returns { ok, status, body }.system/toolsgenerateUUID() (the sandbox has no crypto) and getTimestamp().lodash, an absolute path, or a non-existent module such as system/logs (plural) — is rejected and fails the node to its Error edge.What a script can and cannot do — and why that makes partner code safe to run.
system/http call, which is what makes it safe to run partner-authored code at all.fetch, no crypto. These globals are not present. Use system/http for outbound calls and system/tools for a UUID or timestamp.dictionary global and the resources it imports — nothing else on the host is reachable.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.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();
tools/ folder; a plain helper you import with ./ or ../ is just your own module. See the worked tool example below.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 }; };
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.123 arrives as the string "123" — convert it yourself (Number(props.x)) or pass a placeholder that already holds a number.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.Keen Agents 2026
Documentation
Release 15