# Starlark Reference Armature extensions can include Starlark scripts for server-side logic. Starlark is a Python-like language designed for configuration and embedding — see the [official spec](https://github.com/google/starlark-go). ## Sandbox constraints - **No `while` loops** — use `for` with bounded ranges. - **No `load()`** — use `lib.require()` for library dependencies. - **Max steps:** 1,000,000 bytecode operations per execution. - **No filesystem or OS access** — all I/O goes through gated modules. - **Deterministic** — same input produces same output (no `random`, no `time`). ## Always-available modules These modules are injected into every script with no permission required. ### json Standard Starlark JSON module. ```python data = json.decode('{"key": "value"}') text = json.encode({"key": "value"}) ``` ### settings Read resolved package settings (global → team → user cascade). ```python val = settings.get("theme", "light") # Returns the resolved value, or the default if unset. ``` The cascade respects the `user_overridable` flag from the package manifest. See [Permissions & Groups](PERMISSIONS-AND-GROUPS) for details. ### lib Load exported functions from library packages. ```python helpers = lib.require("my-utils") result = helpers.format_date("2026-01-15") ``` Requirements: - The library must be declared in your package manifest's `dependencies`. - The library must be type `library`, status `active`, tier `starlark`. - Circular dependencies are detected and rejected. - Results are cached per execution (calling `require` twice returns the same object). ## Permission-gated modules These modules are only available if the package has the corresponding permission granted in **Admin > Packages**. ### secrets **Permission:** `secrets.read` Read admin-configured secrets for this package. ```python api_key = secrets.get("OPENAI_KEY") # str or None all_keys = secrets.list() # list of key names ``` Secrets are set in **Admin > Packages > Secrets** and scoped per package. ### notifications **Permission:** `notifications.send` Send in-app notifications to users. ```python notifications.send( user_id, # str — target user UUID title, # str — notification title body="", # str — optional body text type="extension.notify" # str — notification type ) ``` ### db **Permission:** `db.read` (queries) or `db.write` (mutations) Read and write extension data tables. All tables are automatically namespaced as `ext_{package_id}_{table_name}`. #### Read operations ```python # Query with filters, ordering, and pagination rows = db.query( "tasks", # table name (without prefix) filters={"status": "open"}, # equality WHERE clauses order="-created_at", # column name (prefix - for DESC) limit=50, # max 1000 before={"created_at": ts}, # range: column < value after={"created_at": ts}, # range: column > value search_like={"title": "%bug%"} # LIKE/ILIKE search ) # Read from system views (read-only) users = db.view("users", filters={"display_name": "Alice"}, limit=10) channels = db.view("channels", limit=100) # List all tables owned by this package tables = db.list_tables() ``` Available views: `users`, `channels`. #### Write operations ```python row = db.insert("tasks", {"title": "Fix bug", "status": "open"}) # Returns the inserted row dict (with generated id, created_at) db.update("tasks", row_id, {"status": "closed"}) # Returns True on success db.delete("tasks", row_id) # Returns True on success ``` ### http **Permission:** `api.http` Make outbound HTTP requests. ```python resp = http.get("https://api.example.com/data", headers={"Authorization": "Bearer ..."}) resp = http.post(url, body='{"key": "val"}', headers={"Content-Type": "application/json"}) resp = http.put(url, body="...", headers={}) resp = http.delete(url, headers={}) resp = http.request("PATCH", url, body="...", headers={}) ``` Response dict: ```python { "status": 200, "headers": {"content-type": "application/json"}, "body": "..." # capped at 1 MB } ``` **Security:** - Private/loopback IPs are blocked (SSRF protection). - Packages can declare `network_access.allow` (allowlist) or `network_access.block` (blocklist) in their manifest. - Max 10 redirects. 10-second timeout. 1 MB response body limit. ### realtime **Permission:** `realtime.publish` Publish WebSocket events to subscribed clients. ```python realtime.publish( "my-channel", # channel name "item.updated", # event label {"id": "abc"} # payload dict (max 7 KB) ) ``` The payload is automatically tagged with `_pkg: package_id`. ### connections **Permission:** `connections.read` Read external connection configurations (secrets are decrypted). ```python conn = connections.get("postgres", "main-db") # Returns dict with id, type, name, scope, plus flattened config fields # Returns None if not found all_pg = connections.list("postgres") # Returns list of connection dicts ``` Connections are resolved via scope chain: personal → team → global. ### workflow **Permission:** `workflow.access` Read workflow definitions and instances (read-only from Starlark; mutations go through the HTTP API). ```python defn = workflow.get_definition(workflow_id) # Returns dict: id, name, slug, entry_mode, is_active, version, stages[] inst = workflow.get_instance(instance_id) # Returns dict: id, workflow_id, current_stage, status, stage_data, ... instances = workflow.list_instances(workflow_id, status="active") # Returns list of instance dicts ``` ## Example: automated stage hook A simple hook that reads a setting, queries data, and advances: ```python def on_run(ctx): threshold = settings.get("approval_threshold", 1000) amount = ctx["stage_data"].get("amount", 0) if amount > threshold: notifications.send( ctx["started_by"], "High-value submission", body="Amount %d exceeds threshold." % amount, ) return {"advance": True, "data": {"needs_review": True}} return {"advance": True, "data": {"needs_review": False}} ```