Skip to content
OpenAppPhysical access, fully managed
Login

Scripting

The Scripting OpenAPI tag is an asynchronous job API: POST /scripting/executions with CreateScriptingExecutionRequest (script — Rhai/OpenApp DSL source) returns 202 Accepted and a ScriptingExecution job. Poll GET /scripting/executions/{id} until status is terminal — succeeded, failed, or canceled — then read result (or error). Every operation requires the api_key security scheme (X-API-Key header) and, per OpenAPI, execute permission in at least one organization for the authenticated principal (no X-Org header on these routes).

The synchronous POST /scripting/execute route is removed — this is a breaking change with no compatibility shim.

For script syntax, built-ins, CLI, and the interactive shell, see OpenApp Scripting reference and Getting started: Scripting.

ConcernHTTPoperationIdNotes
Create executionPOST /scripting/executionscreate_scripting_executionBody CreateScriptingExecutionRequest; 202 with ScriptingExecution. Runs with caller permissions (see reference doc).
Get executionGET /scripting/executions/{id}get_scripting_execution200 with ScriptingExecution — the only route that carries result.
List executionsGET /scripting/executionslist_scripting_executions200 with an array of ScriptingExecutionSummary (no result). limit defaults to 20, max 100.
Cancel executionDELETE /scripting/executions/{id}cancel_scripting_execution204 cancel accepted, 409 already terminal, 404 unknown to the caller.

Generated clients derive method names from the operationId. If your bundle ships different names, map them with the operationId column above — the wire routes and payloads are the contract.

CapabilityPythonRust (openapp_sdk)GoTypeScript (AsyncClient)
Create executionclient.scripting.create_execution(script=…)client.scripting().create_execution(&json!({ "script": … }))ScriptingAPI.CreateScriptingExecutionNot on façade yet
Get executionclient.scripting.get_execution(id)client.scripting().get_execution(id)ScriptingAPI.GetScriptingExecutionNot on façade yet
List executionsclient.scripting.list_executions(limit=20)client.scripting().list_executions(Some(20))ScriptingAPI.ListScriptingExecutionsNot on façade yet
Cancel executionclient.scripting.cancel_execution(id)client.scripting().cancel_execution(id)ScriptingAPI.CancelScriptingExecutionNot on façade yet
Execute (submit + poll)client.scripting.execute(script=…)client.scripting().execute(&json!({ "script": … }))——
Tune execute polling—client.scripting().with_poll_schedule(PollSchedule::new(…))——

create_execution is the low-level call: it returns the job and you poll it yourself. execute wraps that submit-and-poll cycle and returns the script’s result directly — use it unless you need the job id (to cancel) or want to observe progress. Extra keyword arguments are merged into the JSON body (script remains the only documented field).

No SDK ships a filesystem helper — an HTTP client should not do filesystem I/O. Read the file yourself and pass its contents as script (see Run a .openapp file from disk).

Rust polling budget: execute polls with PollSchedule::default() — 1 s before the first read, backing off to 5 s, giving up at the 15-minute server-side runtime cap. Override it with with_poll_schedule when you need a different budget (short timeouts in tests, a longer ceiling behind a slow worker):

use std::time::Duration;
use openapp_sdk::resources::PollSchedule;
use serde_json::json;
let result = client
.scripting()
.with_poll_schedule(PollSchedule::new(
Duration::from_millis(250),
Duration::from_secs(2),
Duration::from_secs(60),
))
.execute(&json!({ "script": "1 + 1" }))
.await?;

The API gateway imposes a 120-second response deadline. Provisioning programs routinely run longer than that (many chained create calls, integration round-trips, sleep waits), so they cannot complete inside a single synchronous request. Submitting an execution returns immediately and the program continues on a backend worker.

401 without a valid API key/session. 403 when the caller lacks scripting execute permission (or equivalent policy). 400 when the request body is malformed (for example, missing script). 404 when reading or canceling an execution that belongs to another user. 409 when canceling an execution that is already terminal.

A script that fails at runtime is not an HTTP error — the submission still returns 202 and the failure lands in the job as status: "failed" with error. Surfaces ApiErrorResponse where applicable — see Errors & retries. Each POST counts toward the scripting_executions quota key on the QuotaKey schema in OpenAPI.

  • Runtime cap: 15 minutes — the execution becomes failed with an error naming the limit.
  • Result cap: the serialized result must stay under 8 MB, otherwise the execution fails.
  • Retention: executions are deleted 24 hours after completion.
  • Backend restart: an in-flight execution is marked failed (“interrupted by backend restart”) and is never retried.

Create an execution and wait for the result

Section titled “Create an execution and wait for the result”

Submit, read id, then poll get_scripting_execution until status is terminal. The loop below is bounded — 16 minutes of budget covers the 15-minute runtime cap — and sleeps 2 seconds between polls.

import asyncio
TERMINAL = {"succeeded", "failed", "canceled"}
async def wait_for_execution(client, execution_id: str, timeout_s: float = 960.0) -> dict:
deadline = asyncio.get_event_loop().time() + timeout_s
while True:
job = await client.scripting.get_execution(execution_id)
if job.get("status") in TERMINAL:
return job
if asyncio.get_event_loop().time() >= deadline:
raise TimeoutError(f"execution {execution_id} did not finish in {timeout_s}s")
await asyncio.sleep(2.0)
job = await client.scripting.create_execution(script="1 + 1")
job = await wait_for_execution(client, job["id"])
if job["status"] != "succeeded":
raise RuntimeError(job["error"])
assert job["result"] == 2

No SDK ships a filesystem helper — read the file yourself and pass its contents as script. Same wire shape, no extra endpoint.

from pathlib import Path
result = await client.scripting.execute(script=Path("setup.openapp").read_text())
print(result)

Use create_execution instead if you want the job rather than the result.

Read the result and handle script failures

Section titled “Read the result and handle script failures”

ScriptingExecution.result is the JSON value the script returned — the runtime preserves whatever JSON-shaped value it produced. Treat it as untyped at the SDK boundary and assert / deserialize at the call site. 401 / 403 / 400 signal request-level problems (credentials, permissions, malformed body), never script faults; a Rhai parse or runtime error arrives as status: "failed" with error.

Python’s and Rust’s execute raise on failed / canceled with the job’s error text, so the explicit status checks below are only needed when you poll the job yourself.

from openapp_sdk.errors import ApiError
try:
job = await client.scripting.create_execution(script="let xs = [1, 2, 3]; xs.len()")
except ApiError as err:
if err.status == 401:
# Bad API key — `err.body` carries `ApiErrorResponse`
...
raise
job = await wait_for_execution(client, job["id"])
if job["status"] == "failed":
raise RuntimeError(job["error"])
if job["status"] == "canceled":
raise RuntimeError("execution was canceled")
n: int = int(job["result"])

cancel_scripting_execution asks the worker to stop. 204 means the cancel was accepted — the job is not terminal yet, so keep polling until status becomes canceled. 409 means it already finished; 404 means the id is unknown to the caller.

await client.scripting.cancel_execution(job["id"])
job = await wait_for_execution(client, job["id"])
assert job["status"] == "canceled"

list_scripting_executions returns ScriptingExecutionSummary objects — the execution without result — newest first. Pass limit (default 20, max 100). Only your own executions are returned.

recent = await client.scripting.list_executions(limit=20)
for job in recent:
print(job["id"], job["status"], job["created_at"])

HTTP contract: API reference · OpenAPI JSON · OpenApp Scripting