Reference

Jedes Tool, an einem Ort

Diese Seite ist die praktische Referenz für jedes Tool in der Sidebar deiner Archestack-Umgebung. Jeder Abschnitt behandelt, was das Tool tut, wann du danach greifst, die Schritte der gängigsten Workflows und die Stolperfallen, in die neue Nutzer tappen. Der letzte Abschnitt (Aus Skripten aufrufbare APIs) dokumentiert die exakte Oberfläche, die Skripte aufrufen können.

Schema Designer

Ein visueller Editor für das Datenbankschema, Tabellen, Spalten, Typen, Beziehungen, Indizes, der automatisch in ein JSON-Dokument speichert. Das Schema ist die Single Source of Truth für deine Anwendung; kein anderes Tool sieht eine Spalte, bis du sie hier anlegst und deployst.

Gängiger Workflow: Klick in der Symbolleiste auf Add Table (Icon-Button). Es öffnet sich ein Dialog mit dem Titel "Create New Table"; tippe einen Namen ein (der Hilfetext weist auf "Lowercase with underscores recommended" hin), klicke auf Create. Die Tabelle erscheint auf dem Canvas mit einem automatisch erzeugten id-SERIAL-Primärschlüssel. Wähle die Tabelle aus, das rechte Panel öffnet sich auf dem Columns-Tab. Füge Spalten über die gestrichelte "Add column"-Box unten hinzu (Namensfeld, Type-Dropdown, auf Add Column klicken). Klicke jede Spalte an, um sie aufzuklappen, und passe die Schalter (PK, NULL, UQ), Length und Default value an.

Spaltentypen verfügbar im Type-Dropdown: SERIAL, BIGSERIAL, INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, REAL, DOUBLE PRECISION, VARCHAR, CHAR, TEXT, BOOLEAN, DATE, TIME, TIMESTAMP, TIMESTAMPTZ, UUID, JSON, JSONB, BYTEA, INET, CIDR, MACADDR, MONEY, INTERVAL, plus geometrische und Array-Typen. Length ist nur für VARCHAR und CHAR relevant.

Fremdschlüssel leben im Relations-Tab des rechten Panels (nicht "Relationships"). Scrolle zu "Add relationship": Wähle die FK-Spalte auf der aktuellen Tabelle, den Beziehungstyp (One-to-One / One-to-Many / Many-to-Many), die referenzierte Tabelle, die referenzierte Spalte und klicke auf Add Relationship. Jede Beziehung hat On Delete- und On Update-Dropdowns (CASCADE / SET NULL / SET DEFAULT / RESTRICT / NO ACTION).

Auto-save: Die Anzeige unten links wechselt zwischen "Auto saving…" und "Saved". Es gibt auch einen Save-Button in der Symbolleiste (nur aktiv, wenn es ausstehende Änderungen gibt) für explizites Speichern.

Andere Symbolleisten-Buttons: Add Group (visuelle Container für verwandte Tabellen, Properties-Panel mit dem Titel "Group"), Add Text (freie Annotationen, Properties-Panel mit dem Titel "Text Annotation"), Zoom-Controls, Package-Filter, Tabellensuche.

Stolperfallen: Eine Spalte umzubenennen erzeugt einen destruktiven Plan (drop + add), bearbeite das SQL auf dem Generated SQL-Tab des Deployments, um RENAME COLUMN zu verwenden, wenn du Daten erhalten willst. Eine NOT NULL-Spalte ohne Default zu einer nicht-leeren Tabelle hinzuzufügen schlägt fehl, mach sie nullable, befülle nach, dann altere. Der Deploy-Symbolleisten-Button bringt dich zu einer Deployment-Konfigurationsseite, nicht direkt in ein Deploy.

Database Deployments

Listet jedes Deployment mit seinem Status (Draft / Executing / Succeeded / Failed) und führt neue aus. Route: /database-deployments/overview.

Oben rechts: Refresh (Liste neu laden), New Deployment (einen neuen Entwurf erstellen).

Aus dem Schema Designer: Der grüne Deploy-Button mit Raketensymbol in der Schema Designer-Symbolleiste bringt dich zu /database-deployments/configure/new, einer frischen Deployment-Konfigurationsseite.

Deployment-Konfigurationsseite: Zwei Tabs.

  • Configuration - Pre-/Post-Deployment-Skript-Editoren (PostgreSQL) mit je einem Test Run-Button. Pre-Skript läuft vor Schemaänderungen; Post-Skript danach. Nützlich für Befüllungen, Index-Wiederaufbau oder Staging-Bereich-Bereinigung. Plus ein Force Deploy (Allow Data Loss)-Toggle für destruktive Operationen.
  • Generated SQL - zeigt die Migration, die die Plattform erzeugt hat. Das SQL ist editierbar. Ein Regenerate-Button (Refresh-Icon) führt das Diff neu aus, falls du seit dem Öffnen dieser Seite Schemaänderungen gemacht hast.

Obere Buttons: Back, Save (speichert die Konfiguration als Draft), Deploy (führt das Deployment sofort aus). Deploy ist das, was du zuletzt klickst.

Stolperfallen: Deployments sind nicht automatisch in eine Transaktion eingewickelt, ein Fehler auf halbem Weg lässt die Datenbank in einem teilweisen Zustand zurück. Für riskante Migrationen wickle deine Statements in BEGIN; … COMMIT; im Pre-Skript ein. Der Force Deploy-Toggle überspringt Sicherheitsprüfungen; nutze ihn bewusst.

Business Entities

Kuratierte Sichten, gebaut auf einer Master-Tabelle plus Joins von verwandten Tabellen. Pages binden an Business Entities, nie an rohe Tabellen. Siehe Concepts → Business Entities für das Warum.

Gängiger Workflow: Klicke auf Create. Der ganzseitige Editor öffnet sich mit den Tabs Visual, JSON, Events. Setze Entity Name, Master Table (Autocomplete) und Label Column (die Spalte, deren Wert einen Datensatz in Pickern repräsentiert). Speichern, die BE erzeugt automatisch eine Liste nativer Spalten aus der Master-Tabelle.

Joins hinzufügen: Scrolle zur Join Configurator-Karte und klicke auf Add Join. Jeder Join öffnet sich als Accordion mit: Join Type (INNER / LEFT / RIGHT), From Table, To Table (Autocomplete gruppiert nach Database Tables / Third Party), From Column, To Column, optionale Type Cast-Dropdowns und einen Spalten-Picker, welche Zielspalten herausgereicht werden sollen.

Aggregationen: Auf einem Join schalte Aggregate Mode ein. Für jede gewählte Spalte wählst du dann eine Aggregate Function: COUNT / SUM / AVG / MIN / MAX / COUNT DISTINCT. "Anzahl offener Kontakte zu dieser Company" ist das kanonische Beispiel.

Run Preview: Der Editor hat einen Run Preview-Button (mit einem Play-Icon), der die vollständige Join-Konfiguration ausführt und echte Zeilen in einem Datengrid zeigt. Wenn eine Join-Spalte leer zurückkommt, wurde die Spalte im Spalten-Picker des Joins nicht angekreuzt (oder die Beziehung ist falsch konfiguriert).

Mehrere BEs pro Master-Tabelle: Nichts hindert dich daran, customer (sales view) und customer (support view) auf demselben Master zu haben. Verschiedene Pages, verschiedene zielgruppengerechte Spalten, dieselbe zugrundeliegende Zeile.

Stolperfallen: Gejointe und aggregierte Spalten sind schreibgeschützt. Um das gejointe Label zu bearbeiten, navigiere zum Quelldatensatz. Aggregierte Spalten werden bei jeder Query neu berechnet, in Ordnung für hunderte Zeilen, langsam bei Millionen.

Page Editor

Konfiguriert die Laufzeit-UI, indem eine Page an eine Business Entity gebunden wird. Die Plattform erzeugt automatisch Abschnitte basierend auf den BE-Spalten; du feinjustierst das Layout von dort. Es gibt keinen separaten "Template-Picker", jede Page startet von derselben erzeugten Basis und du passt sie an.

Gängiger Workflow: Klicke auf Create. Fülle Page Name, Page Route (z. B. /companies), wähle die Business Entity (Autocomplete). Die Page öffnet sich im Editor mit den Tabs Visual, Overview, Create, Entities, Events, JSON. Justiere das automatisch erzeugte Layout, dann schalte den Published-Switch im oberen Header auf AN.

Field-Widget-Typen (das Type-Select auf jedem Feld): Text, Textarea, Number, Date, Select, Checkbox, Email. Nutze Select für Fremdschlüssel-Felder und setze das Entity-Autocomplete des Feldes auf die referenzierte BE, damit Nutzer per Label auswählen.

Tabs im Detailformular: Klicke auf Add Tab im Visual-Tab. Innerhalb eines Tabs kannst du eine RelatedGrid-Sektion hinzufügen, die an eine andere BE gebunden ist, gefiltert nach dem PK des aktuellen Datensatzes. Klassisches Beispiel: eine Company-Page mit "Contacts"- und "Deals"-Tabs, jeweils gefiltert nach company_id = current company's id.

Aktions-Buttons im Header-Inspector des Detailformulars. Klicke auf Add im Actions-Bereich. Jede Action hat Label, Icon (Save / Delete / Add / Refresh / Download / Bolt / None), Variant (Contained / Outlined / Text), Color (Primary / Secondary / Error / Success / Warning / Info) und Steps (eine geordnete Liste aus: Save Record, Delete Record, Navigate, Business Events, Custom). Um ein Business Event von einem Button auszuführen, setze den Step-Typ auf Business Events und wähle das Event per Name.

Publishing: Der Published-Switch im oberen Header ist die einzige Publish-Steuerung. AN = die Page erscheint unter Published Pages in der Sidebar und Endnutzer, die zu ihrer Route navigieren, sehen sie. AUS = Entwurf (nur du siehst deine Änderungen im Editor).

Stolperfallen: Die automatisch erzeugten Spalten auf einer frischen Page spiegeln die BE, wenn sich die BE ändert (du fügst einen Join hinzu), wird die Page neue Spalten nicht automatisch übernehmen. Lade die Page im Editor neu oder füge die Spalte manuell zur Sektion hinzu. Es gibt aktuell keinen "Generate PDF"-Action-Step-Typ, um ein PDF an einen Page-Button zu binden, gib das PDF über ein Script Module heraus, das den REST-Endpunkt aufruft, und nutze einen Custom-Step (oder rufe den Endpunkt direkt aus einem Frontend Template auf).

Business Events

Regeln, die auf Datenänderungen feuern (oder nach Zeitplan, oder manuell) und Actions ausführen, wenn Conditions erfüllt sind. Der "wenn X passiert, tu Y"-Mechanismus der Plattform. Siehe Concepts → Business Events für das Modell.

Gängiger Workflow: Klicke auf Create. Setze Rule Name, schalte Enabled. Wähle Business Entity. Kreuze eine oder mehrere Triggers an: BeforeCreate / BeforeUpdate / AfterCreate / AfterUpdate / BeforeDelete / OnSchedule / Manual / InitialValue. Baue den Condition-Baum (AND/OR-Gruppen). Füge eine oder mehrere Actions im FlowCanvas hinzu. Speichern.

Action-Typen (RuleActionKind):

  • Execute Script - führt ein C#-Skript aus. Die flexibelste Action; greife danach, wann immer du ein Feld setzen, eine Berechnung machen, eine API aufrufen oder irgendwas Eigenes tun willst. Das Skript bekommt Entity (mutiere es auf Before*-Triggern, um den laufenden Schreibvorgang zu ändern), OldEntity, Log, Db, Modules.
  • Validate - ein Skript, das bool zurückgibt. true bedeutet, dass die Validierung fehlschlägt und das Speichern mit der konfigurierten Fehlermeldung blockiert wird. Hebt optional bestimmte Spalten über errorColumns hervor.
  • Block Operation - stoppt die Operation hart mit einer konfigurierten Nachricht. Kein Skript.
  • Create Entity - fügt einen Datensatz in eine andere BE ein. Feldwerte unterstützen Template-Ausdrücke wie {{ Entity.column_name }} und {{ now() }}.
  • Update Entity - aktualisiert Datensätze in einer anderen BE, die auf einen Bedingungsfilter passen. Gleiche Template-Ausdrücke.
  • Delete Entity - löscht Datensätze, die auf eine Bedingung passen. Verweigert ohne Bedingung (Sicherheit).
  • Send Email / Send Webhook / Publish Event - im Schema als zukünftige Erweiterungen definiert; aktuell geloggt und übersprungen.

"Set a field"-Muster: Es gibt keine eigene Set Field-Action. Der Weg, ein Feld zu setzen, ist ein Execute Script auf einem Before*-Trigger, das Entity mutiert. Beispiel: Entity.title = ((string)Entity.title)?.Trim();. Die Plattform persistiert die modifizierte Entity als Teil des laufenden Schreibvorgangs, keine zusätzliche Query, kein Rekursionsrisiko.

Run Simulation: Der Simulate-Tab des Trigger-Editors hat einen Datensatz-Picker und einen Run Simulation-Button. Die Plattform führt deine Conditions und Actions gegen den gewählten Datensatz aus, ohne Änderungen zu persistieren: Schreiboperationen werden geprüft und festgehalten, aber nie ausgeführt, die eines Skripts über Db und Files eingeschlossen, daher erreicht nichts die Datenbank oder die Dateien des Workspaces und es feuert auch kein weiteres Business Event. Mails und Benachrichtigungen werden nicht verschickt. Alles andere, was ein Skript selbst tut, ist echt: HTTP-Aufrufe, System.IO und alles, was ein NuGet-Paket tut, etwa eine Verbindung zu einer anderen Datenbank. Das Ausgabe-Panel zeigt, welche Conditions zugetroffen haben und was jede Action getan hätte. Nutze das, um Fehlkonfigurationen zu fangen, bevor sie auf echte Daten treffen.

Template-Ausdrücke in der Action-Konfiguration nutzen {{ … }}-Klammern. Die tatsächlichen Tokens: {{ Entity.column }}, {{ OldEntity.column }}, {{ now() }} / {{ getdate() }}, {{ today() }}, {{ guid() }} / {{ newid() }}, {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}, Aggregate wie {{ SUM(column) }} in Join-Kontexten und Post-Substitutions-Arithmetik wie {{ Entity.quantity * Entity.price }}. Es gibt keinen {{ user.email }}-Token.

Condition-Operatoren: Equals, NotEquals, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, Like (= ILIKE), Contains (= ILIKE %value%), IsNull, IsNotNull, In (= ANY(...)).

Stolperfallen: Ein Trigger, der denselben Datensatz mutiert, den er beobachtet, feuert sich selbst erneut, wenn du Update Entity nutzt; nutze stattdessen Before Update + Execute Script + Entity.field = …. Zwei Trigger auf demselben Event feuern in Prioritätsreihenfolge. Deaktivierte Events erscheinen weiterhin in der Liste, der Enabled-Toggle ist getrennt von der Trigger-Konfiguration.

Script Modules

Wiederverwendbare C#-Skripte, zur Laufzeit von Roslyn kompiliert. Nehmen Parameter, fragen die Datenbank über den Db-Helper ab, geben einen Wert zurück. Aufrufbar aus Business Events, Scheduled Events und direkt aus dem Frontend.

Gängiger Workflow: Klicke auf Create. Der Editor öffnet sich mit den Tabs Edit, Parameters, Test, JSON. Benenne das Modul (PascalCase, z. B. RecalcOpenRevenue). Im Parameters-Tab klicke pro Parameter auf Add (Name, Type, Required, Default Value). Type-Optionen: string, int, decimal, double, bool, DateTime. Zurück zum Edit-Tab, schreibe C#. IntelliSense ist an.

Test-Panel: Der Test-Tab zeigt deine Parameter-Eingaben links und einen Run-Button (PlayArrow-Icon). Klicke Run; die rechte Seite zeigt Erfolg- oder Fehler-Indikator, Ausgabe-Log und den Rückgabewert (JSON-formatiert).

Der Db-Helper, siehe die vollständige Referenz in Script-aufrufbare APIs unten.

Aufruf aus einem Business Event: Füge eine Execute Script-Action hinzu, deren Body das Modul aufruft:

// The module is a method; its name and parameters are checked when the script compiles
await Modules.RecalcCompanyOpenDealValue(company_id: (int)Entity.company_id);

// Calling it by name still works
await Modules.CallAsync("RecalcCompanyOpenDealValue", new Dictionary<string, object?> {
    ["company_id"] = Entity.company_id
});

Aufruf aus einem Scheduled Event: Erstelle ein Event mit dem On Schedule-Trigger, setze den Cron, füge eine Execute Script-Action mit demselben Modules.CallAsync-Body hinzu.

Logging: Jeder Aufruf schreibt einen Eintrag in Event Logs mit Ausführungszeit, Parametern und Ergebnis (oder Fehler).

Stolperfallen: awaite immer Db-Aufrufe, Vergessen kompiliert, gibt aber einen Task zurück. Rückgaben werden als Objekt durchgereicht (nicht vom Framework JSON-serialisiert); für die Frontend-Nutzung gib anonyme Objekte mit primitiven Feldern zurück. Suggestion-on-commit-character ist im Editor absichtlich deaktiviert; nutze Tab zum Akzeptieren.

Shared Code

Code, den mehr als ein Skript oder eine Page braucht, in eigenen Dateien abgelegt. Der Name einer Datei ist ihr Pfad, etwa Sales/OrderSummary.cs oder components/StatusChip.tsx, und ihre Endung sagt, was sie ist: .cs ist C#, das jedes Skript verwenden kann, während .tsx, .ts, .jsx und .js Module sind, die von Pages, Frontend Templates und Code-Blöcken in Layouts importiert werden. Shared-Code-Dateien gehören wie jedes andere Artefakt zu Packages.

Gängiger Workflow: Öffne in der Sidebar unter Logic den Eintrag Shared Code und klicke auf New file. Tippe den Pfad ein; der Editor legt die Datei mit einem Beispiel ihrer Art an. Schreibe den Code und klicke auf Save.

C#-Dateien sind gewöhnliches C#: Namespaces, Records, Klassen und statische Helper. Jedes Skript (Script Modules, Business Events, Scheduled Events, PDF Data Scripts und Tests) kann ihre öffentlichen Typen verwenden. Ein Typ ohne Namespace ist über seinen Namen verfügbar; einer in einem Namespace braucht ein using im Skript oder ein global using in einer beliebigen Shared-Code-Datei, das dann für jedes Skript gilt. Eine Shared-Code-Datei sieht das typisierte Datenmodell (Order, WorkspaceDb), aber nicht die Globals eines Skripts. Ein Helper, der Db oder Log braucht, nimmt es deshalb als Argument entgegen.

// Shared code: Sales/OrderSummary.cs, ordinary C# every script can use
namespace Sales;

/// <summary>One order as the invoices and the dashboard report it.</summary>
public record OrderSummary(int Id, string Number, decimal Total);

public static class Vat
{
    public static decimal Add(decimal amount, decimal rate = 0.21m) =>
        Math.Round(amount * (1 + rate), 2);
}
// A script module using it
using Sales;

var open = await Db.Order.Where(o => o.Status == "Open").ToListAsync();
return open.Select(o => new OrderSummary(o.Id, o.Number, Vat.Add(o.Total ?? 0))).ToList();

Eine gespeicherte Änderung erreicht jedes Skript bei seinem nächsten Lauf. Der Editor prüft eine Shared-Code-Datei zusammen mit den anderen Shared-Code-Dateien, und Go to Definition auf einem Typ aus dem Shared Code öffnet in einem Skript die Datei, die ihn deklariert. Kompiliert der Shared Code nicht, schlagen die Skripte fehl, die ihn verwenden, und nennen die fehlerhafte Datei; Skripte, die ihn nicht verwenden, laufen weiter. Eine Tabellenänderung, nach der eine Shared-Code-Datei nicht mehr kompilieren würde, wird zusammen mit den betroffenen Skripten gemeldet, bevor das Deployment läuft.

Module werden aus @/ importiert, gefolgt von ihrem Pfad ohne Endung: Für components/StatusChip.tsx lautet der Import import { StatusChip } from "@/components/StatusChip". Eine Page kann neben ihren Ansichten auch eigene Dateien haben, die mit + über ihrem Code-Editor hinzugefügt und als ./OrderRow importiert werden. Ein Modul sieht dieselben Globals wie Page-Code und exportiert, was andere verwenden. Eine Ansicht, die importiert, darf statt mit return mit einem Default-Export ihrer Komponente enden. Code ohne import oder export läuft genau wie bisher.

// Shared code: components/StatusChip.tsx, a module pages import
export function StatusChip({ status }: { status: string }) {
  const color = status === "Open" ? "warning" : status === "Paid" ? "success" : "default";
  return <Chip size="small" color={color} label={status} />;
}
// A page's overview, with a file of its own, OrderRow.tsx, beside it
import { StatusChip } from "@/components/StatusChip";
import { OrderRow } from "./OrderRow";

export default function Orders() {
  const [rows, setRows] = useState([]);
  useEffect(() => {
    backend.getBusinessEntityData("Order").then((r) => setRows(r.rows.items));
  }, []);
  return (
    <Stack spacing={1}>
      {rows.map((row) => (
        <OrderRow key={row.id} row={row}>
          <StatusChip status={row.orders_status} />
        </OrderRow>
      ))}
    </Stack>
  );
}

Code kann die Pakete importieren, die die App bereits mitbringt, und ein Import ist dasselbe Objekt, das auch in den Globals der Page steckt: react, @mui/material, @mui/x-date-pickers, dayjs, lucide-react, three, @react-three/fiber und @react-three/drei. Andere Pakete gibt es dort nicht.

Publishing: Die veröffentlichte App liest die Module aus dem Shared Code in dem Stand, den das aktuelle Release festhält. Eine Änderung erreicht Endnutzer also erst, wenn das nächste Release veröffentlicht wird, wie bei einer Page. Ein Besucher, der nicht angemeldet ist, bekommt nur die Module, die von öffentlichen Pages und öffentlichen Layouts importiert werden.

Stolperfallen: Der Pfad ist in jedem Workspace die Identität einer Datei, und Imports nennen ihn. Benennst du eine Datei um, funktionieren die Dateien, die sie importieren, erst wieder, wenn ihre Imports nachgezogen sind. Pfade bestehen aus Buchstaben, Ziffern, Punkten, Unterstrichen und Bindestrichen. Der Namespace Archestack ist reserviert. Ein Alias in einem global using (global using Money = decimal;) wird nicht an Skripte weitergegeben.

NuGet Packages

Bibliotheken von NuGet für die C#-Skripte des Workspaces: ein Treiber für eine andere Datenbank, ein CSV- oder Excel-Leser, ein Client für einen Dienst. Jedes Skript kann sie verwenden (Script Modules, Skripte in Business Events, Scheduled Events, PDF Data Scripts, Test-Skripte und Code-Prüfungen), ebenso die .cs-Dateien im Shared Code. Der Skript-Editor und VS Code vervollständigen ihre Typen. Ein Workspace, der keine hinzufügt, führt seine Skripte genau wie bisher aus.

Gängiger Workflow: Öffne in der Sidebar unter Logic den Eintrag NuGet Packages und klicke auf Add NuGet package. Suche danach oder tippe die genaue ID ein, wenn sich die Quelle nicht durchsuchen lässt, und wähle eine genaue Version: Die neueste stabile ist vorausgewählt, und Include prerelease versions listet die anderen. Wähle unter Belongs to, wozu es gehört: zu einem der Packages des Workspaces oder Workspace only. Add lädt es zuerst mit seinen Abhängigkeiten auf den Server herunter. Schlägt das fehl, sagt der Dialog, warum, und nichts wird gespeichert; klappt es, zeigt er die Namespaces, die es mitbringt.

In einem Package: Ein NuGet-Paket, das zu einem Package gehört, reist mit ihm, in seinem Export und in dem, was eine Pipeline deployt, und die Installation eines Packages listet vor jeder Änderung die NuGet-Pakete auf, die es hinzufügt oder auf eine höhere Version hebt. In einem Repository stehen sie in packages/<package>/nuget.json. Diese Datei wird nur geschrieben, wenn das Package welche deklariert, und VS Code prüft sie beim Tippen. Ein Push, der die Datei ändert, lädt sie zuerst herunter und wird abgelehnt, ohne etwas zu ändern, wenn sie sich nicht wiederherstellen lassen.

{
  "packages": [
    { "id": "CsvHelper", "version": "33.0.1" },
    { "id": "Dapper", "version": "2.1.35" },
    { "id": "MySqlConnector", "version": "2.4.0" }
  ]
}

Verwenden: Ein NuGet-Paket bringt Typen mit, nie ein Global, und nichts wird für dich importiert. Schreibe ein using für seinen Namespace, der oft nicht die ID ist: Die Spalte Namespaces der Seite zeigt die Namespaces jedes NuGet-Pakets als using-Zeilen zum Kopieren. Gib aus einem Skript einfache Werte zurück, nicht die eigenen Objekte der Bibliothek.

// NuGet package: CsvHelper. Reads a price list an admin uploaded on the Files page.
using System.Globalization;
using System.IO;
using CsvHelper;

var text = await Files.ReadTextAsync("imports/prices.csv");
var csv = new CsvReader(new StringReader(text), CultureInfo.InvariantCulture);
var rows = new List<object>();
try
{
    csv.Read();
    csv.ReadHeader();
    while (csv.Read())
        rows.Add(new { Sku = csv.GetField("sku"), Price = csv.GetField<decimal>("price") });
}
finally
{
    csv.Dispose();
}

// Plain rows, not CsvHelper objects.
return rows;
// NuGet packages: MySqlConnector and Dapper. A script module that lists the open invoices in
// the old ERP's MySQL database. The connection string is a file an admin uploads on the Files
// page of each environment: a password written in a script travels to git, package exports
// and every environment.
using Dapper;
using MySqlConnector;

var connectionString = await Files.ReadTextAsync("config/erp-connection.txt");
var connection = new MySqlConnection(connectionString.Trim());
try
{
    await connection.OpenAsync();
    var invoices = await connection.QueryAsync(
        "SELECT number, customer_number, amount FROM invoices WHERE paid = 0 ORDER BY number");

    // Plain values, not driver objects.
    return invoices
        .Select(i => new { Number = (string)i.number, Customer = (string)i.customer_number, Amount = (decimal)i.amount })
        .ToList();
}
finally
{
    // A top-level using var does not parse in a script, so the connection is closed here.
    await connection.DisposeAsync();
}

Versionen: Eine Deklaration nennt genau eine Version, etwa 2.1.35, nie einen Bereich. Verlangen mehrere Packages unterschiedliche Versionen desselben NuGet-Pakets, verwendet jedes Skript des Workspaces die höchste. Die Spalte In use zeigt, welche Version das ist und wer sie verlangt hat.

Wessen Kopie läuft: Ein Workspace lädt seine eigene Kopie jedes NuGet-Pakets, getrennt von jedem anderen Workspace auf dem Server, auch wenn der Server dieselbe Bibliothek für sich selbst mitbringt. .NET selbst und die Bibliotheken, die Skripte bereits haben, etwa System.Text.Json, Logging, EF Core und Npgsql, bleiben die Kopie des Servers. Ein NuGet-Paket, das eine neuere Version einer davon verlangt, bekommt eine Warnung: Es läuft mit der Kopie des Servers, und eine Methode, die nur die neuere Version hat, schlägt fehl, wenn sie aufgerufen wird.

Restores: Das erste Skript, das die NuGet-Pakete auf einem Server braucht, lässt diesen Server sie herunterladen. Es wartet bis zu fünf Sekunden, und dauert der Download länger, wird es gebeten, gleich noch einmal zu laufen. Nach einer Änderung stellt der Workspace die neue Zusammenstellung wieder her. Bis das klappt, behalten Skripte die letzte Zusammenstellung, die sich wiederherstellen ließ, ein fehlgeschlagener Restore nimmt also nie NuGet-Pakete weg, die funktioniert haben. Das Banner auf der Seite zeigt den Stand und erklärt die Fehler eines fehlgeschlagenen Restores. Nach einem Fehlschlag versucht der Server es erneut, wenn ein Skript die NuGet-Pakete nach dem Zeitpunkt im Banner zum ersten Mal braucht, oder sofort mit Restore now. Resolved listet jedes NuGet-Paket auf, das Skripte verwenden, Abhängigkeiten eingeschlossen.

Probeläufe und Tests: Ein Probelauf (der Test-Tab, Run Simulation) hält nur zurück, was über Archestack läuft: Schreibvorgänge über Db und Files werden simuliert und nicht gespeichert, und Mails und Benachrichtigungen werden nicht verschickt. Ein Testlauf führt seine Schreibvorgänge über Db in seiner Sandbox aus und hält den Rest genauso zurück. Alles andere, was das Skript selbst tut, ist echt: HTTP-Aufrufe, System.IO und alles, was ein NuGet-Paket tut, etwa eine Verbindung zu einer anderen Datenbank, ein Schreibvorgang über einen Treiber passiert also bei jedem Lauf. Richte ein solches Skript auf eine Testdatenbank, solange du es ausprobierst.

Stolperfallen:

  • NuGet-Pakete brauchen den typisierten Datenzugriff, wie Shared Code in C#. Auf einem Server, der ihn abschaltet (Scripting:TypedData:Enabled), sagt die Seite das und kann keines hinzufügen.
  • #r "nuget: Dapper, 2.1.35", womit andere Skript-Runner ein NuGet-Paket laden, funktioniert hier nicht. Füge es auf der Seite hinzu.
  • Eine native Bibliothek, die ein NuGet-Paket mitbringt, wird einmal pro Server-Prozess geladen, von den Workspaces geteilt, die dieselbe Datei verwenden, und bleibt geladen, bis der Server neu startet.
  • NuGet-Pakete mit Workspace only werden nicht mit einem Package exportiert, stehen nicht im Repository und werden nicht von Pipelines deployt. Gib ein NuGet-Paket, das eine App braucht, dem Package dieser App.
  • Ein using var auf oberster Ebene lässt sich in einem Skript nicht parsen, schließe eine Verbindung also selbst, in einem finally, wie es das Beispiel tut.
  • Ein Wert aus Entity ist dynamic, und eine Extension-Methode wie Dappers QueryAsync kann keinen als Argument nehmen: Das Skript kompiliert nicht (CS1973). Caste ihn zuerst: (string)Entity.customers_number.
  • NuGet-Pakete, die Tools, Templates oder MSBuild-SDKs sind, werden abgelehnt, ebenso eines, gegen das Skripte auf .NET 8 nichts kompilieren können, oder eines, das den PostgreSQL-Treiber oder EF Core des Servers erweitert. IDs namens OmniCore oder Archestack, oder solche, die mit einem davon und einem Punkt beginnen, gehören der Plattform.

Für Betreiber: NuGet-Pakete sind standardmäßig an und in einem Trial immer aus. Die Einstellungen liegen unter Scripting:NuGet, oder als Umgebungsvariablen Scripting__NuGet__Enabled und so weiter. Jeder Server lädt für sich selbst herunter, jeder muss also nuget.org oder die hier konfigurierten Feeds erreichen, über den Proxy in https_proxy, http_proxy und no_proxy, wenn es einen gibt. Gib allen Servern einer Installation dieselben Quellen.

EinstellungStandardWas sie tut
EnabledtrueIst sie aus, wird nichts heruntergeladen und Skripte laden keines. Die Seite kann keines hinzufügen; Deklarationen, die ein Import oder ein Push mitbringt, bleiben ungenutzt erhalten.
Sourcesnuget.orgWoher NuGet-Pakete kommen, getrennt durch ;: Feeds über ihren v3-Service-Index (die URL, die auf index.json endet) oder absolute Ordner, aus denen kopiert wird und denen vertraut wird: Die Download-Limits gelten nur für Feeds. nuget.org wird nur verwendet, wenn weder diese Einstellung noch Feeds eine Quelle nennt.
Feeds:<name>:Url, Username, PasswordKeineEin benannter Feed, für einen, der Zugangsdaten braucht. Die URL enthält nie einen Benutzernamen oder ein Passwort.
Feeds:<name>:PatternsKeineDie IDs, die dieser Feed liefert, getrennt durch ; (Contoso.*). Hat irgendein Feed Muster, kommt eine ID von dem Feed, auf dessen Muster sie passt, und ein Feed ohne Muster liefert alles andere.
Feeds:<name>:AllowInsecureConnectionsfalseLässt einen einfachen http://-Feed durch.
DirectoryEin Ordner in den lokalen AnwendungsdatenWo dieser Server die heruntergeladenen NuGet-Pakete aufbewahrt: ein Ordner pro Server auf seiner lokalen Platte, nie ein Volume, das sich Server teilen. Gib ihm in einem Container ein eigenes Volume dieses Servers, sonst lädt jeder neue Container sie erneut herunter.
MaxPackageBytes, MaxExtractedBytes100 MB, 1 GBDas größte NuGet-Paket, das ein Restore herunterlädt, und wie viel ein NuGet-Paket entpackt höchstens belegen darf.
MaxRestoreBytes, MaxRestoreExtractedBytes, MaxPackages500 MB, 2 GB, 150Wie viel ein Restore insgesamt höchstens herunterlädt, entpackt und an NuGet-Paketen zählt.
MaxCacheBytes10 GBDarüber nimmt der Ordner keine neuen Restores an, und die NuGet-Pakete, die dieser Server nicht verwendet, werden entfernt, die am längsten unbenutzten zuerst.
MinFreeDiskBytes1 GBBei weniger freiem Plattenplatz startet kein Restore, und ein Download, der weniger übrig ließe, wird gestoppt.
RestoreTimeoutSeconds, AddWaitSeconds, MaterializeWaitMilliseconds300, 120, 5000Wie lange ein Restore laufen darf, wie lange das Hinzufügen eines NuGet-Pakets auf seinen Download wartet, und wie lange ein Skript auf den Download dieses Servers wartet, bevor es gebeten wird, erneut zu laufen.
AllowNativeAssetstrueIst sie aus, wird ein NuGet-Paket mit nativen Bibliotheken abgelehnt, und keine wird je geladen.
AuditRefuseLevelLeerlow, moderate, high oder critical: Ein NuGet-Paket mit einer bekannten Schwachstelle auf oder über dieser Stufe wird abgelehnt. Leer lehnt keines ab, und Schwachstellen erscheinen als Warnungen; jeder andere Wert lässt jeden Restore fehlschlagen, der ein NuGet-Paket hinzufügt.
CertificateRevocationModeonlineonline oder offline: wie die Signaturzertifikate von NuGet-Paketen auf Widerruf geprüft werden.

Frontend Templates

Wiederverwendbare UI-Fragmente, in TSX geschrieben, die der Page Editor in eine Page ablegen kann. Nützlich, wenn die konfigurierten Templates nicht ausreichen, maßgeschneiderte Diagramme, ungewöhnliche Layouts, Integrationen von Drittanbieter-Widgets.

Gängiger Workflow: Klicke auf Create, benenne das Template, schreibe eine TSX-Komponente im Editor. Die Komponente erhält Props: record (die aktuelle Zeile, wenn in einem Detailformular eingebettet), refresh (Daten neu laden) und ein paar Helper. Speichern. Referenziere das Template aus einem Page Editor-Feld.

Paketierbar: Templates fließen wie jedes andere Konfigurationsobjekt durch das Package-System, sie werden mit kaskadiert, wenn eine Page, die sie nutzt, einem Package hinzugefügt wird.

Stolperfallen: Das TSX kann keine beliebigen npm-Pakete importieren: Es importiert Shared Code aus @/ und die wenigen Pakete, die die App mitbringt.

PDF Templates

HTML + Scriban-Template + ein kleines C#-Data-Script, das in ein PDF rendert (headless Chromium). Jedes Template ist per Name aus einem Skript aufrufbar, über REST oder direkt von einem Button auf einer Page (mit einem kleinen Adapter, siehe unten).

Gängiger Workflow: Klicke auf + Create. Der Editor öffnet sich mit Name- und Description-Textfeldern oben, fünf Tabs auf der linken Seite (HTML Template, Data Script, Settings, Params, JSON) und einer immersichtbaren PDF Preview-Pane auf der rechten Seite. Die Tabs Data Script und HTML Template öffnen je einen Monaco-Editor auf der linken Hälfte; die Vorschau rendert neu, wenn du speicherst (grüner Update-Button oben rechts).

Deklariere Parameter im Params-Tab über den + Add-Button (Name, Type, Required, Default Value). Das Tab-Label aktualisiert sich auf Params (n) mit der Anzahl. Seitenformat / Orientierung / Ränder / Skalierung / Kopfzeile / Fußzeile leben im Settings-Tab.

Data-Script-Form (siehe PDF Invoices Tutorial für ein echtes Beispiel):

var deal = await Db.GetAsync("deal", deal_id);
var lines = await Db.From("deal_line")
    .Where("deal_id", "=", deal_id).ToListAsync();

return new {
    InvoiceNumber = $"INV-{deal.id:D6}",
    Lines = lines.Select(l => new { l.description, l.quantity })
};

HTML-Template-Form (Scriban, ähnlich Liquid/Handlebars):

<h1>{{ InvoiceNumber }}</h1>
<table>
  {{ for line in Lines }}
  <tr><td>{{ line.description }}</td><td>{{ line.quantity }}</td></tr>
  {{ end }}
</table>

HTML-Encoding: Jeder {{ }}-Wert wird HTML-kodiert, ein Datensatzwert mit <b> erscheint also als der Text <b>. html.raw ist das explizite Opt-out, nur für vertrauenswürdiges HTML: {{ notes | html.raw }}. Nutze es nie für Werte, die Endnutzer eingeben können, denn damit landet ihr Markup im gerenderten Dokument. Nur das Template kann das Encoding abschalten, die Daten können es nicht. html.escape und html.newline_to_br funktionieren weiterhin (newline_to_br escapt seinen Text, behält aber die <br />-Tags).

Preview-Pane: Immer sichtbar auf der rechten Seite des Editors. Rendert neu, wenn du speicherst (grüner Update-Button). Enthält eine Thumbnail-Spalte für mehrseitige PDFs und eine eingebaute Viewer-Symbolleiste (Zoom, Drehen, Download, Druck). Die Icon-Buttons oben rechts im Editor schalten die Sichtbarkeit der Preview-Pane und den Vollbild-Bearbeitungsmodus um.

Aufruf von außerhalb des Editors:

  • POST /api/v1/pdf-templates/{id}/preview - rendert nach ID, gibt application/pdf zurück. Wird von der Preview-Pane des Editors genutzt.
  • POST /api/v1/pdf-templates/generate/{name} - rendert nach Name, gibt application/pdf zurück.
  • POST /api/v1/pdf-templates/generate/{name}/base64 - gleich, gibt aber { "data": "<base64>" } zurück.

Übergib Parameterwerte im JSON-Body. Auth ist das standardmäßige Bearer-Token.

Trial-Einschränkung: Sowohl der generate-Endpunkt als auch der {id}/preview-Endpunkt (genutzt von der Preview-Pane des Editors) geben im Trial-Modus 403 zurück, PDF-Rendering ist komplett gesperrt. Du kannst das Template trotzdem bearbeiten (Data Script, HTML, Settings, Parameter) und speichern; du wirst nur kein gerendertes PDF sehen, bis du in einer bezahlten Umgebung bist.

Hinweis zum script-aufrufbaren Rendering: Der C#-Skripting-Kontext stellt sehr wohl ein Pdf-Global bereit, ein Script Module rendert ein gespeichertes Template also direkt mit await Pdf.GenerateAsync("invoice", ...) oder Pdf.GenerateBase64Async(...). Ein HttpClient-Umweg über den generate-Endpunkt ist nicht nötig. (Die Autocomplete des Skript-Editors bewirbt weiterhin Pdf.RenderAsync und ein Templates-Objekt; keiner der beiden Namen existiert zur Laufzeit.)

Stolperfallen: Scriban escapt HTML standardmäßig. Seitenumbrüche sind CSS-gesteuert, page-break-before: always auf einer Sektion. Schriftarten, die auf dem Render-Server verfügbar sind, sind auf System-Schriften plus die Noto- und Liberation-Familien beschränkt, bette deine eigene per base64 @font-face ein, wenn du eine Markenschrift brauchst.

Bilder, Schriftarten und Stylesheets: Der Renderer lädt Ressourcen nur von öffentlichen http(s)-Adressen, dazu data:- und blob:-URLs. Interne und private Netzwerkadressen (localhost, 10.x, 192.168.x, Container-Servicenamen usw.) sind gesperrt, ein Bild von einem internen Dienst erscheint also nicht im PDF. Nutze eine öffentliche URL oder eine data:-URL.

Scheduled Events

Dasselbe Event Trigger-System wie Business Events, mit dem On Schedule-Trigger angekreuzt. Nutze es für nächtliche Neuberechnungen, wöchentliche Digests, periodische Aufräumarbeiten, monatliche Berichte.

Gängiger Workflow: Erstelle ein Business Event mit dem On Schedule-Trigger (anstelle der oder zusätzlich zu den Datenänderungs-Triggern). Konfiguriere den Cron-Ausdruck. Füge eine Execute Script-Action hinzu, deren Body ein Script Module über Modules.CallAsync(...) aufruft.

Cron-Ausdrücke: Quartz-Style 6 oder 7 Felder, siehe Concepts → Scheduled Events für gängige Muster. Zeiten sind Serverzeit (UTC auf dem Produktions-Stack).

Stolperfallen: Ein langlaufender Job, der sein Cron-Intervall überzieht, verdoppelt sich nicht, Quartz feuert nicht zwei Instanzen desselben Jobs gleichzeitig, der zweite Tick wird übersprungen. Fehlgeschlagene Läufe wiederholen sich nicht automatisch; baue Retry in das Skript ein.

Packages

Exportierbare Bündel von Konfigurationsobjekten (BEs, Pages, Events, Scripts, Templates, Schema) mit kaskadierenden Abhängigkeiten. Eine Page einem Package hinzuzufügen zieht automatisch alles mit, was diese Page referenziert.

Gängiger Workflow (Export): Klicke auf Create, benenne und versioniere das Package. Füge die Top-Level-Objekte hinzu, die dich interessieren, meist eine Handvoll Pages. Öffne das Linked-Items-Panel, um die kaskadierte Liste zu sehen (was das Package tatsächlich enthalten wird). Hake alles ab, was du weglassen willst. Entscheide, ob Daten enthalten sein sollen (Toggle beim Export). Klicke auf Export, du bekommst ein ZIP.

Gängiger Workflow (Import): Öffne in der Zielumgebung Packages, klicke auf Import, lade das ZIP hoch. Die Plattform zeigt, was hinzugefügt oder aktualisiert wird. Schema-Deltas werden nicht automatisch angewendet, wenn das Package eine Spalte referenziert, die in der Zielumgebung nicht existiert, schlägt der Import fehl.

Stolperfallen: Identifikatoren sind nach Name, nicht nach numerischer ID. Eine BE namens "customer" in der Quelle bindet an eine BE namens "customer" im Ziel, benenne eine Seite um und die Verbindung bricht.

In git: Die VS Code-Erweiterung pullt ein Package als eine Datei pro Artefakt in einen Ordner und pusht den Ordner zurück, sodass sich ein Package wie jeder andere Code reviewen und versionieren lässt.

Third-Party Data Connections

Verbindungen zu externen Datenbanken (Postgres, SQL Server, MySQL, andere), die Tabellen nach Zeitplan importieren. Importierte Tabellen verhalten sich wie native Tabellen, sie tauchen im Schema Designer auf und können von Business Entities, Pages und Skripten referenziert werden.

Gängiger Workflow: Erstelle eine Verbindung (Connection String, Test, Speichern). Liste verfügbarer Remote-Tabellen; wähle, welche importiert werden sollen. Für jede importierte Tabelle setze Sync-Modus (Full / Delta) und Sync-Trigger (Cron / manuell / festes Intervall).

Gespiegelt, nicht föderiert: Die importierten Daten leben in deiner eigenen omnicore-db. Lesen ist schnell (lokales Postgres). Nachteil: Veralterung zwischen Syncs.

App-managed Spalten: Markiere einige Spalten als "von Archestack verwaltet", sie werden von zukünftigen Syncs nicht überschrieben. Nützlich, wenn du Lieferantendaten mit deinen eigenen Statusflags annotieren willst.

Stolperfallen: Der erste Sync einer großen Tabelle kann eine Weile dauern. Delta-Syncs erfordern eine Spalte, die der Connector als Wasserzeichen nutzen kann, meist ein modified_at-Timestamp.

AI Assistant

Ein in die App eingebauter Chat-Assistent, der sowohl Fragen zu deinen Daten beantworten als auch deine Umgebung verändern kann. Frag "wie viele offene Arbeitsaufträge pro Standort in diesem Monat?" und er fragt ab und antwortet. Sag "füge Serviceterminen eine Status-Spalte hinzu und eine Seite, die sie auflistet" und, wenn du Administrator bist, baut er es.

Bring dein eigenes Modell mit. Archestack liefert kein LLM aus und bezahlt keine Inferenz. Ein Administrator legt auf dem Bildschirm AI Configuration einen Provider an: eine Base-URL, einen Modellnamen und einen API-Schlüssel. Alles, was die OpenAI-chat-completions-API spricht, funktioniert (ChatGPT, DeepSeek, OpenRouter, Groq, Together, ein selbst gehostetes Ollama oder vLLM), und Anthropics eigene API wird direkt unterstützt. Du kannst mehrere konfigurieren und pro Unterhaltung eines auswählen.

Alles, was er tut, läuft über den MCP Server. Der Assistent hat keine eigene Hintertür in die Plattform, er ruft dieselben Tools auf wie jeder externe MCP-Client, mit deiner Identität. Die Grenzen, die du dort setzt, gelten also auch für ihn, und jede Aktion landet im selben Audit-Log. Siehe MCP Server weiter unten.

Gängiger Workflow: Öffne den Assistenten über die Sidebar oder die schwebende Blase. Frag in ein, zwei Sätzen. Er liest erst dein aktuelles Schema, sagt, was er ändern will, und wartet auf deine Bestätigung. Jeder Tool-Aufruf erscheint unter der Antwort, du siehst also, was er tatsächlich getan hat.

Funktioniert gut für: Ad-hoc-Fragen quer über deine Daten, CRUD-artige Funktionen, einfache Automatisierungen, Erweiterungen bestehender Entitäten.

Weniger gut für: nicht-offensichtliche Geschäftsregeln und komplexe mehrstufige Workflows. Behandle, was er baut, wie den PR eines Junior-Devs.

Stolperfallen: Er arbeitet auf Live-Daten und es gibt kein Rückgängig, mach also vor Riskantem ein Backup. Deine Prompts und alles, was die Tools zurückgeben, gehen an den Provider, den du konfiguriert hast, zu dessen Bedingungen; richte ihn auf ein selbst gehostetes Modell, wenn das zählt. Lange Unterhaltungen wiederholen nur ihre letzten Züge, ein den ganzen Tag offener Chat erinnert sich also nicht an den Anfang. Und beachte die zwei Schalter: Bei gestopptem MCP Server kann der Assistent noch reden, aber nichts lesen oder ändern.

MCP Server

Jede Umgebung kann sich über das Model Context Protocol verfügbar machen, sodass ein KI-Client außerhalb von Archestack, Claude Desktop, Claude Code, ChatGPT deep research, eine IDE oder dein eigenes Skript, direkt mit ihr arbeiten kann. Darauf läuft auch der eingebaute AI Assistant.

Standardmäßig aus, und die Tools, die verändern, wie die Anwendung gebaut ist, bleiben auch nach dem Start aus. Das ist Absicht.

Drei Tool-Gruppen. Read deckt das Auflisten und Beschreiben von Entitäten ab, das Abfragen von Zeilen und das Lesen von Schema, Pages, Events, Script Modules, Packages, Translations und Event Logs. Write ist Anlegen, Ändern und Löschen von Datensätzen und geht denselben Weg wie die UI, deine Business Events feuern also genau wie bei einer Person, die auf Speichern klickt. Admin ist die Gruppe, mit der ein Modell die Anwendung selbst bauen kann: Entitäten, Schema, Pages, Events, Script Modules, Translations, Packages und die NuGet-Pakete, die Skripte nutzen.

Vier Tore entscheiden, ob ein Aufruf ankommt: Läuft der Server, ist die Gruppe erlaubt, ist genau dieses Tool aktiviert, und trägt das Token den Scope. Ein von einem davon blockiertes Tool ist für den Client unsichtbar, statt im Nachhinein abgelehnt zu werden.

Tokens. Gib pro Client eines auf dem MCP-Server-Bildschirm aus, mit den Scopes, die er wirklich braucht, und einem optionalen Ablauf. Das Geheimnis wird einmalig bei der Erstellung angezeigt und nur als Hash gespeichert, ein verlorenes Token wird also rotiert, nicht wiederhergestellt. Gib jeder Integration ihr eigenes Token: Das hält das Activity-Log lesbar, und das Widerrufen des einen stört die anderen nicht.

Activity-Log. Jeder Aufruf wird aufgezeichnet, auch die abgelehnten und warum. Hier prüfst du, was eine Integration tatsächlich getan hat.

Stolperfallen: Ein Token mit dem Admin-Scope wird von allem Nachgelagerten als vollwertiger Administrator behandelt, gib die also sparsam aus. "Allow token in URL" gibt es für Clients, die keinen Authorization-Header senden können, aber eine URL landet deutlich leichter in Logs und Screenshots als ein Header, lass es aus, wenn du es nicht brauchst. Und auf einer Produktivumgebung mit echten Daten: Lass die Admin-Gruppe aus.

Object Browser

Rohe Sicht auf jede native und importierte Tabelle, Zeilen direkt hinzufügen, bearbeiten, löschen. Enthält einen CSV-Import zum Bulk-Laden.

Wann du danach greifst:

  • Bulk-Laden von Referenzdaten, bevor Pages existieren (Länder, Währungen, Status-Enums).
  • Debugging, anschauen, was tatsächlich in einer Tabelle ist, wenn sich eine Page seltsam verhält.
  • Schnelle Admin-Edits an Daten, die noch nicht über eine Page freigegeben sind.
  • CSV-Import: wähle eine Tabelle, lade eine Datei hoch, mappe Spalten zu Feldern, Vorschau, commit.

Stolperfallen: Object Browser feuert Business Events (er läuft denselben EntityService-Schreibpfad ab wie das Frontend). Die Ausnahme ist direktes SQL über Database Deployments, das umgeht alles.

Dependency Graph

Eine schreibgeschützte Karte davon, wie die Konfigurationsobjekte in deiner Umgebung aufeinander verweisen. Acht Ressourcentypen erscheinen als Knoten, eingefärbt nach Typ: Business Entities, Pages, Event Triggers, Script Modules, PDF Templates, Frontend Templates, Scheduled Events und Packages. Eine Kante zwischen zwei Knoten bedeutet, dass eine Ressource von der anderen abhängt. Greif danach, um die Fragen zu beantworten "was würde brechen, wenn ich das lösche?" und "ist hier irgendetwas ungenutzt?".

Was eine Kante bedeutet: eine Page, die an eine Business Entity gebunden ist, ein Event Trigger, der auf einer Business Entity registriert ist, ein Page-Aktionsschritt, der einen Event Trigger feuert, ein Skript, das ein anderes Script Module über Modules.CallAsync("Name") aufruft, ein Skript, das ein PDF Template rendert, ein Scheduled Event, der einen Event Trigger feuert, oder ein Package, das eine Ressource enthält. Es ist eine Design-Zeit-Sicht, sie spiegelt wider, was deine Konfiguration miteinander verdrahtet, nicht den Live-Laufzeit-Verkehr.

Zwei Arten von Problemen werden visuell hervorgehoben:

  • Loose ends (Waisen) - Knoten, auf die nichts verweist und die selbst auf nichts verweisen. Sie werden in einem rot umrandeten "Loose ends"-Band am unteren Rand der Leinwand gesammelt. Ein Script Module, das niemand aufruft; eine Page, die an nichts gebunden ist.
  • Dangling references - als rote gestrichelte Kanten gezeichnet. Der Verweis existiert in der Konfiguration, aber sein Ziel kann nicht gefunden werden, zum Beispiel ein Page-Aktionsschritt, der noch auf einen gelöschten Event Trigger zeigt, oder ein Skript, das ein Script Module unter einem Namen aufruft, der nicht mehr existiert.

Gängiger Workflow: öffne Dependency Graph aus dem Tools-Abschnitt der Sidebar. Das linke Panel hat eine Namenssuche, eine Layout-Auswahl (Left to right, Top to bottom, By type, Force-directed), die Schalter Group by package, Show loose ends und Show edges sowie eine Checkliste pro Typ. Klicke auf einen Knoten, um den Fokusmodus zu aktivieren: dieser Knoten und seine direkten Nachbarn bleiben hell, während der Rest des Graphen abdunkelt, sodass du genau nachverfolgen kannst, was eine Ressource berührt. Klicke auf den Leinwand-Hintergrund, um den Fokus aufzuheben. Jeder Knoten trägt einen kleinen Knopf, der die Ressource in einem neuen Tab öffnet, direkt in ihrem Editor. Die Statistikzeile oben meldet die Anzahl an Knoten, Kanten, Waisen und Dangling references.

Group by package: umrahmt die Mitglieder jedes Packages in einer beschrifteten gestrichelten Box, sodass du ein Package als Einheit siehst und die Ressourcen erkennst, die es per Kaskade hereingezogen hat.

Stolperfallen: der Graph wird bei Bedarf aus dem aktuellen Zustand aufgebaut, nutze den Refresh-Knopf, nachdem du anderswo Ressourcen geändert hast. Skript-abgeleitete Kanten werden gefunden, indem Skriptkörper nach der wörtlichen Modules.CallAsync("Name")-Aufrufform durchsucht werden, ein Module, das über einen berechneten Namen aufgerufen wird, zeigt keine Kante. Eine Dangling reference ist immer eine Behebung wert; eine Waise oft nicht, ein brandneues Script Module gilt als verwaist, bis etwas es aufruft.

Event Logs

Der Audit-Trail für alles, was die Plattform ausgeführt hat, Business Events, Script Modules, Scheduled Events, Third-Party-Syncs.

Gängiger Workflow: Öffne Event Logs, setze Status auf Failed, klicke in einen Eintrag. Das Detail-Panel zeigt die Payload (Datensatzzustand zum Zeitpunkt, übergebene Parameter), die Exception mit Stack Trace und das Timing.

Logs und Charts: Die Seite hat zwei Tabs, die sich eine Filterzeile teilen. Logs ist die Tabelle oben. Charts ist ein Betriebs-Dashboard über denselben Filter: Volumen im Zeitverlauf, gestapelt nach Status, ein Trend der Erfolgsquote, eine Aufschlüsselung pro Source, die Events mit den meisten Fehlern und die langsamsten Events, durchschnittliche und maximale Dauer im Zeitverlauf, eine Aufschlüsselung nach Trigger-Typ und eine Heatmap aus Wochentag und Tagesstunde, mit den wichtigsten Summen oben. Beide Tabs teilen sich einen Von / Bis Datumsbereich (mit den Presets Letzte 1h / 24h / 7d); der Charts-Tab wählt automatisch eine passende Bucket-Größe für den Zeitraum und kann sich in einem festen Intervall aktualisieren, für Live-Monitoring.

Echtzeit-Benachrichtigungen: Das Glockensymbol in der oberen Leiste der Admin-App zeigt ungelesene Fehler über SignalR an.

Stolperfallen: Das Log kann groß werden. Alte Einträge werden standardmäßig für immer aufbewahrt; wenn Platz ein Thema ist, richte ein Scheduled Event ein, das Einträge älter als N Tage löscht.

User Management

Nutzer anlegen, einladen und verwalten; Realm-Rollen zuweisen. Hüllt die Keycloak Admin REST API ein.

Rollen:

  • admin - alles; kann alle Business Units, Nutzer, Rollen, Systemeinstellungen verwalten.
  • owner - verwaltet ihre eigenen Business Units, kann Administrations- und Laufzeit-Pages bearbeiten.
  • editor - kann Laufzeit-Pages ansehen und bearbeiten, aber nicht Schema/BE/Event-Konfiguration ändern.
  • user - schreibgeschützt auf Laufzeit-Pages.

Stolperfallen: Einen Nutzer mitten in der Session von admin zu degradieren wirft ihn nicht raus, die Änderung greift beim nächsten Token-Refresh (typischerweise innerhalb einer Minute).

Business Units

Keycloak-gestützte Gruppen, die Sichtbarkeit auf einzelne Ressourcen steuern. Siehe Concepts → Business Units für das Modell.

Gängiger Workflow: Erstelle eine BU. Lade Mitglieder per E-Mail ein und wähle die Rolle. Weise Ressourcen zu, indem du zu jeder BE / Page / Trigger gehst oder über das Linked-Items-Panel der BU.

Kaskadierende Zuweisungen: Wenn du eine Page einer BU zuweist, kaskadiert die Zuweisung zur BE, an die die Page bindet.

Stolperfallen: Die aktive BU eines Nutzers wird im BU-Umschalter der oberen Leiste gesetzt und in localStorage persistiert. Neue Nutzer defaulten auf ihre erste BU; wenn eine Listen-Page verdächtig leer ist, prüfe, welche BU aktiv ist.

Database Backups

Nächtlicher pg_dump aller drei Datenbanken, dazu ein ZIP der Dateien des Workspaces. Manueller Trigger / Upload / Restore aus der UI.

Gängiger Workflow: Trigger now → Download → Upload → Restore. Der Zeitplan ist standardmäßig nächtlich um 03:00 UTC.

Trial-Einschränkung: Backups sind in der Trial-Umgebung deaktiviert.

Branding

Ändert den Anzeigenamen der App, das Logo (hell + dunkel), Farben, Schriften und das übrige Material-UI-Theme, zur Laufzeit angewendet, ohne Neu-Build.

Gängiger Workflow: Setze den App-Namen, lade ein helles und ein dunkles Logo hoch (PNG/JPG/SVG, ≤ 10 MB), wähle die Markenfarben im Tab Theme. Speichern: deine Sitzung zeigt es sofort, alle anderen beim nächsten Laden der Seite.

Theme: Der Tab Theme ist ein vollständiger Material-UI-Theme-Editor mit Live-Vorschau im hellen und dunklen Modus: die Markenfarben und jede Palettenfarbe pro Modus, die Schriften (jede Google-Schrift) und jeder Textstil, Eckenradius, Abstände, Schatten, Animationstempo, der Stil von Buttons, Karten und Eingabefeldern, Komponenten-Standardwerte und Komponenten-Overrides als JSON. Er prüft Kontraste, startet von Vorlagen, importiert ein Theme aus einer anderen Material-UI-App und exportiert dieses als Code. Show on the whole app probiert einen Entwurf auf jedem Bildschirm aus, bevor du speicherst. Ein Workspace, der sein Theme nie ändert, behält den eingebauten Look unverändert. In einem Repository ist das Theme der Schlüssel theme in workspace/branding.json.

Stolperfallen: Das Logo ist auch das Symbol im Browser-Tab und das Symbol, das ein Telefon für die App auf dem Startbildschirm zeigt. Der Startbildschirm braucht ein Logo als PNG, JPG oder WebP; bei einem SVG zeigt er das Symbol von Archestack.

Analytics

Google Analytics in der veröffentlichten App, eingerichtet auf der Seite Analytics (Configuration). Der Builder wird nie erfasst.

Typischer Ablauf: Füge die GA4-Mess-ID ein (G-XXXXXXXXXX, in Google Analytics unter Verwaltung, Datenstreams), hake Track visits to the published app an und speichere. Ask visitors for consent first ist standardmäßig an: Das Tag von Google lädt erst, nachdem ein Besucher das Banner akzeptiert hat, wie es die Cookie-Regeln der EU verlangen. Text und Schaltflächen des Banners und ein Link zu deiner Datenschutzerklärung können deine eigenen sein.

Ereignisse aus Seitencode: Sobald das Tag geladen ist, kann Seitencode eigene Ereignisse senden, etwa window.gtag?.("event", "sign_up"). Solange die Erfassung aus ist oder der Besucher abgelehnt hat, wird nichts gesendet.

Translations

Key/Value-Editor für die UI-Strings der App. Neue Locales hinzufügen, Standardlabels pro Kunden überschreiben.

Gängiger Workflow: Wähle eine Locale, suche nach einem Key, tippe einen neuen Wert, speichere. Admin-App aktualisieren.

Stolperfallen: Übersetzungen werden einmal pro Session geladen; Nutzer auf einem veralteten Tab sehen Änderungen nicht, bis sie aktualisieren. Manche Labels (Spaltenüberschriften aus BEs) kommen aus der BE-Konfiguration, nicht aus Translations.


Script-aufrufbare APIs

Die exakte Oberfläche, die Skripte aufrufen können. Gegen den Quellcode verifiziert, jede Methode unten existiert tatsächlich.

Globals

Jedes Skript bekommt diese Top-Level-Variablen (keine anderen Namen existieren im Skript-Scope):

  • Entity - dynamic ExpandoObject. Aktueller Datensatz. Greife auf Felder mit Entity.column_name zu. Entity in einem Before*-Trigger zu mutieren persistiert die Änderungen als Teil des laufenden Schreibvorgangs.
  • OldEntity - dynamic ExpandoObject. Vorheriger Datensatz (Update- + Delete-Trigger). Schreibgeschützt.
  • Log - ILogger. Log.LogInformation("…"), Log.LogWarning(…), Log.LogError(…). Schreibt in Server-Logs und den Event Log-Eintrag.
  • Db - DbHelper. Datenbankzugriff: ein typisiertes Set pro Business Entity und die untypisierten Helper (siehe unten).
  • Modules - andere Script Modules aufrufen. Jedes Modul ist eine Methode mit seinen eigenen Parametern: await Modules.OrdersOf(customer: "Acme"), geprüft, wenn das Skript kompiliert wird; das Ergebnis ist dynamic. await Modules.CallAsync("OrdersOf", new Dictionary<string, object?> { ["customer"] = "Acme" }) ruft ein Modul weiterhin per Name auf und gibt Task<object?> zurück.
  • Pdf - ScriptPdfHelper. Ein gespeichertes PDF Template per Name rendern: await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) gibt byte[] zurück; await Pdf.GenerateBase64Async(…) gibt dieselbe Nutzlast base64-kodiert zurück. Parameter werden an das Data-Script des Templates als string-typisierte lokale Variablen weitergegeben.
  • Ocr - ScriptOcrHelper. Gedruckten Text aus einem Bild lesen, in-process, ohne externen Dienst und ohne API-Key: await Ocr.ExtractAsync(byte[] image) gibt eine OcrExtraction zurück. await Ocr.ExtractBase64Async(string) nimmt dasselbe Bild base64-kodiert (ein data:image/png;base64,-Präfix wird toleriert), await Ocr.ExtractTextAsync(byte[]) gibt nur den Text zurück.
  • Doc - ScriptDocHelper. Das Dokument in einem Foto finden und flach ziehen: await Doc.CropAsync(byte[] image) gibt einen DocumentCrop zurück. await Doc.CropBase64Async(string) nimmt eine base64-Aufnahme.
  • Files - ScriptFilesHelper. Der eigene Dateiordner des Workspaces, den die Seite Files verwaltet: await Files.WriteAsync("invoices/1042.pdf", bytes), await Files.ReadAsync(path), ListAsync(folder), MoveAsync, DeleteAsync. Siehe Files.

Es gibt kein User, Http, Email, Templates oder irgendein anderes Global.

Db - Single-Row + Write-Methoden

Alle async, alle auf dem Top-Level-Db-Objekt:

  • await Db.GetAsync(string table, object id) - eine Zeile per Primärschlüssel holen. Gibt dynamic? zurück (oder null, wenn nicht gefunden).
  • await Db.CreateAsync(string table, object values) - einfügen. values kann ein anonymes Objekt oder ein Dictionary<string, object?> sein. Feuert Before/After Create-Trigger. Gibt int zurück (die neue ID).
  • await Db.UpdateAsync(string table, object id, object values) - per PK aktualisieren. Feuert Before/After Update-Trigger.
  • await Db.DeleteAsync(string table, object id) - per PK löschen. Feuert Before Delete-Trigger.

Db - fluenter Query-Builder

Beginne mit Db.From(table). Verkette Filter, dann ein Terminal:

  • .Where(string column, string op, object? value = null) - eine WHERE-Klausel hinzufügen. Operatoren: =, != / <>, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL. Mehrere .Where(...)-Aufrufe verketten für AND.
  • .OrderBy(string column, bool desc = false) - ORDER BY setzen. Nur eines pro Query erlaubt.
  • .Limit(int limit) - maximale Zeilen. Standard ist 1000.

Terminale (jedes führt die Query aus):

  • await ...ToListAsync() - gibt List<dynamic> zurück.
  • await ...FirstAsync() - gibt dynamic? zurück (erster Treffer oder null). Setzt das Limit vorübergehend auf 1.
  • await ...CountAsync() - gibt int zurück.

Es gibt kein SumAsync, MaxAsync, FirstOrDefaultAsync oder SingleAsync auf Db.From. Für Aggregationen hole mit ToListAsync() und reduziere in C#, oder nutze die typisierten Sets weiter unten, die all diese Methoden haben.

Durchgearbeitetes Beispiel

// Get a row by ID, query a list, calculate a sum, update.
var order = await Db.GetAsync("orders", 42);
if (order == null) return false;

var items = await Db.From("order_items")
    .Where("order_id", "=", order.id)
    .OrderBy("id")
    .ToListAsync();

decimal totalAmount = 0;
foreach (var item in items) {
    totalAmount += (decimal)(item.amount ?? 0);
}

await Db.UpdateAsync("orders", 42, new {
    total = totalAmount,
    updated_at = DateTime.UtcNow
});

return true;

Db - Business Entities als typisierte Sets

Jede Business Entity ist außerdem ein typisiertes Set auf Db, benannt nach der Entity ohne die Leerzeichen: Eine Entity namens Service Order ist Db.ServiceOrder. Darunter liegt EF Core, daher funktioniert LINQ vollständig (Where, Include, SumAsync, GroupBy, Projektionen), mit einer Navigation für jeden Fremdschlüssel und jeden Join einer Business Entity. Spalten sind PascalCase-Properties mit ihren echten Typen: planned_date ist PlannedDate, ein DateOnly.

// Every business entity is a typed set on Db, named after the entity.
var since = new DateOnly(2026, 1, 1);
var open = await Db.Order
    .Where(o => o.Status == "Open" && o.OrderDate >= since)
    .Include(o => o.OrderItems)
    .OrderBy(o => o.OrderDate)
    .ToListAsync();

foreach (var order in open)
    order.Total = order.OrderItems.Sum(i => i.Amount ?? 0);

var customer = await Db.Customer.SingleAsync(c => c.Email == "ada@example.com");
Db.Order.Add(new Order { Customer = customer, Status = "Open" });

// Writes every changed, added and removed row; business events fire as usual.
await Db.SaveChangesAsync();
return open.Count;
  • Namen. Eine Tabelle, auf der keine Business Entity aufbaut, bekommt ein Set mit dem Namen der Tabelle (bank_account ist Db.BankAccount). Eine Fremdschlüssel-Spalte ohne ihr _id-Suffix ist die Navigation: customer_id ist Customer, und die Gegenseite steht im Plural, Customer.Orders.
  • Speichern. await Db.SaveChangesAsync() schreibt jede hinzugefügte, geänderte oder entfernte Zeile so, wie es CreateAsync, UpdateAsync und DeleteAsync tun: Business Events feuern, Audit-Spalten werden gestempelt, und von einer geänderten Zeile werden nur die geänderten Spalten gesendet. Es ist keine einzelne Transaktion: Zeilen, die vor einem Fehler gespeichert wurden, bleiben gespeichert. ExecuteUpdate, ExecuteDelete und Schreibzugriffe über rohes SQL werden verweigert.
  • Typen. Ein timestamp with time zone wird als DateTime in UTC gelesen, und ein Wert, der damit verglichen wird, muss ebenfalls UTC sein: DateTime.UtcNow. Spalten eines Typs, den das Modell nicht abbildet (Arrays, Enums, geometrische Typen), werden weggelassen; lies sie mit Db.From.
  • Immer aktuell. Das Modell wird neu aufgebaut, sobald sich eine Tabelle oder eine Business Entity ändert, eine entfernte Spalte ist also ein Kompilierfehler in der Zeile, die sie verwendet, statt eines Laufzeitfehlers. Ein ausstehendes Deployment listet vor dem Ausführen die typisierten Skripte auf, die danach nicht mehr kompilieren würden.

Db.From und die Methoden oben funktionieren neben den typisierten Sets weiter, und der Skript-Editor und VS Code vervollständigen die Sets, die Spalten und die Navigationen.

Simulationsmodus

Während der Trigger-Simulation (der Run Simulation-Button auf einem Business Event) und im Test-Tab eines Script Modules steht Db.SimulationMode auf true. Schreiboperationen (CreateAsync, UpdateAsync, DeleteAsync und SaveChangesAsync) prüfen die Tabelle, die Zeile und die Werte, halten fest, was sie getan hätten, und schreiben nichts, daher feuert auch kein Business Event. Leseoperationen sehen die echten Daten. Schreib-, Verschiebe- und Löschvorgänge über Files werden ebenfalls simuliert, und Mails und Benachrichtigungen werden aufgezeichnet, nicht verschickt. Das ist, was Run Simulation sicher gegen echte Daten macht. Alles andere, was das Skript selbst tut, ist echt: HTTP-Aufrufe, System.IO und alles, was ein NuGet-Paket tut, etwa eine Verbindung zu einer anderen Datenbank.

Modules

  • await Modules.OrdersOf(customer: "Acme") - jedes Script Module ist eine Methode, benannt nach dem Modul ohne Leerzeichen ("Orders of" ist Modules.OrdersOf), mit den Parametern und Typen, die es deklariert, die Pflichtparameter zuerst; lässt du einen optionalen Parameter weg, bekommt er seinen Standardwert. Ein falsch geschriebener Name oder ein Wert vom falschen Typ ist ein Kompilierfehler in seiner Zeile. Gibt Task<dynamic?> zurück, das Ergebnis liest du also ohne Cast, auch einen anonymen Typ. Argumente aus Entity sind dynamic: caste sie ((int)Entity.company_id), damit der Aufruf beim Kompilieren geprüft wird.
  • await Modules.CallAsync(string moduleName, Dictionary<string, object?>? parameters = null) - ein anderes Script Module per Name aufrufen. Parameter werden im Skript des aufgerufenen Moduls zu typisierten Variablen. Gibt Task<object?> zurück (was immer das Modul zurückgegeben hat).

Files - der Ordner des Workspaces

Jeder Workspace hat auf dem Server einen eigenen Ordner, den die Seite Files (Operations) verwaltet. Uploads landen dort, ebenso alles, was ein Script aufbewahrt: eine erzeugte Rechnung, ein Export, ein trainiertes Modell. Nichts davon liegt in einer Datenbank, und kein Pfad führt in den Ordner eines anderen Workspaces. Pfade sind relativ und verwenden Schrägstriche; ein Pfad mit .., einem Laufwerk oder einem Link wird abgelehnt.

// A generated invoice, kept in the workspace's own folder.
var pdf = await Pdf.GenerateAsync("Invoice", new Dictionary<string, string> { ["id"] = Entity.orders_id.ToString() });
var path = $"invoices/{DateTime.UtcNow:yyyy}/{Entity.orders_number}.pdf";
await Files.WriteAsync(path, pdf);

// Read it back, or anything an admin uploaded on the Files page.
var rates = await Files.ReadTextAsync("config/vat-rates.json");
foreach (var file in await Files.ListAsync("invoices", recursive: true))
    Log.LogInformation("{Path} is {Size} bytes", file.Path, file.Size);

return path;
  • ReadAsync(path) gibt die Bytes zurück und wirft, wenn es die Datei nicht gibt; ReadTextAsync liest UTF-8-Text; ReadDataUriAsync gibt data:image/png;base64,… zurück, für ein Bild in einem PDF Template oder einer Mail.
  • WriteAsync(path, bytes) und WriteTextAsync legen die Ordner an und ersetzen eine vorhandene Datei. Ein Schreibvorgang gelingt ganz oder gar nicht.
  • ListAsync(folder, recursive) listet Ordner zuerst; jeder Eintrag hat Path, Name, IsFolder, Size, ModifiedUtc und ContentType.
  • MoveAsync(from, to) überschreibt nie; DeleteAsync(path) löscht eine Datei oder einen Ordner mit allem darin.
  • PathOf(path) ist der absolute Pfad auf der Festplatte, für eine Bibliothek, die einen Pfad erwartet.

In einem Probelauf (die Schaltfläche Test) und in einem Testlauf erreichen Schreib-, Verschiebe- und Löschvorgänge den Ordner nie: der Lauf behält sie für sich, sodass eine gerade geschriebene Datei wieder gelesen werden kann, und listet sie bei dem auf, was er aufgezeichnet hat. Der Ordner .archestack enthält die eigenen Dateien der Plattform (Logos, Paket-Icons, Mail-Anhänge): Scripts dürfen ihn lesen, nie schreiben. Ein Backup enthält den Ordner als workspace-files-Archiv neben den Datenbank-Dumps.

Ocr und Doc - ein Foto lesen

Beide laufen im API-Prozess. Kein OCR-Dienst zum Deployen, kein Key zum Konfigurieren, keine Kosten pro Seite.

OcrExtraction (was Ocr zurückgibt):

  • Text - jede erkannte Zeile mit Newlines verbunden, in Lesereihenfolge.
  • Lines - pro Zeile Text, Confidence und eine Bounding Box: X, Y, Width, Height, dazu Right und Bottom. Pixel im Quellbild, Ursprung oben links.
  • Confidence - Mittel der Zeilen-Confidences, 0 bis 1. ElapsedMs, IsEmpty.

Nutze die Boxen. Ein Dokument zu lesen heißt, ein Label zu finden und dann zu lesen, was daneben steht, und genau das kann Text allein nicht ausdrücken: suche die Zeile mit "Summe" und nimm dann die Zeile, deren Y-Bereich sie überlappt und deren X hinter ihrem Right liegt.

DocumentCrop (was Doc zurückgibt):

  • Found - ob tatsächlich eine Kontur erkannt wurde. Prüfe das zuerst.
  • Image - PNG-Bytes der flach gezogenen Seite, oder die unveränderten Originalbytes, wenn Found false ist. Ein nie zugeschnittenes Foto liest sich immer noch; ein auf das falsche Viereck zugeschnittenes nicht, deshalb lehnt der Detektor lieber ab, als zu raten.
  • Corners - die Kontur im Quellfoto, oben links, oben rechts, unten rechts, unten links. Width, Height, ElapsedMs.

Die beiden lassen sich in der naheliegenden Reihenfolge kombinieren, und weil ein fehlgeschlagener Zuschnitt das Original zurückgibt, bringt das Verketten immer etwas: var crop = await Doc.CropAsync(photo); var scan = await Ocr.ExtractAsync(crop.Image);

Drei Grenzen, die in den Entwurf gehören. Das mitgelieferte Modell ist PP-OCRv5 latin, also westliche Alphabete und Ziffern, kein CJK. Es ist kein PDF-Leser, eine PDF-Seite muss zuerst zu einem Bild gerastert werden. Und das ist OCR, kein Dokumentenverständnis: aus Zeilen Lieferant, Rechnungsnummer und Summe zu machen, ist Logik, die du schreibst, und sie wird manchmal falsch liegen, also prüfe line.Confidence und leite niedrige Werte an einen Menschen weiter, statt sie direkt in eine Tabelle zu schreiben.

Achte auf das Skript-Timeout. Eine volle Seite dauert ein bis zwei Sekunden, und der erste Aufruf im Prozess kommt mit rund einer Sekunde zum Laden der Modelle dazu. Das Zehn-Sekunden-Timeout unterbricht kein await, ein Skript, das mehrere Seiten liest, gibt also gar nichts zurück, statt früh zu scheitern. Mehrseitige Arbeit gehört in ein Scheduled Event, eine Seite pro Lauf.

Template-Ausdrücke (Business Event Actions)

Action-Konfiguration nutzt {{ … }}-Klammern. Die vollständige Liste gültiger Tokens:

  • {{ Entity.column_name }} / {{ Entity.id }}
  • {{ OldEntity.column_name }}
  • {{ now() }} / {{ getdate() }} - ISO 8601 UTC-Datetime
  • {{ today() }} - Datums-String
  • {{ guid() }} / {{ newid() }} - frische GUID
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }} - Unix-Sekunden
  • {{ empty() }}, {{ null() }}
  • Aggregate in einem Join-Kontext: {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Arithmetik: {{ Entity.quantity * Entity.price }} - nach der Substitution über DataTable.Compute() ausgewertet

Es gibt keinen {{ user.email }}- oder anderen nutzerbezogenen Token.