Skip to content

Scripts through the API

The concept behind tools is explained in Tools.

A script is a program of your own that the model calls as one tool. You upload a small package once — a Python or Node entrypoint, the files it needs and the arguments it takes — and Core offers it to the model as script__<name>. When the model calls it, Core runs the package on the turn's computer, with the model's arguments, and answers what it printed and the files it wrote. The model never sees your code: it sees a name, a description and an argument schema.

Use a script when the model should reach for a tested program instead of writing the code again on every turn. "Make a presentation in our house style" becomes one call to script__house_deck with the slides as arguments, run against the template you uploaded once.

What your key needs

  1. The tooling capability, to register and offer scripts.
  2. The computer capability, to run them. Your organisation must permit computer.
  3. files if the model should work on a turn's attachments or if you register files by upload, and artifacts if you want the files a script writes back.

A key does not need the system tool computer under tooling.system_tools. That switch offers the three shell tools of Computers; without it, a key runs your scripts and is never handed a shell.

{
  "name": "Presentations",
  "capabilities": {
    "tooling": {},
    "computer": {},
    "files": {},
    "artifacts": {}
  }
}

Your deployment must run computers for your organisation. Where it does not, a request naming a script is refused with "This tool is not configured on this deployment." before the model runs, and the problem body carries "reason_code": "computer_not_configured".

Register a script

POST /scripts creates the script and its version 1:

curl -s -X POST "$CORE_URL/scripts" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d @house_deck.json
{
  "name": "house_deck",
  "description": "Make a PowerPoint presentation in the company house style. Use it whenever the user asks for slides.",
  "entrypoint": ["python3", "main.py"],
  "parameters": {
    "type": "object",
    "properties": {
      "theme": { "type": "string", "description": "Theme of the deck" },
      "title": { "type": "string", "description": "Title of the first slide" },
      "slides": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "heading": { "type": "string" },
            "bullets": { "type": "array", "items": { "type": "string" } }
          },
          "required": ["heading"]
        }
      }
    },
    "required": ["title", "slides"]
  },
  "files": [
    { "path": "main.py", "content": "import json, os\nfrom pptx import Presentation\n..." },
    { "path": "template.pptx", "file_id": "file_8Kd2..." }
  ],
  "packages": [
    { "ecosystem": "pypi", "name": "python-pptx", "version": "1.0.2" }
  ],
  "timeout_seconds": 60,
  "labels": { "team": "marketing" }
}
Field What it holds
name lower-case letters, digits, - and _, at most 48 characters. The model sees script__<name>. The name never changes: policy rules, usage rows and the audit trail record it. Unique within your organisation; a duplicate is 409
description what the tool does and when the model should use it, at most 1024 characters
entrypoint the program to run, as a list of 1 to 16 arguments, such as ["python3", "main.py"] or ["node", "index.js"]. It is never a shell line: pipes, && and redirections do not work
parameters a JSON Schema object (Draft 2020-12, at most 64 KiB) the model's arguments must satisfy. Lists and nested objects are allowed. Leave it out for a script without arguments
files every file of the package, see Files
packages the approved libraries the package needs, see Packages
timeout_seconds optional: how long one call may run. It can only shorten the calling key's own limit (computer.exec_timeout_seconds); leave it out to take that limit
labels, metadata your classification of the script, which policy rules over tools read, and an opaque document Core stores and never reads. For a request that names a person, the rules over script__<name> also decide whether GET /scripts lists the script and whether it may be given a version or deleted (Registrations)

Core answers 201 with the script and a Location header. GET /scripts lists your scripts, each with its latest version's fields and number; GET /scripts/{script_id} answers one.

Files

Each entry of files has a path and exactly one of two sources:

  • content: the file's UTF-8 text, written in the body. Use it for source code.
  • file_id: a file you uploaded first with POST /files (Files). Use it for binary assets such as a template or a font, and for anything large. Core copies the upload into the version, so deleting the upload later leaves the version whole. Your key needs files and must be allowed to read that upload.

A path is relative, such as main.py or lib/deck.py, at most 255 characters, without .., a backslash or a leading /. A package holds at most 200 files and 20 MiB in total. A request body is capped at 1 MiB, so put larger files in by file_id.

GET /scripts/{script_id}/versions/{version}/files/{path} answers one file's bytes, with the file's SHA-256 digest as its ETag.

What the script receives

The entrypoint runs in the package's own directory, so main.py finds its own modules and files. It reads the model's arguments as one JSON object, from standard input or from the file the environment names:

Variable What it names
ARTIFIC_ARGUMENTS the file holding the arguments, the same JSON as on standard input
ARTIFIC_OUTPUTS an empty directory for this call. Every file the script writes directly in it is returned
ARTIFIC_INPUTS /workspace/inputs, where the turn's attachments are, as <file id>/<file name>

A minimal main.py:

import json
import os
from pptx import Presentation

args = json.load(open(os.environ["ARTIFIC_ARGUMENTS"]))
deck = Presentation("template.pptx")
# ... add a slide per entry of args["slides"] ...
deck.save(os.path.join(os.environ["ARTIFIC_OUTPUTS"], "presentation.pptx"))
print(f"{len(args['slides'])} slides written")

A new version

A version never changes once written. To change anything — code, files, description, parameters, packages, timeout — post the whole package again:

curl -s -X POST "$CORE_URL/scripts/$SCRIPT_ID/versions" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d @house_deck_v2.json

The body has the fields of POST /scripts without name. Files you leave out are not in the new version: a version is one package that was tested together. Core answers 201 with the new version. When the body is identical to the latest version, file by file, nothing is written and Core answers 200 with the latest. labels and metadata in the body replace the script's; leave them out to keep them.

GET /scripts/{script_id}/versions lists the versions, newest first, and GET /scripts/{script_id}/versions/{version} answers one.

An administrator can also save a version as a draft, in the console or its API. Your key does not see a draft: the script reads as its published version, a turn without a version runs that one, and pinning the draft's number is refused as a version that does not exist. A script that has only a draft is no script and no tool for your key until it is published.

DELETE /scripts/{script_id} deletes the script with all its versions and files and answers 204. While an agent names the script, the delete is 409 and lists those agents; remove the script from them first.

Offer it to the model

Name the script like any other tool:

{
  "model": "<a model id>",
  "input": "Make a deck of five slides on our Q3 results.",
  "tools": [{ "type": "tool", "name": "script__house_deck" }]
}

Without version the turn takes the latest version. Pin a version to keep a turn on code you tested:

{ "type": "tool", "name": "script__house_deck", "version": 2 }

A version that does not exist is refused, naming the entry's version. The version a turn resolved is recorded with the response, so the later rounds of a background run call the same version.

A script also fits everywhere else a tool name does:

  • A toolset holds it by name or by pattern, such as script__*. A toolset stays on the latest version.
  • An alias gives it a second name and fixes arguments the model never sees, such as alias__board_deck with {"theme": "board"} fixed. An alias can also fix an argument to a file of yours: the script then receives the path of a copy on its computer, under /workspace/.calls/, and the copy is removed when the script returns (Fix an argument to a file).
  • An agent names it in its tools.

Your key's policy decides by name: the rules over Tools must permit script__<name>, both when the tool is offered and again when the model calls it. A key without tooling or computer that names a script is refused; in a toolset the script is left out for that key. A script cannot be used in a nested agent turn: like the computer tools, it needs a turn of its own.

GET /tools/script__house_deck answers the script as the model sees it: its name, the latest version's description and parameters, and its labels.

A script can run in the background. Send background with the script or a new version, as for an API connector, and the model may start it as a task: it then runs on a computer of its own and its result comes back to the thread later. See Run a tool in the background.

What a call does

  1. The arguments are checked first. Arguments that do not satisfy parameters are answered Tool arguments were not valid., with the path of the first argument at fault and the rule it broke, so the model can correct itself. Nothing runs.
  2. The script runs on the turn's computer. The first computer call of a response makes the computer, with the turn's attachments copied in; every later script or shell call in the same response uses the same machine, one call at a time, in the order the model wrote them. So one script can read what another wrote. Core checks before every call that the package on the computer is exactly the version's, and writes it again when anything was changed.
  3. The entrypoint runs under one timeout: the shortest of the version's timeout_seconds, your key's computer.exec_timeout_seconds and the deployment's limit. The model cannot lengthen it.
  4. The model is answered the exit code, standard output and standard error, cut at 32 KiB together, followed by the files the call produced:

    exit_code: 0
    stdout:
    5 slides written
    stderr:
    
    files:
    presentation.pptx: kept as an artifact
    
  5. Each file written directly in ARTIFIC_OUTPUTS becomes an artifact of your key, at most 10 per call and 25 MiB each. A file past those limits, a subdirectory, a link, and every file for a key without artifacts is listed as left on the computer. Files written anywhere else stay on the computer until the response is done.

The output is untrusted tool content, like every tool result (Tool-result trust). When a call does not come back on its own terms, the model reads one of Core's fixed sentences:

What happened What the model is told Status of the call
the arguments do not meet parameters Tool arguments were not valid. with the path and the rule — no computer is made error
the script exits with a code other than 0 its exit code and output, as above error, executor_failed
the timeout passes what it printed so far, headed timed out after <n> seconds error, timeout
the version names a package that was revoked This script needs a package that is no longer approved: <ecosystem> <name> <version>. — no computer is made error, executor_failed
your key may not reach the region the computer runs in Denied by policy. — no computer is made denied
the computer would reach the internet and your policy limits where data may go, or your key hides personal data, or your organisation has not switched computer_internet on Denied by policy. — no computer is made for the call denied
the computer could not be made or reached Computer is unavailable. error

None of these ends the turn; the model can carry on.

Call a script directly

Test a script before a model uses it, with the arguments a model would send:

curl -s -X POST "$CORE_URL/tools/script__house_deck/call" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"arguments": {"title": "Q3", "slides": [{"heading": "Revenue"}]}, "version": 2}'

The answer is the tool_call object of Call a tool directly: status, error_code, text with exactly what the model would read, and the files in outputs, base64-encoded when your key holds artifacts and they fit. Leave out version to run the latest.

A direct call makes a computer for that one call and removes it when the call returns. That computer is billed to your key like any other, so a trial run costs sandbox time.

No network

A script runs without network access: it cannot reach the internet, your own systems or a package index. Everything it needs travels with it: its files, the packages Core installs for it, and the turn's attachments. A script that needs data from elsewhere gets it as an argument or an attachment, or works alongside an API connector the model calls first.

A deployment whose operator offers computers that reach the internet is the exception, for an organisation that switched computer_internet on, along with every organisation above it: a script there can connect out and send its arguments and files anywhere. Do not count on it. Without the switch the script runs with no network, or gets no computer where the deployment has only open ones. Even with it you get no such computer when your policy names allowed regions or vendors, narrows the hosts it may reach, or requires personal data hidden, or when your key hides personal data. An organisation that has set allowed hosts is refused too. A residency exception does not change that; every script call answers Denied by policy..

Where the deployment runs Core's egress proxy, such a script is told to use it, and the traffic that goes through it is logged -- host, bytes, time -- and, where the deployment configures a rate limit, limited per organisation, as for any computer. Traffic that bypasses the proxy still reaches the internet, and is not logged, not limited, and not checked against the block list or the private-address rule. The proxy's credential is in the script's environment; a script that prints it puts it in its output, and from there in the turn and its trace.

Packages

A script can import only libraries that were approved. There are three origins, which GET /packages lists together, each marked by approval:

approval What it is Who decides
image a library the sandbox image already ships, such as python-pptx or pandas (What the sandbox image ships) the platform: every release ships one image, and the list follows it
deployment a file your operator approved for every organisation on the deployment your operator; you read it, you cannot change it. The approver reads as operator
organisation a file your organisation approved for its own scripts an administrator of your organisation, or a key with management

Libraries in the image can be imported without naming them. Every other package must be named in the version's packages, as {ecosystem, name, version} with pypi or npm and the exact version. When the version is written, Core looks each one up — the deployment's list first, then your organisation's — and records which approval it resolved to and the approved file's SHA-256 digest:

{ "ecosystem": "pypi", "name": "python-pptx", "version": "1.0.2", "approval": "image", "sha256": null }

A script you installed from a bundle can show a fourth value, bundle: a package only the publisher approved, which you accepted by installing and which holds for that copy alone (Packages in a bundle).

Name every package, dependencies included. Core installs the files offline and resolves no dependencies: a library your package needs, and every library that library needs, must each be approved and named. A dependency you leave out fails at the script's own import, which a direct call shows at once. A version names at most 32 packages and 128 MiB of approved files together. A package no list approves, or one named twice, is refused 422 on packages.<i>.

Naming an image library is optional, and pins it: if a later image drops it, the version stops running instead of failing on import.

On the first call of a version on a computer, Core copies the approved files into the sandbox and installs them there; the script finds them on its normal import path (PYTHONPATH, and NODE_PATH for require; an ES module reaches one through createRequire). An install that fails is answered like a failing script, with the installer's output, and the entrypoint does not run.

Approve a package for your organisation

A key with management approves one file per call. The body is the file itself; name, version and filename go in the query:

curl -s -X POST \
    "$CORE_URL/packages?ecosystem=pypi&name=qrcode&version=8.2&filename=qrcode-8.2-py3-none-any.whl" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/octet-stream" \
    --data-binary @qrcode-8.2-py3-none-any.whl
  • For PyPI, upload the wheel (.whl), never a source distribution: the sandbox has no compiler and builds nothing. The wheel must load on the sandbox: a pure-Python wheel (py3-none-any) always does; a compiled one must be built for the image's Python and platform, CPython 3.12 on Linux x86-64 (manylinux, Runtimes). A wheel that does not fit, or whose filename does not carry the name and version you gave, is 422 on filename. Test a script locally shows how to try a wheel in the image before you upload it.
  • For npm, upload the tarball npm pack writes (.tgz). It is installed without running its install scripts, so a package that needs a build step, such as a native addon, does not work.
  • A file is at most 32 MiB. Core computes and keeps its SHA-256 digest; GET /packages/{package_id} answers it.
  • Your organisation's list holds at most 500 approvals; an approval past that is 409. A name and version already on it is 409 too; to use another version, approve that version separately.
  • When an organisation above yours keeps key management to itself, approving and revoking are its administrators' and your own key is answered 403.

An approval belongs to your organisation alone. No other organisation's scripts can name it.

Revoke a package

DELETE /packages/{package_id} revokes one of your organisation's approvals and deletes its file; it answers 204. A package the deployment approved is 403: only your operator revokes those.

A version is never rewritten, so a version that names a revoked package still names it, and its next call is refused with This script needs a package that is no longer approved: …. Write a new version without the package, or with an approved replacement. Approving the very same file again lets those versions run again; another file under the same name and version does not, because each version pinned the digest of the file it was tested with. The same holds when your operator revokes a deployment package or moves to an image without a library a version names.

Share a script

A script can go into a publication like a prompt or a connector. Its files are held beside the publication by digest, packages only your organisation approved travel with the bundle, and the subscriber needs tooling and computer. When you withhold the content, the subscriber's copy shows the name, the description and the parameters but not the entrypoint or the files, and it runs on a computer of its own in each turn. A linked copy cannot get a new version or be deleted (409); only a forked one can.

Usage and audit

  • The computer is billed as on Computers: one sandbox_seconds record per computer. Installing packages and writing the package are part of the call and count as computer time.
  • Each call writes one tool_invocation usage record named script__<name>. When your deployment's price list has a price for that tool name, each call is priced; without one, a call costs only its share of the computer.
  • The audit trail records script.created, script.version_created, script.updated (a version body that changed only labels or metadata) and script.deleted; package.approved and package.revoked; and per call computer.executed with kind script, the script's id, the version, the duration and the exit code. Arguments, output and file names never reach the audit trail.

The same screens in the console are on Scripts in the console.