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.
Operations vs wire routes
Section titled “Operations vs wire routes”| Concern | HTTP | operationId | Notes |
|---|---|---|---|
| Create execution | POST /scripting/executions | create_scripting_execution | Body CreateScriptingExecutionRequest; 202 with ScriptingExecution. Runs with caller permissions (see reference doc). |
| Get execution | GET /scripting/executions/{id} | get_scripting_execution | 200 with ScriptingExecution — the only route that carries result. |
| List executions | GET /scripting/executions | list_scripting_executions | 200 with an array of ScriptingExecutionSummary (no result). limit defaults to 20, max 100. |
| Cancel execution | DELETE /scripting/executions/{id} | cancel_scripting_execution | 204 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.
SDK coverage
Section titled “SDK coverage”| Capability | Python | Rust (openapp_sdk) | Go | TypeScript (AsyncClient) |
|---|---|---|---|---|
| Create execution | client.scripting.create_execution(script=…) | client.scripting().create_execution(&json!({ "script": … })) | ScriptingAPI.CreateScriptingExecution | Not on façade yet |
| Get execution | client.scripting.get_execution(id) | client.scripting().get_execution(id) | ScriptingAPI.GetScriptingExecution | Not on façade yet |
| List executions | client.scripting.list_executions(limit=20) | client.scripting().list_executions(Some(20)) | ScriptingAPI.ListScriptingExecutions | Not on façade yet |
| Cancel execution | client.scripting.cancel_execution(id) | client.scripting().cancel_execution(id) | ScriptingAPI.CancelScriptingExecution | Not 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?;Why this is asynchronous
Section titled “Why this is asynchronous”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.
Typical errors
Section titled “Typical errors”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.
Execution lifecycle
Section titled “Execution lifecycle”- Runtime cap: 15 minutes — the execution becomes
failedwith an error naming the limit. - Result cap: the serialized
resultmust 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.
Examples
Section titled “Examples”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"] == 2CreateScriptingExecution expects CreateScriptingExecutionRequest with Script set; GetScriptingExecution returns the job.
import ( "context" "errors" "time"
openapiclient "github.com/tomers/openapp-sdk/go")
body := *openapiclient.NewCreateScriptingExecutionRequest(`1 + 1`)job, httpResp, err := client.ScriptingAPI.CreateScriptingExecution(ctx). CreateScriptingExecutionRequest(body). Execute()if err != nil { return err}httpResp.Body.Close()
terminal := map[string]struct{}{"succeeded": {}, "failed": {}, "canceled": {}}deadline := time.Now().Add(16 * time.Minute)for { if _, done := terminal[string(job.GetStatus())]; done { break } if time.Now().After(deadline) { return errors.New("execution did not finish before the deadline") } time.Sleep(2 * time.Second)
job, httpResp, err = client.ScriptingAPI.GetScriptingExecution(ctx, job.GetId()).Execute() if err != nil { return err } httpResp.Body.Close()}
if job.GetStatus() != "succeeded" { return errors.New(job.GetError())}_ = job.GetResult()use openapp_sdk::Client;use serde_json::json;use std::time::{Duration, Instant};use tokio::time::sleep;
const TERMINAL: &[&str] = &["succeeded", "failed", "canceled"];
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let created = client .scripting() .create_execution(&json!({ "script": "1 + 1" })) .await?;let id = created .get("id") .and_then(|v| v.as_str()) .ok_or("missing execution id")? .to_string();
let deadline = Instant::now() + Duration::from_secs(960);let mut job = created;while !TERMINAL.contains(&job.get("status").and_then(|v| v.as_str()).unwrap_or("")) { if Instant::now() >= deadline { return Err("execution did not finish before the deadline".into()); } sleep(Duration::from_secs(2)).await; job = client.scripting().get_execution(&id).await?;}
if job.get("status").and_then(|v| v.as_str()) != Some("succeeded") { return Err(job .get("error") .and_then(|v| v.as_str()) .unwrap_or("execution failed") .to_string() .into());}let _result = &job["result"];The Node AsyncClient does not expose /scripting/executions yet. Use fetch (or your HTTP stack), extend the façade, or use Python/Rust/Go.
const TERMINAL = new Set(["succeeded", "failed", "canceled"]);
type Execution = { id: string; status: "pending" | "running" | "succeeded" | "failed" | "canceled"; result?: unknown; error?: string | null;};
async function waitForExecution( apiBase: string, apiKey: string, executionId: string, timeoutMs = 960_000,): Promise<Execution> { const deadline = Date.now() + timeoutMs; for (;;) { const res = await fetch(`${apiBase}/scripting/executions/${executionId}`, { headers: { "x-api-key": apiKey }, }); const job = (await res.json()) as Execution; if (TERMINAL.has(job.status)) return job; if (Date.now() >= deadline) throw new Error("execution did not finish before the deadline"); await new Promise((r) => setTimeout(r, 2_000)); }}
const submit = await fetch("https://openapp.house/api/v1/scripting/executions", { method: "POST", headers: { "content-type": "application/json", "x-api-key": "https://openapp.house_openapp_YOUR_SECRET", }, body: JSON.stringify({ script: "1 + 1" }),});const { id } = (await submit.json()) as { id: string };
const job = await waitForExecution( "https://openapp.house/api/v1", "https://openapp.house_openapp_YOUR_SECRET", id,);if (job.status !== "succeeded") throw new Error(job.error ?? "execution failed");console.log(job.result);Run a .openapp file from disk
Section titled “Run a .openapp file from disk”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.
import ( "context" "os"
openapiclient "github.com/tomers/openapp-sdk/go")
source, err := os.ReadFile("setup.openapp")if err != nil { return err}
body := *openapiclient.NewCreateScriptingExecutionRequest(string(source))job, httpResp, err := client.ScriptingAPI.CreateScriptingExecution(ctx). CreateScriptingExecutionRequest(body). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = jobGo has no submit-and-poll helper — follow with the polling loop above.
use openapp_sdk::Client;use serde_json::json;use tokio::fs;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let source = fs::read_to_string("setup.openapp").await?;let result = client .scripting() .execute(&json!({ "script": source })) .await?;Rust has no filesystem helper, but execute polls for you and returns the result.
import { readFile } from "node:fs/promises";
const source = await readFile("setup.openapp", "utf8");const submit = await fetch("https://openapp.house/api/v1/scripting/executions", { method: "POST", headers: { "content-type": "application/json", "x-api-key": "https://openapp.house_openapp_YOUR_SECRET", }, body: JSON.stringify({ script: source }),});const { id } = (await submit.json()) as { id: string };TypeScript has no submit-and-poll helper — follow with the polling loop above.
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"])import ( "context" "errors"
openapiclient "github.com/tomers/openapp-sdk/go")
body := *openapiclient.NewCreateScriptingExecutionRequest(`let xs = [1, 2, 3]; xs.len()`)job, httpResp, err := client.ScriptingAPI.CreateScriptingExecution(ctx). CreateScriptingExecutionRequest(body). Execute()if err != nil { var apiErr *openapiclient.GenericOpenAPIError if errors.As(err, &apiErr) && httpResp != nil && httpResp.StatusCode == 401 { // Bad API key — apiErr.Body() has the ApiErrorResponse JSON } return err}httpResp.Body.Close()
// ... poll until terminal (see "Create an execution and wait for the result") ...
switch job.GetStatus() {case "failed": return errors.New(job.GetError())case "canceled": return errors.New("execution was canceled")}
if v, ok := job.GetResult().(float64); ok { _ = int(v)}use openapp_sdk::{Client, SdkError};use serde_json::{json, Value};
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let created = match client .scripting() .create_execution(&json!({ "script": "let xs = [1, 2, 3]; xs.len()" })) .await{ Ok(job) => job, Err(SdkError::Api { status, .. }) if status == 401 => return Err("bad API key".into()), Err(err) => return Err(err.into()),};
// ... poll until terminal (see "Create an execution and wait for the result") ...
match job.get("status").and_then(Value::as_str) { Some("failed") => { return Err(job .get("error") .and_then(Value::as_str) .unwrap_or("execution failed") .to_string() .into()) } Some("canceled") => return Err("execution was canceled".into()), _ => {}}
let n: i64 = job .get("result") .and_then(Value::as_i64) .ok_or("expected integer result")?;let _ = n;const submit = await fetch("https://openapp.house/api/v1/scripting/executions", { method: "POST", headers: { "content-type": "application/json", "x-api-key": "https://openapp.house_openapp_YOUR_SECRET", }, body: JSON.stringify({ script: "let xs = [1, 2, 3]; xs.len()" }),});const created = (await submit.json()) as { id?: string; message?: string };
if (!submit.ok) { // Body matches ApiErrorResponse — surface `created.message` to the user throw new Error(created.message ?? `submit failed: ${submit.status}`);}
const job = await waitForExecution( "https://openapp.house/api/v1", "https://openapp.house_openapp_YOUR_SECRET", created.id!,);if (job.status === "failed") throw new Error(job.error ?? "execution failed");if (job.status === "canceled") throw new Error("execution was canceled");
const n = job.result as number;Cancel an execution
Section titled “Cancel an execution”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"httpResp, err := client.ScriptingAPI.CancelScriptingExecution(ctx, executionID).Execute()if err != nil { return err}httpResp.Body.Close()client.scripting().cancel_execution(&id).await?;const res = await fetch(`${apiBase}/scripting/executions/${executionId}`, { method: "DELETE", headers: { "x-api-key": apiKey },});// 204 cancel accepted · 409 already terminal · 404 unknown to the callerif (!res.ok) { throw new Error(`cancel failed: ${res.status}`);}List recent executions
Section titled “List recent executions”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"])import ( "context" "fmt")
jobs, httpResp, err := client.ScriptingAPI.ListScriptingExecutions(ctx).Limit(20).Execute()if err != nil { return err}httpResp.Body.Close()
for _, job := range jobs { fmt.Println(job.GetId(), job.GetStatus(), job.GetCreatedAt())}use openapp_sdk::Client;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let recent = client.scripting().list_executions(Some(20)).await?;if let Some(jobs) = recent.as_array() { for job in jobs { println!("{} {}", job["id"], job["status"]); }}list_executions takes Option<i64> — pass None for the server default.
const res = await fetch(`${apiBase}/scripting/executions?limit=20`, { headers: { "x-api-key": apiKey },});const jobs = (await res.json()) as Array<{ id: string; status: string; created_at: string;}>;HTTP contract: API reference · OpenAPI JSON · OpenApp Scripting