Skip to content
OAOpenAppPhysical Security as a Service
Login

DSL (OpenApp Scripting)

The OpenApp DSL is a scripting language for creating, updating, listing, and deleting platform resources (users, orgs, devices, integrations, zones, entities). Scripts run with the permissions of the caller and are commonly used for provisioning, testing, and automation.

Execute a script from a file (.openapp is the conventional extension):

Terminal window
openapp dsl --file script.openapp

With template expansion (see Template expansion):

Terminal window
openapp dsl --file script.openapp --from-env-file .env
openapp dsl --file script.openapp --from-secret my-secret-id

Use the global --json flag to print only the script result as JSON (no success message).

Open a REPL to run DSL snippets:

Terminal window
openapp shell

At the openapp(dsl)> prompt you can type expressions and multi-line scripts; results are printed after each submission.

Authenticated POST /dsl/execute with JSON body:

{ "script": "let x = create(\"org\", #{ name: \"My Org\" }); x" }

The response is { "result": <last expression value as JSON> }.

  • Execute DSL: The caller must have the execute role in at least one organization (see Roles).
  • Per-operation: Each create, update, delete, or list is checked against the corresponding resource permission (e.g. orgs:create, devices:list) in the relevant org. If a step fails due to permissions, the script stops and earlier side effects are kept.

The DSL is Rhai. Summary:

  • Variables: let x = 10;
  • Map literal: #{ key: "value", num: 42 } (keys are strings; use #{} for empty map)
  • Array literal: [1, 2, 3]
  • Indexing: obj["key"], arr[0]
  • Control flow: if ... { } else { }, for x in array { }, while condition { }
  • Functions: fn name(a, b) { a + b }
  • Unit / null: () (Rhai’s unit type; often used like “no value”)
  • Comments: // line comment

Built-in constants in scope:

  • OPENAPP — string "1" (version marker)
  • TYPES — array of resource kind names: ["user", "org", "device", "integration", "zone", "entity"]

Resource kinds can be given in singular or plural and are normalized (e.g. "users""user").

FunctionDescription
create(kind, fields)Create a resource. Returns the created object.
get(kind, id, opts)Get one resource by ULID. opts is a map (see Output options). Returns the object or null if not found.
update(kind, id, patch)Update a resource by ULID. Returns the updated object.
delete(kind, id)Soft-delete a resource. Returns the deleted object.
purge(kind, id)Hard-delete a resource. Returns the purged object.
list(kind, opts)List resources. opts is a map (see Output options). Returns an array.

All IDs are ULID strings. Invalid ULIDs cause a runtime error.

For get(kind, id, opts) and list(kind, opts), opts can include:

KeyTypeDescription
include_deletedboolInclude soft-deleted resources (default: false).
only_deletedboolReturn only soft-deleted resources (default: false).
include_metadataboolInclude metadata in the result (default: false).

Example: list("entity", #{ include_metadata: true })

FunctionDescription
assert(condition)Fails the script if condition is false.
assert(condition, message)Same, with a custom error message.
sleep(seconds)Pause execution (integer seconds). Subject to max sleep limit.
sleep_ms(ms)Pause execution in milliseconds (max 30_000 ms).
ulid()Return a new ULID string.
toggle_entity(entity_id)Toggle an entity’s state via the integration (e.g. switch on/off). Requires entity permission.
has_image(kind, id)Return true if the resource already has an image in the media store. kind is device or entity. Use before upload_image for idempotent populate scripts.
upload_image(kind, id, url)Fetch image from URL and upload to resource. kind is device or entity; unsupported kinds are rejected. Requires devices:update or entities:update respectively.
  • create: name (string), email (string); optional: id (ULID), roles (map: org_id → array of role names). If roles is omitted, only root platform_admin can create the user.
  • update: name, email
  • roles format: #{ "<org_id>": ["admin", "users:create"], ... }
  • create: name (string); optional: description (string), id (ULID), parent_id (ULID, default root org).
  • update: name, description, parent_id
  • create: org_id (ULID), provider_type (string); optional: enabled (bool, default true), config (object). Secret config fields are stored in AWS Secrets Manager.
  • update: enabled, config
  • provider_type values (examples): home_assistant, knx, mqtt, palgate_cloud, shelly_cloud, shelly_websocket, virtual_budget, virtual_access, waveshare.
  • create: integration_id (ULID), name (string); optional: parent_zone_id (ULID), external_id (string).
  • update: name, parent_zone_id, external_id
  • create: org_id (ULID), name (string), integration_id (ULID); optional: external_id, device_metadata (object).
  • update: name, external_id, device_metadata
  • create: device_id (ULID), entity_type (string); optional: zone_id (ULID), name (string), external_id (string), metadata (object; string values only). channel_index is not supported on create — use metadata if needed.
  • update: zone_id, name, external_id, metadata. channel_index is not supported — use metadata.
  • entity_type values: switch, light, sensor

For some integrations, external_id is not allowed on entities; the engine will error if you set it.

Scripts can contain placeholders {{KEY}} where KEY is an identifier ([A-Za-z_][A-Za-z0-9_]*). When using the CLI with --from-env-file or --from-secret, placeholders are replaced before execution.

  • --from-env-file: Load key-value pairs from a dotenv file. Multiple --from-env-file paths are merged.
  • --from-secret: Load key-value pairs from an AWS Secrets Manager secret (SecretString must be a JSON object). Multiple --from-secret IDs are merged.

If any placeholder in the script has no value in the expansion map, the CLI returns an error listing the missing keys. Replacement is raw text (no extra quoting).

Example script snippet:

let base_url = "{{HOMEASSISTANT_BASE_URL}}";
let token = "{{HOMEASSISTANT_TOKEN}}";

Scripts can inline other files using the include directive:

include "path" // path relative to current script's directory
include "path.openapp" // .openapp extension optional
  • Path: Relative to the directory of the script containing the include (e.g. include "users/main" loads users/main.openapp from the same directory).
  • Processing: Handled at load time by the CLI, before template expansion. Included content is inlined and shares the same scope.
  • Extension: If the path has no extension or a different extension, .openapp is appended.
  • Cycles: Circular includes are detected and rejected.

For cross-module dependencies, include shared helpers and call them in order (paths are often under tooling/dsl/site/):

load_json "data/orgs.json" as orgs_seed_doc
include "../../tooling/dsl/site/orgs_seed"
include "shelly"
let orgs_out = provision_orgs_from_seed(orgs_seed_doc);
let shelly_out = run_shelly(orgs_out);

Scripts can load JSON or TOML files and bind them to variables. Paths are resolved relative to the entry-point script’s directory.

  • load_json "path" as var_name — Load a .json file. Path must end with .json.
  • load_toml "path" as var_name — Load a .toml file. Path must end with .toml. Use TOML when you need comments (e.g. documenting derived values like full emails).

Both directives are expanded at load time before template expansion. The file content is inlined as a Rhai map literal.

load_json "data/orgs.json" as orgs_seed_doc
load_toml "data/demo_users_seed.toml" as demo_users_seed_data
  • Operations: Max 200,000 operations per script (prevents runaway loops).
  • Expression/call depth: Bounded (128 / 64) to avoid stack overflow.
  • Sleep: sleep_ms is capped at 30,000 ms; sleep(seconds) is limited by the same effective cap.

Scripts run with the identity and permissions of the caller; there is no separate “DSL admin” bypass.