Apps
Develop Apps
An app's code, input parameters, environment variables, and App Storage
An app is code that runs on every call and outputs HTML. Where you create and save the code is described in Manage Apps.
Code
The code (technically: the executor) uses enneo's standard executor format (the same structure as in settings and AI agents). It currently supports the Source code type with the languages Python 3.11, Node.js 20, and PHP 8.2. A new app starts with a Python template; you change the language in the Code step using the Language field.
The output on stdout is returned to the browser as HTML.
# Simple app: welcome page
print("<h1>Welcome</h1>")
print("<p>This is my first enneo app.</p>")// Simple app: welcome page
console.log("<h1>Welcome</h1>");
console.log("<p>This is my first enneo app.</p>");<?php
echo "<h1>Welcome</h1>";
echo "<p>This is my first enneo app.</p>";Tip
Apps have access to the same enneo SDK that is used for rule-based AI agents. With it, you can query contracts, read tickets, retrieve settings, and call external APIs. For more information, see the SDK documentation.
Calling an app
Every app runs in an iFrame within enneo. The API URL accepts both the UUID and the slug:
/api/mind/app/{slug}/run
/api/mind/app/{appId}/runGET parameters can be appended directly:
/api/mind/app/my-dashboard/run?param1=value1¶m2=value2Input parameters
The code receives via STDIN exactly the parameters sent with the call: the parameters from the address
and the body of a POST request. A call to /apps/my-dashboard?customer=123, for example, delivers:
{
"customer": "123"
}The body of a POST request must be valid JSON; otherwise the call fails with an error message. Values from the address and the POST request are merged.
Information about the user and the app is not part of the parameters; it is available in the environment variables.
Environment variables
The following environment variables are available in the app code:
| Variable | Description |
|---|---|
ENNEO_API_URL | Base URL of the enneo API |
ENNEO_SESSION_TOKEN | Session token for API calls |
ENNEO_USER_AUTH_HEADER | Auth header of the calling user |
ENNEO_APP_ID | UUID of the app |
ENNEO_APP_SLUG | Slug of the app |
ENNEO_APP_VERSION | Version of the app (e.g. 1.0.0) |
ENNEO_APP_REVISION | Number of the active version in the version history |
ENNEO_USER_ID | ID of the logged-in user who opened the app |
ENNEO_CLIENT_ID | ID of the client |
ENNEO_USER_AUTH_TOKEN | Credentials (Authorization header) with which the app was opened |
SDK | Path to the SDK file |
App info
The AppInfo class provides the app's metadata:
app_id = sdk.AppInfo.get_id() # UUID of the app
app_slug = sdk.AppInfo.get_slug() # Slug of the app
app_version = sdk.AppInfo.get_version() # Version (e.g. "1.0.0")<?php
$appId = AppInfo::getId();
$appSlug = AppInfo::getSlug();
$appVersion = AppInfo::getVersion();const { AppInfo } = require(process.env.SDK || 'sdk.js');
const appId = AppInfo.getId(); // UUID of the app
const appSlug = AppInfo.getSlug(); // Slug of the app
const appVersion = AppInfo.getVersion(); // Version (e.g. "1.0.0")The AppInfo functions return their value immediately; no await is needed here, even in Node.js.
App Storage
Apps can store and read persistent data. The App Storage is a key-value store isolated per app. Values are stored as JSON.
Warning
The App Storage is the app's shared storage, not a personal storage of its users. There is no
separation by user and no owner per key: any code of the app reads and overwrites all keys of that
app. Do not store passwords, tokens, or personal data there — credentials belong in the
Secrets and are referenced from there via
{{secret.KEY}}.
Access paths
| Path | Who | Intended for |
|---|---|---|
SDK (AppStorage) from the app code | The app itself, under the executor's account | The intended path |
REST endpoints /api/mind/app/{appId}/data… | Users with the manageAppData permission — by default only Admin | Viewing and correcting by hand |
| Data step in the app's editor | Users with the manageAppData permission — by default only Admin | Viewing, without the option to change |
The SDK does not need manageAppData: it accesses the storage under the executor's account, not under
that of the logged-in user. An agent can therefore use the storage through the app, but cannot read or
change it directly via the API.
The Data step in the editor lists all of the app's entries with Created, Modified, and Last access; clicking an entry shows its value. The view is meant for checking and troubleshooting — the entries cannot be changed there.
SDK access
# Save a value
sdk.AppStorage.save("counter", 42)
sdk.AppStorage.save("config", {"theme": "dark", "lang": "en"})
# Read a value
counter = sdk.AppStorage.get("counter") # → 42
config = sdk.AppStorage.get("config") # → {"theme": "dark", "lang": "en"}
# Read a value with metadata
meta = sdk.AppStorage.get_meta("counter")
# → {"key": "counter", "value": 42, "createdAt": "...", "modifiedAt": "...", "lastAccessAt": "..."}
# List all keys
keys = sdk.AppStorage.list_keys()
# → [{"key": "counter", "createdAt": "...", ...}, ...]
# Delete a value
sdk.AppStorage.delete("counter")<?php
// Save a value
AppStorage::save("counter", 42);
AppStorage::save("config", ["theme" => "dark", "lang" => "en"]);
// Read a value
$counter = AppStorage::get("counter"); // → 42
$config = AppStorage::get("config"); // → {"theme": "dark", "lang": "en"}
// Read a value with metadata
$meta = AppStorage::getMeta("counter");
// List all keys
$keys = AppStorage::listKeys();
// Delete a value
AppStorage::delete("counter");const { AppStorage } = require(process.env.SDK || 'sdk.js');
(async () => {
// Save a value
await AppStorage.save("counter", 42);
await AppStorage.save("config", { theme: "dark", lang: "en" });
// Read a value
const counter = await AppStorage.get("counter"); // → 42
const config = await AppStorage.get("config"); // → { theme: "dark", lang: "en" }
// Read a value with metadata
const meta = await AppStorage.getMeta("counter");
// List all keys
const keys = await AppStorage.listKeys();
// Delete a value
await AppStorage.delete("counter");
})();Note
In Node.js, the AppStorage functions are asynchronous. Put await before every call; otherwise you
get a Promise (a placeholder for the later result) instead of the value.
Supported value types
| Type | Example |
|---|---|
| String | "hello world" |
| Number | 42, 3.14 |
| Boolean | true, false |
| Null | null |
| Object | {"key": "value"} |
| Array | [1, "two", 3.0] |
Key names may only contain letters, digits, hyphens (-), underscores (_), and dots (.) — for
example my_notes, but not my notes.
User-specific data and protected values
If an app should separate data per user or show values only to certain users, the App Storage cannot do this — it does not know about users. Put the rule into a user-defined function (UDF) that the app calls instead:
- The app calls the UDF. The call runs as the logged-in user.
- The UDF determines the caller by querying the enneo API with the caller's authorization.
- Only then does the UDF access the storage — under the executor's account — and return only what this user is allowed to see.
# In the UDF: determine the caller via the API, not from the parameters
profile = sdk.ApiEnneo.get('/api/mind/profile') # runs as the calling user
user_id = profile['id']
permissions = profile['permissions']
# Only then access the shared storage
notes = sdk.ApiEnneo.get(
f'/api/mind/app/{app_id}/data/notes',
authorizeAs='serviceWorker',
)['value']
return notes.get(str(user_id), {})Warning
Two points decide whether this check holds:
- Determine the caller exclusively via the API call, never from the parameters. Values such as
_metadata.userIdcome from the calling code and can be set arbitrarily.ENNEO_USER_IDis only available to apps, not to UDFs. - The UDF must be executable for the users concerned, otherwise they cannot reach it. The entire permission check therefore lives in the UDF's code — treat it accordingly.
Examples
Dashboard with contract data
import importlib.util, os, json, sys
# Load the SDK
file_path = os.getenv('SDK', 'sdk.py')
spec = importlib.util.spec_from_file_location('sdk', file_path)
sdk = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sdk)
# Load input data
input_data = sdk.load_input_data()
# Load contract data (example)
contract_id = input_data.get('contractId', '715559')
try:
contract = sdk.ApiEnneo.get_contract(contract_id)
name = f"{contract.get('firstname', '')} {contract.get('lastname', '')}"
status = contract.get('status', 'Unknown')
except Exception:
name = "Not found"
status = "-"
# Output HTML
print(f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; }}
.card {{ background: #f5f5f5; padding: 16px; border-radius: 8px; margin: 8px 0; }}
h1 {{ color: #333; }}
</style>
</head>
<body>
<h1>Contract overview</h1>
<div class="card">
<strong>Name:</strong> {name}<br>
<strong>Status:</strong> {status}<br>
<strong>Contract number:</strong> {contract_id}
</div>
</body>
</html>
""")App with a persistent counter
import importlib.util, os, json, sys, html
file_path = os.getenv('SDK', 'sdk.py')
spec = importlib.util.spec_from_file_location('sdk', file_path)
sdk = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sdk)
input_data = sdk.load_input_data()
# Load the counter from App Storage
try:
counter = sdk.AppStorage.get("page_views")
except Exception:
counter = 0
# Increase and save the counter
counter += 1
sdk.AppStorage.save("page_views", counter)
print(f"""
<!DOCTYPE html>
<html>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h1>Visitor counter</h1>
<p>This page has been viewed <strong>{counter}</strong> times.</p>
<p><em>The value is stored in the App Storage and persists across page views.</em></p>
</body>
</html>
""")Static info page
print("""
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }
.info { background: #e8f5e9; padding: 16px; border-radius: 8px;
border-left: 4px solid #4caf50; }
.warning { background: #fff3e0; padding: 16px; border-radius: 8px;
border-left: 4px solid #ff9800; }
</style>
</head>
<body>
<h1>Important information</h1>
<div class="info">
<strong>Service hours:</strong> Mon–Fri, 8:00 am – 6:00 pm
</div>
<br>
<div class="warning">
<strong>Note:</strong> Maintenance work is planned for the weekend.
</div>
</body>
</html>
""")API
All endpoints for apps, App Storage, and the App Store, with their parameters and permissions, are listed in the API reference.
An app's code object is stored in data.executorUser and follows the standard executor format:
{
"id": 1,
"code": "print('<h1>Hello</h1>')",
"type": "sourceCode",
"language": "python311",
"packages": null,
"parameters": [],
"parameterDefinitions": []
}