Skip to content
Select themeSelect language

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 interpreterboa 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.

The Scripts tab of the Build hub, listing each script with its language and last revision.The Scripts tab of the Build hub, listing each script with its language and last revision.
  1. Open Build → Scripts and choose + New script.

  2. 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.
  3. 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.

  4. 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.

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 is null when 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 result is 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.

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_api

Python 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, url or endpoint unlocks that scheme, host and port exactly — so an https:// resource cannot be downgraded to plaintext against the same host.
  • A resource configured with a host (and optionally a port) 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
  1. In the fullscreen editor, use the Run script panel on the right.

  2. 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.

  3. Choose Run. The dispatch returns straight away with a run id and the panel starts polling; the script itself finishes in the background.

  4. 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.

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.

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.

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.