Scripts

Contract, ctx API, derived capabilities, static rules and the path to a live run.

Raw Markdown for agents: scripts.md. MCP: read_guide("scripts").

A script is a small, deterministic Python program that the platform runs in a sandbox. It has no network and no imports beyond an allowlist; everything it may touch goes through one object, ctx. A script belongs to exactly one skill (scripts.create takes skill_id). Every version is immutable and every live run records its write effects.

You write only code. scripts.versions.create takes the source, a one-line purpose and optionally a description, input and output JSON schemas and limits. The platform derives from the ctx calls what the script reads, writes and calls (record types, folders, integrations and operations, files, log) and stores that as the version's manifest. There is no approval of a script by itself: a skill decides where it runs.

Flow

  1. scripts.create with skill_id.
  2. scripts.versions.create with source and purpose. Fix static-check findings (static_check.findings) and readiness.violations first; each fix is a new version.
  3. scripts.testcases.set with real example items. Its name is shown to users as the title of every test run, so use a short readable label such as "Order #41626 updated", not a slug.
  4. scripts.testcases.run with the testcase_id and space_id. This is a dry run: reads are real, writes are only recorded as simulated effects, no confirmation is needed. scripts.runs.dry_run does the same without a test case.
  5. Read scripts.runs.get, scripts.runs.effects and scripts.runs.output, then scripts.testcases.accept with that run.
  6. Pin the version in the skill: skills.draft.save with the script in scripts[] (scriptId, explicit version, alias) and the instructions mentioning it as @script:<alias>, then skills.save. Publishing lets the pinned version run live as the tool run_script__<alias>.

scripts.runs.start (live) through the API works only for a version the published skill pins. With settings.agentScriptsEnabled the skill agent can also write and run the skill's scripts itself during a skill run (script_write, script_run); start a rehearsal with skills.runs.start and dry_run to see what it writes; skills.runs.get lists script_versions and script_runs.

The contract

The source defines one entry point, main, and may add helper functions and classes (at most 12 top-level functions in total):

def main(ctx, items, params):
    return {"items": [...], "exceptions": [...], "summary": {...}}

items are the input items, params the parameters. exceptions holds the items the script could not process; the run still succeeds. Returned exceptions and those recorded with ctx.exception are merged; each needs a code and a message.

Schemas: input.items, input.parameters, output.items and output.summary are optional JSON schemas. Left out, items and parameters are open objects. A schema with declared properties is closed: every property declared, no additional ones.

The ctx API

  • ctx.now: the frozen run timestamp (datetime). The only clock.
  • ctx.random: a seeded random source. The only randomness.
  • ctx.new_id(): a deterministic id derived from the run seed.
  • ctx.params: the parameters, same content as params.
  • ctx.log(message, **fields): one structured log line (at most 500 lines and 2000 characters per message).
  • ctx.exception(item_index, code, message, data=None): record an exception for one input item instead of failing the run.
  • ctx.records.query("<record type key>", filter=None, fields=None, limit=200, cursor=None): one page of records; pass the returned cursor to continue.
  • ctx.records.get("<record type key>", record_id): one record.
  • ctx.records.upsert("<record type key>", rows, merge_on=["<field>", ...], restore_archived=False): write rows; merge_on is a literal list of one to three fields; no match creates, one match updates, several matches fail. At most 1000 rows per call. Returns created, updated, restored, skipped, items and errors; it never raises for a bad row. Rows are written in batches of 200, and one row error discards its whole batch, so check errors and report them (ctx.exception or raise). A row whose merge key matches an archived record, as after a reverted run, is such an error unless restore_archived=True; scripts that refresh their own rows set it.
  • ctx.documents.list(folder_id=None, cursor=None): documents, paged: of a literal folder id, or without folder the documents of the run's scope.
  • ctx.documents.read(document_id, offset=0, limit=20000): a slice of the document's extracted text.
  • ctx.documents.copy_to_workspace(document_id, path): copy the original of a CSV, JSON, XML or TXT document into the working directory.
  • ctx.integrations.call("<integration key>", "<resource.operation>", arguments): one call of an operation the skill enables; the first argument is the key of an integration of the skill.
  • ctx.files.open(path, mode="r"): a normal Python file object in the working directory of the run (modes r, rb, w, wb, a, ab); ctx.files.list() and ctx.files.remove(path). Paths are relative, no "..". The working directory belongs to the skill run and is deleted when it ends; outside a skill run each script run gets its own. A run that reads files cannot be replayed.

Rules

Sandbox

  • Importable modules: abc, base64, binascii, bisect, collections, copy, csv, dataclasses, datetime, decimal, defusedxml, enum, fractions, functools, hashlib, heapq, hmac, io, itertools, json, math, numbers, re, statistics, string, textwrap, typing, unicodedata, zoneinfo. Nothing else.
  • Refused as nondeterministic: calendar, random, secrets, time, uuid. Use ctx.now, ctx.random and ctx.new_id() instead. os, sys, pathlib, open, subprocess, socket, urllib and the rest are not available at all.

Limits

  • Size: at most 400 source lines, 150 lines in main, 12 top-level functions and 64 KB.
  • Run limits are optional, each within the platform ceiling: wall_seconds (default 120, max 600), cpu_seconds (30/120), memory_mb (256/512), max_capability_calls (300/1000), max_effects (2000/5000), max_items_in and max_items_out (2000/5000), max_input_bytes and max_output_bytes (8 MiB/16 MiB).

Determinism

  • The same input must produce the same effects. Derive keys and ids from the input data (or ctx.new_id()), never from the execution order or the current time. Iterate sets only through sorted(...).
  • The only clock is ctx.now. The module time is refused as a whole, so time.time() and time.strftime() are not available. The attributes fromtimestamp, now, today, utcfromtimestamp, utcnow are refused on anything but ctx (for example datetime.now(), date.today(), datetime.fromtimestamp()). Formatting and parsing are fine: ctx.now.strftime("%Y-%m-%d"), datetime.strptime(...), datetime.fromisoformat(...) and timedelta arithmetic are allowed.

Rights and scope come from the skill

  • Everything a script uses must be visible as literals in the code: record type keys, integration keys, operations, folder ids and merge_on.
  • The skill's rights are the upper bound: a script may only use the integrations and operations the skill enables, the record types of its record scope and the documents of the run's scope. readiness.violations of a version lists what the skill would refuse.

One script, one purpose

  • A script is a building block: one input shape, one output shape, one job. Chain several scripts instead of writing one large script.
  • scripts.runs.dry_run, scripts.runs.start and the skill tools take input_from_run_id instead of items: the output items of that finished, successful run become the input (same Space; inside a skill run only runs of the same skill run; a live run only takes the output of a live run). Inside a skill run the tools also take input_file, a JSONL file of the working directory. Bulk data never travels through an agent.

Errors and fixes

Static check (a version with an error finding cannot run; fix the source and create a new version):

  • S-00: the source does not parse. Fix the syntax error at the given line.
  • S-01: import outside the allowlist, relative import or star import.
  • S-02: main(ctx, items, params) is missing, duplicated or has other parameters, or the top level holds more than imports, constant assignments, functions and classes.
  • S-03: blocked builtin (eval, exec, open, getattr, type and the like), dunder attribute or name mangling.
  • S-04: hidden nondeterminism: a clock call or a nondeterministic or unavailable module.
  • S-05: async, await, async for/with or async comprehensions; a decorator outside the allowlist; metaclasses, class keywords, special methods or except*.
  • S-06: recursion, or a while loop without a bound that depends on a variable.
  • S-07: size: more than 400 source lines, 150 lines in main, 12 top-level functions, or more than 64 KB or NUL bytes.
  • S-08: ctx reassigned, aliased or passed other than positionally under the name ctx; a record type key, integration key, operation, folder id or merge_on that is not a literal.
  • S-09: an argument the platform sets itself (space, tenant, idempotency key and the like) passed by the script.
  • S-10: a format template with attribute or index fields, or an extracted format method.
  • S-11 (warning): iterating a set without sorted(...).
  • S-12: identifiers and string literals must be ASCII without invisible or bidirectional characters.

Readiness and pin issues:

  • script_target_out_of_scope: a record type the script reads or writes is outside the skill's record scope. Add it to the skill's record scope in the skill draft, or change the code.
  • script_operation_not_allowed: the skill does not enable an integration operation the script calls. Enable it in the skill draft, or change the code.
  • script_slot_unknown: the integration key in the code is not an integration of the skill, or its provider changed. Use the skill's integration key and write a new version.
  • script_dry_run_missing: the pinned version has no successful dry run. Run a test case (scripts.testcases.run) or scripts.runs.dry_run for exactly that version.
  • script_version_outdated: a newer version exists than the pinned one. Pin the newer version after it passed a dry run, or keep the pin deliberately.
  • script_version_not_published: a live run needs a version the published skill pins. Dry-run it instead, or pin and publish.

Example

from decimal import Decimal


def to_row(item, stamp):
    return {
        "order_id": str(item["id"]),
        "total": str(Decimal(str(item["total"])).quantize(Decimal("0.01"))),
        "synced_on": stamp,
    }


def main(ctx, items, params):
    stamp = ctx.now.strftime("%Y-%m-%d")
    rows = []
    for index, item in enumerate(items):
        if "id" not in item or "total" not in item:
            ctx.exception(index, "missing_field", "id and total are required")
            continue
        rows.append(to_row(item, stamp))
    result = ctx.records.upsert("orders", rows, merge_on=["order_id"])
    for error in result["errors"]:
        ctx.log("upsert error", error=error)
    return {
        "items": rows,
        "exceptions": [],
        "summary": {"created": result["created"], "updated": result["updated"]},
    }

On this page