Enneo

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}/run

GET parameters can be appended directly:

/api/mind/app/my-dashboard/run?param1=value1&param2=value2

Input 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:

VariableDescription
ENNEO_API_URLBase URL of the enneo API
ENNEO_SESSION_TOKENSession token for API calls
ENNEO_USER_AUTH_HEADERAuth header of the calling user
ENNEO_APP_IDUUID of the app
ENNEO_APP_SLUGSlug of the app
ENNEO_APP_VERSIONVersion of the app (e.g. 1.0.0)
ENNEO_APP_REVISIONNumber of the active version in the version history
ENNEO_USER_IDID of the logged-in user who opened the app
ENNEO_CLIENT_IDID of the client
ENNEO_USER_AUTH_TOKENCredentials (Authorization header) with which the app was opened
SDKPath 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

PathWhoIntended for
SDK (AppStorage) from the app codeThe app itself, under the executor's accountThe intended path
REST endpoints /api/mind/app/{appId}/data…Users with the manageAppData permission — by default only AdminViewing and correcting by hand
Data step in the app's editorUsers with the manageAppData permission — by default only AdminViewing, 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

TypeExample
String"hello world"
Number42, 3.14
Booleantrue, false
Nullnull
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:

  1. The app calls the UDF. The call runs as the logged-in user.
  2. The UDF determines the caller by querying the enneo API with the caller's authorization.
  3. 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.userId come from the calling code and can be set arbitrarily. ENNEO_USER_ID is 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": []
}