Provision a building with OpenApp Scripting
Multi-site rollouts (campus wings, new apartment towers, demo environments) repeat the same steps: integrations, devices, entities, portals. OpenApp Scripting runs Rhai programs server-side as asynchronous executions — submit with POST /scripting/executions, then poll the job for the result — ideal for one-shot provisioning jobs and CI pipelines.
This guide is for platform engineers automating building setup. Syntax reference: OpenApp Scripting. Getting started: Scripting step.
Prerequisites
Section titled “Prerequisites”- An unscoped API key with scripting execute permission in at least one org. Scoped credentials (voice keys, keys with
scoped_roles, agent credentials) can’t run scripting. - A tested script in the interactive shell or a
.openappfile
Scripting does not take X-Org on the wire. Each operation names its own org (for example org_id on create), and OpenApp checks the same permission the matching HTTP route checks, in that resource’s org. Creates also count against plan quotas and are refused for a degraded org, the same as over HTTP.
Submit and poll
Section titled “Submit and poll”SDK
import asynciofrom pathlib import Path
job = await client.scripting.create_execution( script='entity_action("01HENTITY...", "switchable.open", #{})')
# Poll until terminal — bounded at 16 minutes (runtime cap is 15).TERMINAL = {"succeeded", "failed", "canceled"}deadline = asyncio.get_event_loop().time() + 960while job.get("status") not in TERMINAL: if asyncio.get_event_loop().time() >= deadline: raise TimeoutError(f"execution {job['id']} did not finish") await asyncio.sleep(2.0) job = await client.scripting.get_execution(job["id"])
print(job["status"], job["result"])
# Or read a .openapp file and submit its contents:result = await client.scripting.execute( script=Path("provision-wing-a.openapp").read_text())const TERMINAL = new Set(["succeeded", "failed", "canceled"]);
const submit = await fetch(`${apiBase}/scripting/executions`, { method: "POST", headers: { "x-api-key": apiKey, "content-type": "application/json", }, body: JSON.stringify({ script: 'entity_action("01HENTITY...", "switchable.open", #{});', }),});
let job = (await submit.json()) as { id: string; status: string; result?: unknown;};
const deadline = Date.now() + 960_000;while (!TERMINAL.has(job.status)) { if (Date.now() >= deadline) throw new Error("execution did not finish before the deadline"); await new Promise((r) => setTimeout(r, 2_000)); job = (await fetch(`${apiBase}/scripting/executions/${job.id}`, { headers: { "x-api-key": apiKey }, }).then((r) => r.json())) as typeof job;}console.log(job.status, job.result);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 mut job = client .scripting() .create_execution(&json!({ "script": r#"entity_action("01HENTITY...", "switchable.open", #{});"#, })) .await?;let id = job["id"].as_str().unwrap_or_default().to_string();
let deadline = Instant::now() + Duration::from_secs(960);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?;}println!("{} {:?}", job["status"], job["result"]);import ( "context" "errors" "time"
openapiclient "github.com/tomers/openapp-sdk/go")
body := *openapiclient.NewCreateScriptingExecutionRequest(`entity_action("01HENTITY...", "switchable.open", #{});`)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[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()}_ = job.GetResult()Replace the script body with your provisioning program (create integration, devices, entities — see reference for built-ins). Every SDK returns the job first; keep polling until status is terminal.
HTTP API (curl)
export OPENAPP_API_BASE='https://openapp.house/api/v1'export OPENAPP_API_KEY='https://openapp.house_openapp_YOUR_SECRET'
# 1. Submit — 202 Accepted with the execution job.EXECUTION_ID=$(curl -sS -X POST \ -H "X-API-Key: ${OPENAPP_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "script": "print(\"provision batch start\");" }' \ "${OPENAPP_API_BASE}/scripting/executions" | jq -r '.id')
# 2. Poll until terminal — bounded at 200 x 5s (runtime cap is 15 minutes).attempt=0while [ "$attempt" -lt 200 ]; do JOB=$(curl -sS -H "X-API-Key: ${OPENAPP_API_KEY}" \ "${OPENAPP_API_BASE}/scripting/executions/${EXECUTION_ID}") case "$(printf '%s' "$JOB" | jq -r '.status')" in succeeded|failed|canceled) break ;; esac attempt=$((attempt + 1)) sleep 5done
# 3. Read the outcome.printf '%s' "$JOB" | jq '{ status, result, error }'Example pattern
Section titled “Example pattern”// Adapt to your org and provider types (see the scripting reference).let org_id = "01HORG00000000000000000000";let site = create("integration", #{ org_id: org_id, provider_type: "virtual_access", name: #{ en: "Tower A" },});// create devices, entities, portals under site.id ...create("integration", …) needs org_id and provider_type; name is a localized map. See Resource kinds and fields.
Keep scripts idempotent where possible (check before create) and run against a staging org first.
When to use scripting vs SDK loops
Section titled “When to use scripting vs SDK loops”| Use scripting | Use SDK / HTTP directly |
|---|---|
| Many chained steps in one job | Single invite or door action |
| CI template copied per building | PMS webhook on each check-in |
Operator-maintained .openapp files | Production microservice |
Related
Section titled “Related”- Integrate existing software for ongoing PMS/SIS sync
- Agent-relevant API —
create_scripting_execution