This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/STARLARK-REFERENCE.md
Jeffrey Smith 8957e99610 Feat v0.7.12 batch.exec concurrent execution primitive
Add batch.exec(callables, timeout=10) — a general-purpose concurrent
execution primitive that runs arbitrary Starlark callables in parallel,
each in its own thread with independent step budget. Enables extensions
to fan out library function calls (frozen exports from lib.require)
without decomposing them back into raw http.post parameters.

New permission: batch.exec. Max 8 callables, timeout 1-30s.
Nesting prohibited via atomic flag. 12 new tests, all pass with -race.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:52:01 +00:00

297 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.
#### Aggregate operations
```python
# Count rows matching filters
count = db.count("tasks", filters={"status": "open"})
# Aggregate a column (sum, avg, min, max, count)
total = db.aggregate("orders", "amount", "sum", filters={"status": "paid"})
# Returns int, float, or None (if no matching rows)
# Batch multiple queries in a single call
results = db.query_batch([
{"table": "tasks", "filters": {"status": "open"}, "limit": 10},
{"table": "logs", "order": "-created_at", "limit": 5},
])
# Returns list of result lists. Max 10 queries per batch.
# Each query spec supports: table (required), filters, order, limit, before, after, search_like
```
#### 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
}
```
#### Batch requests
```python
responses = http.batch([
{"method": "GET", "url": "https://api.example.com/a"},
{"method": "POST", "url": "https://api.example.com/b", "body": "{}", "headers": {"Content-Type": "application/json"}},
])
# Returns list of response dicts (same shape as individual calls).
# Individual failures return {"status": 0, "body": "error: ...", "headers": {}}.
# Max 10 requests per batch. Dispatched concurrently.
```
**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
```
### batch
**Permission:** `batch.exec`
Run multiple callables concurrently. Each callable gets its own
execution thread with an independent step budget.
```python
jira = lib.require("jira-client")
confluence = lib.require("confluence-client")
results, errors = batch.exec([
lambda: jira.create_issue(issue_data),
lambda: confluence.create_page(page_data),
lambda: send_notification(user_id),
], timeout=15)
# results[i] = return value of callables[i], or None on error
# errors[i] = None on success, or error string on failure
# All three ran concurrently.
```
**Constraints:**
- Max 8 callables per call. Dispatched concurrently via goroutines.
- `timeout` (optional): 130 seconds per branch (default 10).
- `lib.require()` is not available inside branch callables.
Load libraries before the `batch.exec` call.
- `batch.exec()` cannot be called from within a branch (no nesting).
- `print()` output from branches is discarded.
---
## 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}}
```