App
An App is a self-contained npm package that you develop, publish, and deploy to an Operaide instance. It bundles everything needed to run one or more automation workflows: the Reaktors that define the logic, an optional custom UI, optional database resources, and quality checks.
When you deploy an App, you create an App Instance — a running version of the App with its own configuration. You can deploy the same App multiple times, each instance independently configured (e.g. different AI providers, different system prompts, different databases).
Development and Production App Instances
An App runs in one of two contexts, and the platform keeps them in separate sections.
A development instance is your own build, deployed for testing. Running npm run deploy tags the build as a dev-channel version (a -dev suffix) and deploys it. Development instances appear under Development Apps. Use them while you build and test an App.
A production instance runs a released version installed from the App Store. After you publish an App, an administrator installs it, which creates a production instance. Production instances appear under Production Apps.
Both sections open the same detail pages: overview, Reaktors, settings, and tracing. While you view a development instance, the header shows an App in Development badge so the context is clear.
| Aspect | Development Apps | Production Apps |
|---|---|---|
| Source | npm run deploy (your build) | App Store (published version) |
| Version | dev-channel (-dev) | released version |
| Created by | you, the developer | an administrator on install |
| Purpose | build and test | run for end users |
To publish an App for production install, see App Store.
App Structure
Every Operaide App is a standard npm package with a small set of conventions on top. Here is the full layout including the optional App UI:
my-app/
├── package.json # npm package manifest
├── tsconfig.json # TypeScript compiler config
├── src/
│ ├── main.ts # Entry point — imports all reaktor files
│ ├── app.reaktor.ts # Reaktor definition(s)
│ ├── aktors.ts # Composed Aktors (intermediate nodes)
│ └── aktorFunctions.ts # Leaf AktorFunctions (plain logic)
└── public/ # Optional: App UI (static files)
├── index.html
├── app.js
├── styles.css
└── .operaide-external-resources.json # Optional: CDN allowlist
package.json
The manifest identifies the App to Operaide, declares its dependencies, and wires up the standard scripts:
{
"name": "operaide-app-my-app",
"version": "1.0.0",
"description": "What this app does",
"main": "src/main.ts",
"scripts": {
"deploy": "operaide-upload-code",
"deploy-watch": "operaide-upload-code --watch",
"check": "npm run check:eslint && npm run check:prettier && npm run check:tsc",
"fix": "npm run fix:eslint && npm run fix:prettier"
},
"peerDependencies": {
"@operaide/aktor": "^0.5.7"
},
"operaide": {
"category": "demo",
"quality": "reference",
"notes": "Short description for the App Store catalog."
}
}
Key points:
mainpoints tosrc/main.ts— this is what Operaide loads.peerDependencieslists the@operaide/*packages your App uses. The platform provides them at runtime; you do not bundle them yourself.operaideis a metadata block used by the App Store:categorygroups the App,qualitysignals how mature it is, andnotesis a short description for the catalog.deployrunsoperaide-upload-code, which uploads your source to the platform and triggers a rebuild.
src/main.ts
The entry point only needs to import your reaktor files. The act of importing them executes registerReaktorDefinition(), which registers your Reaktors with the platform:
import './app.reaktor';
// import './another.reaktor'; // add more reaktor files here
src/*.reaktor.ts
Each file registers one or more Reaktors. By convention, these files are named <feature>.reaktor.ts:
import { aktorSetting, createAktorComposition, registerReaktorDefinition } from '@operaide/aktor';
import { z } from 'zod';
import { aktorHelloWorld } from './aktors';
const aktorMyAppHello = createAktorComposition('aktorMyAppHello', ({ name }) => {
const reaktorName = aktorSetting(z.string().describe('Name'), 'Operaide');
return aktorHelloWorld({ name, reaktorName });
});
registerReaktorDefinition({
reaktorDefinitionId: 'my-app-hello',
label: 'Hello World',
description: 'Greets the user by name',
inputSchema: z.object({
name: z.string(),
}),
outputSchema: z.string(),
aktor: aktorMyAppHello,
});
aktorSetting(type, value, name?, customId?) declares a per-Reaktor setting that operators can override in the UI. name is the label shown to operators; customId is optional and pins the setting to a stable identifier so that saved values survive renames or refactors. Without customId, the setting is identified by position and name — changing either can detach it from previously saved values.
src/aktors.ts and src/aktorFunctions.ts
Split your logic into two layers:
aktors.ts— composed Aktors that wire inputs to AktorFunctions usingcreateAktorComposition.aktorFunctions.ts— leaf nodes that do the actual work usingcreateAktorFunction.
This separation keeps pure business logic in plain TypeScript functions (easy to test) and the Aktor wiring in a separate layer (easy to compose).
Optional: App UI (public/)
If your App needs a custom user interface, place static files in a public/ directory. Operaide serves them automatically at:
/app-ui/<package-name>@<version>/
The page is protected by the platform's login — unauthenticated users are redirected to the login page automatically.
Calling Reaktors from the UI
The platform injects two browser globals into every App UI page:
window.getOperaideAuthToken()— returns the current user's authentication token.window.OperaideAppInstance.apiBasePath— the base URL for this App Instance's REST API.
Use them to call your Reaktors from JavaScript:
async function callReaktor(reaktorName, input = {}) {
const token = window.getOperaideAuthToken();
const basePath = window.OperaideAppInstance?.apiBasePath;
const url = `${basePath}/reaktors/${reaktorName}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Operaide-Token': token,
},
body: JSON.stringify(input),
});
return response.json();
}
External CDN Resources
By default, the platform's Content Security Policy (CSP) blocks resources from external origins. To load CSS or JavaScript from a CDN, list the allowed origins in public/.operaide-external-resources.json:
{
"allowedOrigins": [
"https://cdn.jsdelivr.net"
]
}
The server reads this file at runtime to generate the correct CSP headers. The file itself is not served via HTTP.
The fastest way to start is to open the Blank App template from the App Store in Studio. All files are already in place — rename reaktorDefinitionId and label, replace the sample logic, and deploy.
For an App with UI, use the Hello World with UI template instead — it includes a working public/ directory with authentication and a Reaktor call already wired up.