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.
Running DSL scripts
Section titled “Running DSL scripts”Execute a script from a file (.openapp is the conventional extension):
openapp dsl --file script.openappWith template expansion (see Template expansion):
openapp dsl --file script.openapp --from-env-file .envopenapp dsl --file script.openapp --from-secret my-secret-idUse the global --json flag to print only the script result as JSON (no success message).
Interactive shell
Section titled “Interactive shell”Open a REPL to run DSL snippets:
openapp shellAt 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> }.
Authorization
Section titled “Authorization”- Execute DSL: The caller must have the
executerole in at least one organization (see Roles). - Per-operation: Each
create,update,delete, orlistis 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.
Syntax (Rhai)
Section titled “Syntax (Rhai)”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").
Built-in functions
Section titled “Built-in functions”Resource operations
Section titled “Resource operations”| Function | Description |
|---|---|
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.
Output options
Section titled “Output options”For get(kind, id, opts) and list(kind, opts), opts can include:
| Key | Type | Description |
|---|---|---|
include_deleted | bool | Include soft-deleted resources (default: false). |
only_deleted | bool | Return only soft-deleted resources (default: false). |
include_metadata | bool | Include metadata in the result (default: false). |
Example: list("entity", #{ include_metadata: true })
Assertions and utilities
Section titled “Assertions and utilities”| Function | Description |
|---|---|
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. |
Resource kinds and fields
Section titled “Resource kinds and fields”- create:
name(string),email(string); optional:id(ULID),roles(map: org_id → array of role names). Ifrolesis omitted, only rootplatform_admincan 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
integration
Section titled “integration”- 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
device
Section titled “device”- create:
org_id(ULID),name(string),integration_id(ULID); optional:external_id,device_metadata(object). - update:
name,external_id,device_metadata
entity
Section titled “entity”- create:
device_id(ULID),entity_type(string); optional:zone_id(ULID),name(string),external_id(string),metadata(object; string values only).channel_indexis not supported on create — usemetadataif needed. - update:
zone_id,name,external_id,metadata.channel_indexis not supported — usemetadata. - entity_type values:
switch,light,sensor
For some integrations, external_id is not allowed on entities; the engine will error if you set it.
Template expansion
Section titled “Template expansion”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-filepaths are merged.--from-secret: Load key-value pairs from an AWS Secrets Manager secret (SecretString must be a JSON object). Multiple--from-secretIDs 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}}";Include directive
Section titled “Include directive”Scripts can inline other files using the include directive:
include "path" // path relative to current script's directoryinclude "path.openapp" // .openapp extension optional- Path: Relative to the directory of the script containing the
include(e.g.include "users/main"loadsusers/main.openappfrom 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,
.openappis 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_docinclude "../../tooling/dsl/site/orgs_seed"include "shelly"
let orgs_out = provision_orgs_from_seed(orgs_seed_doc);let shelly_out = run_shelly(orgs_out);Load data directives
Section titled “Load data directives”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.jsonfile. Path must end with.json.load_toml "path" as var_name— Load a.tomlfile. 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_docload_toml "data/demo_users_seed.toml" as demo_users_seed_dataLimits and safety
Section titled “Limits and safety”- Operations: Max 200,000 operations per script (prevents runaway loops).
- Expression/call depth: Bounded (128 / 64) to avoid stack overflow.
- Sleep:
sleep_msis 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.