Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
6.1 KiB
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.
Sandbox constraints
- No
whileloops — useforwith bounded ranges. - No
load()— uselib.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, notime).
Always-available modules
These modules are injected into every script with no permission required.
json
Standard Starlark JSON module.
data = json.decode('{"key": "value"}')
text = json.encode({"key": "value"})
settings
Read resolved package settings (global → team → user cascade).
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 for details.
lib
Load exported functions from library packages.
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, statusactive, tierstarlark. - Circular dependencies are detected and rejected.
- Results are cached per execution (calling
requiretwice 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.
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.
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
# 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
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.
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:
{
"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) ornetwork_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.
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).
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).
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:
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}}