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
scripts.createwithskill_id.scripts.versions.createwith source and purpose. Fix static-check findings (static_check.findings) andreadiness.violationsfirst; each fix is a new version.scripts.testcases.setwith 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.scripts.testcases.runwith thetestcase_idandspace_id. This is a dry run: reads are real, writes are only recorded as simulated effects, no confirmation is needed.scripts.runs.dry_rundoes the same without a test case.- Read
scripts.runs.get,scripts.runs.effectsandscripts.runs.output, thenscripts.testcases.acceptwith that run. - Pin the version in the skill:
skills.draft.savewith the script inscripts[](scriptId, explicitversion,alias) and the instructions mentioning it as@script:<alias>, thenskills.save. Publishing lets the pinned version run live as the toolrun_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 asparams.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_onis 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.exceptionor raise). A row whose merge key matches an archived record, as after a reverted run, is such an error unlessrestore_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()andctx.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.randomandctx.new_id()instead.os,sys,pathlib,open,subprocess,socket,urlliband 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_inandmax_items_out(2000/5000),max_input_bytesandmax_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 throughsorted(...). - The only clock is
ctx.now. The moduletimeis refused as a whole, sotime.time()andtime.strftime()are not available. The attributes fromtimestamp, now, today, utcfromtimestamp, utcnow are refused on anything butctx(for exampledatetime.now(),date.today(),datetime.fromtimestamp()). Formatting and parsing are fine:ctx.now.strftime("%Y-%m-%d"),datetime.strptime(...),datetime.fromisoformat(...)andtimedeltaarithmetic 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.violationsof 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.startand the skill tools takeinput_from_run_idinstead 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 takeinput_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,typeand 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
whileloop 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:
ctxreassigned, aliased or passed other than positionally under the namectx; a record type key, integration key, operation, folder id ormerge_onthat 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) orscripts.runs.dry_runfor 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"]},
}