Skip to content

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:

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

ts
defaultAppRegistry.register(MyApp, defaultComponentRegistry);

ComponentKind — Bausteine deklarieren

Jeder Baustein im Editor ist ein ComponentKind:

ts
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.

ts
{
    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

TypeBeschreibungSpezielle Felder
textEinzeilige Eingabeplaceholder, required
longtextMehrzeilige Eingaberows, placeholder
numberZahleneingabemin, max, step
booleanCheckbox
selectDropdownoptions: [{value, label}, ...]
string-arrayListe von Strings (add/remove)placeholder
choice-options[{label, value}, ...] Editor (fuer guide.choice)
screen-refDropdown der guide.screen Components im Template
jsonRoher JSON-Editorrows
colorColor-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.

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

ts
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 (siehe prilog-module.json der 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

ts
// 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:

ts
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

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 via propertiesSchema UI-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