Apps
Apps
Create your own HTML applications and integrate them into enneo
Warning
Preview Feature — Apps are currently in the preview phase. Functionality and API may still vary. Feedback is appreciated!
Tip
Apps allow creating your own HTML applications directly in enneo and running them. Use apps for custom dashboards, reports, forms, or tools — all within the familiar enneo interface.
Overview
Apps are user-defined applications that output HTML and are displayed directly in enneo as their own page. Every app is executed in a sandbox and has access to the enneo API.
Typical use cases:
- Custom dashboards — visualizing real-time data from the ERP or CRM
- Reports and evaluations — Tailor-made reports with proprietary logic
- Internal tools — Forms, calculators, or workflow helpers
- Data queries — Showing contract data, customer status, or statistics
Structure of an App
Every app consists of:
| Component | Description |
|---|---|
| Name | Display name in the sidebar and in the settings |
| Slug | Unique technical identifier (e.g. my-dashboard). Used in the URL |
| Description | Optional description of the purpose of the app |
| Vendor | Optional: Who created the app |
| Executor | The code that is executed when called (Python, Node.js, or PHP) |
Executor
The executor uses the standard executor format from enneo (same structure as for settings and AI agents). It currently supports the source code type with Python 3.11, Node.js 20, and PHP 8.2 languages.
The output to stdout is returned as HTML to the browser.
# 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>";Input Parameters
With each call, the code automatically receives metadata as parameters via STDIN:
{
"_metadata": {
"userId": 1,
"clientId": "1",
"appId": "a1b2c3d4-...",
"appRevision": 3,
"userAuthToken": "Bearer ..."
}
}Additional GET or POST parameters are automatically passed and are available in the code.
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) |
SDK | Path to the SDK file |
Using Apps
Sidebar
As soon as at least one app exists and the user has the readApps permission, the Apps menu item appears
in the sidebar. Here all available apps are displayed as tabs.
The browser's app URL uses the slug of the app:
/apps/my-dashboardCalling an App
Each App is executed in an iFrame within enneo. The API URL accepts both UUID and Slug:
/api/mind/app/{slug}/run
/api/mind/app/{appId}/runGET parameters can be attached directly:
/api/mind/app/my-dashboard/run?param1=value1¶m2=value2App Storage
Apps can store and read persistent data. The App Storage is a key-value store, which is isolated per app. The values are stored as JSON.
Warning
The App Storage is common storage of the app, not 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 this app. Do not store any passwords, tokens or
personal data there — access data belongs in the
Secrets and are referenced from there via
{{secret.KEY}}.
Access Paths
| Path | Who | What's it for |
|---|---|---|
SDK (AppStorage) from the app code | The app itself, under the account of the executor | The intended way |
REST endpoints /api/mind/app/{appId}/data… | Users with the manageAppData permission — by default, only admin | Viewing and correcting by hand |
The SDK does not require manageAppData: It accesses the storage under the account of the executor,
not under that of the logged-in user. An agent can therefore use the storage via the app, but cannot read or change it directly via the API.
SDK Access
# Save value
sdk.AppStorage.save("counter", 42)
sdk.AppStorage.save("config", {"theme": "dark", "lang": "en"})
# Read value
counter = sdk.AppStorage.get("counter") # → 42
config = sdk.AppStorage.get("config") # → {"theme": "dark", "lang": "en"}
# Read 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 value
sdk.AppStorage.delete("counter")<?php
// Save value
AppStorage::save("counter", 42);
AppStorage::save("config", ["theme" => "dark", "lang" => "en"]);
// Read value
$counter = AppStorage::get("counter"); // → 42
$config = AppStorage::get("config"); // → {"theme": "dark", "lang": "en"}
// Read value with metadata
$meta = AppStorage::getMeta("counter");
// List all keys
$keys = AppStorage::listKeys();
// Delete value
AppStorage::delete("counter");App Info
The AppInfo class can be used to retrieve app 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();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] |
User-specific data and protected values
If an app needs to separate data per user or show values only to certain users, this cannot be solved via the App Storage — it doesn't know users. Instead, put the rule in a User-Defined Function (UDF) that the app calls:
- The app calls the UDF. The call runs under the logged-in user.
- The UDF determines the caller by querying the enneo API with its authorization.
- Only after this, the UDF — under the account of the executor — accesses the storage and only returns what this user is allowed to see.
# In the UDF: Determine caller via the API, not from the parameters```python
profil = sdk.ApiEnneo.get('/api/mind/profile') # runs under the calling user
user_id = profil['id']
permissions = profil['permissions']
# Only after this, 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 true:
- Determine the caller exclusively via the API call, never from the parameters. Values
like
_metadata.userIdcomes from the calling code and can be set at will.ENNEO_USER_IDis only available for apps, not UDFs. - The UDF must be executable for the affected users; otherwise, they cannot reach it. The entire rights check thus lies in the code of the UDF - handle it accordingly.
Manage Apps
Management is under Settings → Advanced Settings → Apps.
Create New App
- Navigate to Settings → Apps
- Click on Create App
- Enter name, slug, and optionally description
- Write the executor code
- Use the Preview to check the output
- Click on Save
Edit App
Each change automatically creates a new Revision. This allows you to revert to a previous version at any time.
Unsaved changes are shown by a Draft tag in the sidebar. A warning appears when navigating with unsaved changes.
Export and Import
Apps can be exported and imported as JSON. The export button is in the Specifications tab. This allows for transfer between environments.
Revisions and Rollback
Apps support versioning:
- Each save creates a new revision
- You can return to any past version via the revisions overview
- The active revision is shown to users
Permissions
| Permission | Description | Default Roles |
|---|---|---|
readApps | View and execute apps | Admin, Agent |
appCreator | Create, edit, and preview apps | Admin |
manageApps | Import and delete apps | Admin |
manageAppData | Read, write, and delete app storage directly via the API | Admin |
Examples
Dashboard with Contract Data
import importlib.util, os, json, sys
# Load 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()
metadata = input_data.get('_metadata', {})
# 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 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 counter from App Storage
try:
counter = sdk.AppStorage.get("page_views")
except Exception:
counter = 0
# Increment counter and save
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 accessed <strong>{counter}</strong> times.</p>
<p><em>The value is stored in the app storage and remains 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 planned for the weekend.
</div>
</body>
</html>
""")Tip
Apps have access to the same enneo SDK, which is used for rule-based AI agents. This allows you to query contracts, read tickets, retrieve settings, and call external APIs. For more information, see the SDK documentation.
API Reference
App Management
| Method | Endpoint | Description |
|---|---|---|
GET | /api/mind/app | List all apps |
POST | /api/mind/app | Create new app |
GET | /api/mind/app/{appId} | Fetch app details (UUID or slug) |
PATCH | /api/mind/app/{appId} | Update app (new revision) |
DELETE | /api/mind/app/{appId} | Delete app |
GET | /api/mind/app/{appId}/revisions | View all revisions |
POST | /api/mind/app/{appId}/rollback/{revision} | Rollback to revision |
POST | /api/mind/app/{appId}/preview | Execute preview |
POST | /api/mind/app/import | Import app |
GET/POST | /api/mind/app/{appId}/run | Execute app (HTML response) |
App Storage
| Method | Endpoint | Description |
|---|---|---|
GET | /api/mind/app/{appId}/data | List all keys |
GET | /api/mind/app/{appId}/data/{key} | Read value |
GET | /api/mind/app/{appId}/data/{key}/meta | Read value with metadata |
PUT | /api/mind/app/{appId}/data/{key} | Save/update value |
DELETE | /api/mind/app/{appId}/data/{key} | Delete key |
All endpoints accept both the app UUID and the slug as {appId}.
These five endpoints require the manageAppData permission. From within the app code, use
the SDK (AppStorage) instead — it does not require the permission.
Data Format (executorUser)
The executor object in data.executorUser follows the standard executor format:
{
"id": 1,
"code": "print('<h1>Hello</h1>')",
"type": "sourceCode",
"language": "python311",
"packages": null,
"parameters": [],
"parameterDefinitions": []
}