Author and run a Script
A Script is one named piece of source code stored in your workspace — one file, one language, one slug — written in the browser and reusable everywhere else in SupaCloud that needs that logic. Scripts live under Build → Scripts.
A script does not run in a container. It runs inside a fixed, pre-built
interpreter — boa for JavaScript and TypeScript, CPython for Python — hosted
in a WebAssembly sandbox (WASI 0.2). Your source is handed to that interpreter as
data; nothing is compiled, no packages are installed, no image is pulled.
That is what makes a script cheap to start and safe to run unattended: by default
it has no filesystem, no shell, no database and no network at all. Network
access is something you grant, one host at a time, and never by accident.


Create a script
Section titled “Create a script”-
Open Build → Scripts and choose + New script.
-
Fill in the header fields:
- Name — the human label, up to 200 characters.
- Slug — auto-derived from the name while you have not touched it. Lowercase letters, digits and hyphens only, 1–63 characters, and it must not start with a hyphen. This is the stable address other things use, so pick it deliberately; it must be unique in the workspace.
- Language — TypeScript, JavaScript or Python. Changing the language while the editor is still empty swaps the starter snippet.
- Description — optional, shown on the board.
-
Write the body in the Source editor. A script is a single file, so the editor shows no file tree — just the code and a line count.
-
Choose Create script. You land in the fullscreen editor, with the run panel on the right and an Edit / Revisions tab strip in the header.
Write for the run contract
Section titled “Write for the run contract”The starter snippet the editor pre-fills is shaped for a workflow code node — a
handler function you export, or Python that reads standard input. A direct
script run never calls it. The substrate evaluates your source top to bottom
and takes the value of the final expression as the result, so replace the starter
before your first run.
The rules are the same in every language:
- Whatever you pass as input arrives as the global
input(it isnullwhen you send nothing). - The result is the value of the last expression. In Python, if the last
top-level statement is not an expression, a top-level variable named
resultis used instead — and if you set neither, the result is empty. - The result must be JSON-serialisable. Python rejects a coroutine or generator result with an explicit message, because scripts run synchronously.
// input is the JSON you passed in; the last expression is the result.const names = (input as { names?: string[] })?.names ?? [];
({ count: names.length, upper: names.map((n) => n.toUpperCase()),});TypeScript is type-stripped before it is evaluated, so types are erased rather than checked — a type error is not a run failure, and a runtime error is.
# input is the JSON you passed in.names = (input or {}).get("names", [])
result = {"count": len(names), "upper": [n.upper() for n in names]}Full Python builtins are available, and a broad slice of the standard library is
frozen into the interpreter. Third-party packages are not — there is no pip
step and no network at install time.
Give a script network access
Section titled “Give a script network access”A script starts with an empty egress allowlist, and an empty allowlist means every outbound request is refused. You open a hole by declaring, in the source itself, which workspace resource the script needs:
// resource: billing_apiPython may use either comment style — # resource: billing_api works the same
way. The marker must be the whole comment; a trailing // note after the name is
stripped, and repeated names are de-duplicated.
At dispatch the server looks up each declared name among your workspace’s resources, decrypts its secret, and derives the authorities that resource unlocks:
- A resource whose config carries a
base_url,urlorendpointunlocks that scheme, host and port exactly — so anhttps://resource cannot be downgraded to plaintext against the same host. - A resource configured with a
host(and optionally aport) unlocks that host and port.
Nothing else is reachable. Every request is additionally checked against the SSRF classifier — private, loopback, link-local and cloud-metadata addresses are blocked — and the address is pinned after that check, so a DNS answer cannot change between the check and the connection.
Inside the script, egress is an explicit call rather than an ambient capability:
| Language | Make a request | Read the bound resource |
|---|---|---|
| JavaScript / TypeScript | scHttp.request(method, url, { headers, body }), or the fetch shim over the same path |
scResource(name) |
| Python | sc_http |
sc_resource |
Run it and read the result
Section titled “Run it and read the result”-
In the fullscreen editor, use the Run script panel on the right.
-
Put a JSON object in Input (JSON) — this becomes the global
input. Leave it as{}if the script takes nothing. Invalid JSON is rejected before anything is dispatched. -
Choose Run. The dispatch returns straight away with a run id and the panel starts polling; the script itself finishes in the background.
-
Watch Recent runs. Each row shows the status and how long ago it ran, and links through to the full run detail. A failed run carries the reason inline.
The input is passed to the interpreter directly and is never stored — only the fact that an input was supplied, plus the names of the resources the script declared, are recorded on the run. That makes it safe to pass a one-off token or a customer identifier without it landing in the run history.
Call the script from somewhere else
Section titled “Call the script from somewhere else”The editor panel is only the nearest way in. The same script can be started from four other places, and each has a different reason to exist.
Add a code node, set its mode to Script, and pick the script from the workspace list. The engine loads that script’s source and language into the node before it runs.
This is the way to put a script on a cadence: a schedule cannot target a script directly, but a workflow can carry a Schedule trigger, and its code node can reference the script. See Build your first workflow and Set up a schedule.
Remember the substrate difference from the note at the top of this page: a code
node with no resource bindings runs your script in a Docker container with
networking off, where scHttp does not exist.
Choose Start run anywhere it is offered, then the Script tab. Pick the script, optionally supply JSON input, and launch — you are taken to the run detail page. This is the same dispatch as the editor panel, just reachable without opening the script.
curl -X POST https://supacloud.example.com/api/scripts/<script-id>/run \ -H "Authorization: Bearer <session-or-API-token>" \ -H "Content-Type: application/json" \ -d '{ "input": { "names": ["ada"] } }'The response carries run_id, the initial status, the script id and slug, and
bound_resources — the names the server resolved from your markers, which is the
quickest way to confirm a binding took effect. Poll
GET /api/scripts/<script-id>/runs for the outcome.
There is no unauthenticated trigger URL for a script. If you need one, put the script in a workflow and give the workflow a webhook trigger.
An agent can run a script through the MCP tool script.run, addressing it by
id or slug. This is deliberately harder to reach than the other read tools:
it needs the dedicated supacloud.script.run grant, which is carved out of the
ops tier rather than included in it, and it counts against the agent’s ops rate
budget.
An agent profile can narrow it further with a script allowlist — a list of slugs the profile may run. No list means every script in the workspace; an empty list means none. See Entitlements and the MCP tool tiers.
Only the fact that an input was present, plus the bound resource names, reach the audit log — never the input itself.
Track changes with revisions
Section titled “Track changes with revisions”Every save that changes the source captures the previous version as a revision, so the editor always shows current code and the history holds everything before it. Open the Revisions tab to list them newest-first, view one version’s source, or diff a version against current. Revisions written by repo-sync or another system path show no author. Deleting a script deletes its history with it.
Deleting is also blocked while a workflow code node still references the script — the error names the workflows so you can unpick them first.
Limits
Section titled “Limits”| Guard | Value |
|---|---|
| Wall-clock budget per run | 30 seconds, fixed — not configurable per script |
| Memory per run | 128 MiB, with a 256 MiB hard ceiling |
| Source size | 1 MiB, and it may not be empty |
| Language, input and source together | 8 MiB per dispatch |
| Name | 200 characters |
| Slug | 1–63 characters, lowercase letters, digits and hyphens |
There is no separate cap on what a script returns; the result is bounded by the per-run memory above. A run that overruns its budget finishes as failed and is marked as timed out.
See also
Section titled “See also”- Runs and tasks — how a script run fits the shared run model.
- Create a custom connection — add the resource a script binds for network access.
- Publish a marketplace bundle — ship a script together with the workflow or App that uses it.
- Web terminal command reference