Process-Engine App-Architektur
Stand: 2026-05-01
Die Process-Engine ist plugin-basiert. Apps (Crisis-Management, Konzept-Framework, n8n, Anleitung, ...) registrieren ihre Bausteine zentral, die Engine kennt sie nicht hart-kodiert. Drittanbieter-Apps koennen denselben Mechanismus nutzen — eigene Components ohne Frontend-Code-Aenderung.
Aufbau einer App
Jede App ist eine ProcessAppDefinition:
import type { ProcessAppDefinition } from '../../process-engine/app-registry.js';
export const MyApp: ProcessAppDefinition = {
appKind: 'my-app', // Eindeutig, wird auch in Template.appKind gespeichert
displayName: 'Mein Plugin', // UI-Sektion-Header im Editor
moduleKey: 'my-app-feature', // featureFlag aus prilog-module.json,
// null = System-App immer aktiv
componentKinds: [...], // siehe unten
uiHints: { editor: 'visual' }, // 'visual' | 'form' | 'wizard'
};Die App wird in process-engine/bootstrap.ts registriert:
defaultAppRegistry.register(MyApp, defaultComponentRegistry);ComponentKind — Bausteine deklarieren
Jeder Baustein im Editor ist ein ComponentKind:
export interface ComponentKind {
key: string; // Eindeutig, z.B. 'my-app.send-sms'
appKind: AppKind; // Bezug zur App
label: string; // Anzeige-Label
designer?: ComponentKindDesigner; // UI-Metadata (siehe unten)
validateConfig?(c): Record<string, unknown>; // Sanity-Check
onActivate?(ctx): Promise<ComponentExecutionResult>; // Server-side Effekt
onComplete?(ctx): Promise<void>;
}Designer-Metadata
Wird mit jedem Kind ans Frontend ausgeliefert. Damit muss kein React-Code pro Baustein in den Editor geschrieben werden.
{
icon: 'message-square', // Lucide-Name
color: 'emerald', // Farb-Token (siehe unten)
description: 'Sendet eine SMS via Anbieter X.',
canBeRoot: true, // false = braucht Parent (groupId)
defaultConfig: { // Initial-Config beim Erstellen
to: '',
body: '',
},
propertiesSchema: [ // Form-Felder im Properties-Panel
{ key: 'to', type: 'text', label: 'Telefonnummer', required: true, helpText: '+49...' },
{ key: 'body', type: 'longtext', label: 'Nachricht', rows: 3 },
],
}Verfuegbare Farb-Tokens
blue, emerald, red, amber, violet, pink, rose, yellow, zinc, gray, purple.
Verfuegbare Field-Types
| Type | Beschreibung | Spezielle Felder |
|---|---|---|
text | Einzeilige Eingabe | placeholder, required |
longtext | Mehrzeilige Eingabe | rows, placeholder |
number | Zahleneingabe | min, max, step |
boolean | Checkbox | — |
select | Dropdown | options: [{value, label}, ...] |
string-array | Liste von Strings (add/remove) | placeholder |
choice-options | [{label, value}, ...] Editor (fuer guide.choice) | — |
screen-ref | Dropdown der guide.screen Components im Template | — |
json | Roher JSON-Editor | rows |
color | Color-Picker | — |
Alle Felder unterstuetzen optional helpText — wird unter dem Eingabefeld als Hinweis angezeigt.
Lifecycle-Hooks
validateConfig
Wird beim Anlegen/Update der Component aufgerufen. Defensives Mapping, Default-Werte.
validateConfig(config: unknown) {
const c = asObject(config);
return {
to: asString(c.to) ?? '',
body: asString(c.body) ?? '',
};
},onActivate
Wird vom Engine aufgerufen, wenn die Component aktiv wird. Die Plugin-Logik:
async onActivate(ctx) {
const cfg = ctx.component.config;
// Side-Effect ausfuehren
await mySmsProvider.send(cfg.to, cfg.body);
return { status: 'completed' };
}Returncodes:
{ status: 'completed' }— Engine geht zum naechsten Component{ status: 'waiting' }— Component bleibt aktiv (z.B. wartet auf User-Input){ status: 'failed', error: '...' }— Fehler
Modul-Bezug — moduleKey
moduleKey verbindet die App mit einer ModuleRegistration in der Plattform:
null= System-App, immer aktiv (z.B.flow-core,guide)'crisis-management'= nur aktiv wenn der Tenant das Crisis-Management-Modul installiert hat (sieheprilog-module.jsonder entsprechenden Modul-Implementierung)
Der Endpoint GET /platform/v1/process/components/kinds filtert die ausgelieferten Kinds anhand der aktiven Module. Sieht ein Tenant nicht das Crisis-Modul, kommen crisis.* Components ueberhaupt nicht in den Editor.
Bootstrap-Reihenfolge
// process-engine/bootstrap.ts
export async function bootstrapProcessEngineApps(): Promise<void> {
if (bootstrapped) return;
bootstrapped = true;
defaultAppRegistry.register(FlowDesignerApp, defaultComponentRegistry);
defaultAppRegistry.register(ConceptFrameworkApp, defaultComponentRegistry);
defaultAppRegistry.register(CrisisApp, defaultComponentRegistry);
defaultAppRegistry.register(N8nApp, defaultComponentRegistry);
defaultAppRegistry.register(FlowCoreApp, defaultComponentRegistry);
defaultAppRegistry.register(GuideApp, defaultComponentRegistry);
await defaultAppRegistry.initAll();
}Wird beim Server-Start aufgerufen (in server.ts direkt nach Datenbank-Verbindung).
Frontend-Konsum
Der Web-Client laedt die Kinds via:
const { kinds, apps } = await flowsGateway.listKinds(jwt);kinds[] enthaelt nur die fuer den aktuellen Tenant verfuegbaren Components (gefiltert serverseitig).
apps[] listet die App-Sektionen mit appKind, displayName, moduleKey, isSystemApp — fuer Section-Headers im KindPicker.
Properties-Panel rendert kind.designer.propertiesSchema via <GenericPropertiesForm>. Keine Editor-Code-Aenderung pro neuem Kind.
Beispiel: Neue Drittanbieter-App
Datei: src/modules/whisper-app/process-engine-app.ts
import type { ComponentKind } from '../../process-engine/types.js';
import type { ProcessAppDefinition } from '../../process-engine/app-registry.js';
const transcribeKind: ComponentKind = {
key: 'whisper.transcribe',
appKind: 'whisper',
label: 'Audio transkribieren',
designer: {
icon: 'mic',
color: 'rose',
description: 'Wandelt eine Audio-Datei in Text via Whisper.',
defaultConfig: { audioUrl: '', language: 'de' },
propertiesSchema: [
{ key: 'audioUrl', type: 'text', label: 'Audio-URL', required: true },
{ key: 'language', type: 'select', label: 'Sprache', options: [
{ value: 'de', label: 'Deutsch' },
{ value: 'en', label: 'Englisch' },
]},
],
},
async onActivate(ctx) {
const cfg = ctx.component.config;
const result = await whisperApiClient.transcribe(cfg.audioUrl, cfg.language);
return { status: 'completed', variables: { transcript: result.text } };
},
};
export const WhisperApp: ProcessAppDefinition = {
appKind: 'whisper',
displayName: 'Whisper-Transkription',
moduleKey: 'whisper-transcription', // muss in ModuleRegistration mit diesem featureFlag existieren
componentKinds: [transcribeKind],
uiHints: { editor: 'visual' },
};Plus: prilog-module.json der Whisper-Modul mit featureFlag: 'whisper-transcription'. Plus Eintrag in bootstrap.ts. Fertig — der Editor kennt automatisch den neuen Baustein, mit Icon, Farbe, Properties-Panel.
Migrations-Hinweise
Apps die vor diesem Refactor existieren haben kein designer-Feld. Im Frontend wird ein Fallback-Icon (Hash) und Fallback-Farbe (gray) verwendet, kein Properties-Panel. Sie funktionieren weiter, sehen aber im Editor "nackt" aus.
Empfehlung: bei jeder Anpassung einer existierenden App das designer-Feld nachtragen.
Was bewusst NICHT in der App liegt
- Player (Anleitungen): liegt im Web-Client (
guide-player.tsx), weil er die Components rendert. Apps koennen aber viapropertiesSchemaUI-Hints geben. - Wartung der Edges: Edges sind Engine-Konzepte, nicht App-spezifisch.
- Permissions: liegen im Modul-System (
tenantModuleInstallation), nicht in der App-Definition.
Anschlussfaehig an
- App-Store-Vision (
project_app_store_vision.md) — Drittanbieter-Apps werden ueber den Marketplace verfuegbar gemacht - Process-Engine-Konzept (
prilog_docs/docs/umsetzung/process-engine-konzept.md) — Engine-Architektur insgesamt