Handbuch · 11

Workflows

Workflows verbinden kleine Schritte zu einem Ablauf – ähnlich wie Node-RED, nur abgespeckt. Ein Flow beginnt an einem Start-Node (Knopf, Zeitplan, HTTP-Endpunkt oder Collection-Ereignis); danach holst du Daten, formst sie mit JavaScript um, verzweigst, speicherst sie in einer Collection oder gibst sie als HTTP-Antwort zurück. Typische Fälle: ein JSON-Endpunkt für eine Anzeigetafel, ein Kontaktformular, das in einer Collection landet, ein nächtlicher Abgleich mit einem anderen System.

Der Editor

Unter Workflows steht jeder Workflow als Reiter oben. Darunter:

  • Palette (links): Nodes auf die Fläche ziehen oder anklicken.
  • Fläche (Mitte): Verbinden durch Ziehen vom Ausgang (rechts am Node) zum Eingang (links). Ein Ausgang darf zu mehreren Nodes führen – die Nachricht läuft dann in alle Zweige. Entf löscht markierte Nodes und Drähte; Mausrad zoomt, Ziehen auf freier Fläche verschiebt.
  • Seitenleiste (rechts): Node zeigt die Einstellungen des angeklickten Nodes, Debug die Ausgaben des Laufs, Läufe die letzten 50 Läufe, Speicher den Speicher des Workflows.
  • Kopieren und Einfügen: Mehrere Nodes markierst du mit Strg (Mac: ⌘) + Klick oder mit ⇧ + Rahmen ziehen. Strg+C kopiert sie samt den Drähten zwischen ihnen, Strg+V fügt sie ein – auch in einen anderen Workflow oder einen anderen Browser-Tab; Strg+X schneidet aus. Die Kopien bekommen neue IDs und sind danach markiert, damit du sie gleich verschieben kannst.

▶ am Start-Node löst den Flow aus – mit dem Stand auf der Fläche, auch ungespeichert. Speichern macht HTTP-Eingänge, Zeitpläne und Ereignisse scharf; der Schalter Aktiv schaltet sie gemeinsam ab (▶ geht trotzdem).

Nach einem Lauf zeigt jeder Node, wie oft er ausgeführt wurde (rot bei Fehler). Klickst du ihn an, siehst du unten in der Seitenleiste seinen Eingang und was an jedem Ausgang herauskam – so findest du schnell den Schritt, der hakt.

Nachrichten

Über die Drähte läuft eine Nachricht msg – ein JSON-Objekt. Die eigentlichen Daten stecken in msg.payload; Nodes können weitere Felder anhängen (msg.req beim HTTP-Eingang, msg.statusCode nach einer HTTP-Anfrage). Jeder Node bekommt eine eigene Kopie, Zweige beeinflussen sich also nicht.

In Textfeldern der Nodes (URL, Header, Body, Eintrags-ID) setzt {{ msg.pfad }} Werte ein, z. B. https://api.example/artikel/{{ msg.payload.id }}. Umgebungsvariablen des Servers sind in Workflows nicht verfügbar – Zugangsdaten und Einstellungen trägst du direkt im Node oder im Skript ein.

Die Nodes

Node Was er tut
Start Startet per ▶ und/oder nach Zeitplan. msg.payload = der hinterlegte Wert (JSON oder Text)
HTTP-Eingang Eigener Endpunkt unter /api/v1/flows/<pfad>. msg.payload = JSON- oder Formular-Body, ohne Body die Query-Parameter; msg.req = method, path, params, query, headers, body
Collection-Ereignis Startet, wenn in einer Collection ein Eintrag angelegt, geändert oder gelöscht wird. msg.payload = der Eintrag, msg.event, msg.collection
Funktion JavaScript mit beliebig vielen Ausgängen (siehe unten)
Weiche Prüft einen Wert (msg.payload.preis, msg.req.query.format, …) gegen Regeln – je Regel ein Ausgang. Regeln: ==, !=, <, >, enthält, passt auf RegExp, ist wahr/falsch/leer, sonst. Normal geht die Nachricht an die erste passende Regel, auf Wunsch an alle
HTTP-Anfrage Ruft eine URL auf (Methode, Header, Body). Die Antwort landet in msg.payload (JSON wird gelesen), Status in msg.statusCode, Header in msg.headers. Pfade wie /api/v1/… gehen an dieses CMS. Optional: Fehler bei Status ab 400
Collection Einträge lesen (alle oder einen), anlegen, ändern, löschen. Zum Anlegen und Ändern kommen die Felder aus msg.payload, die ID standardmäßig aus {{ msg.payload.id }}. Ergebnis in msg.payload
IllusionDB Datensätze aus IllusionDB lesen (Filter, Sortierung, Gruppierung, Seiten), einen über Primärschlüssel oder Spalte lesen, anlegen, ändern, löschen, Schema und Tabellen abfragen. Ergebnis in msg.payload
Verzögern Wartet die eingestellte Zeit (Sekunden, auch Bruchteile), bevor die Nachricht weitergeht. Andere Zweige laufen solange weiter. Höchstens bis zum Zeitlimit des Laufs
Zusammenführen Sammelt Nachrichten aus mehreren Zweigen und gibt eine weiter, sobald alle da sind – standardmäßig so viele, wie Drähte hineinführen. msg.payload wird zur Liste der payloads (Ankunftsreihenfolge) oder zum Objekt mit msg.topic als Schlüssel. Kommt nicht alles an, geht nichts weiter (Warnung im Debug)
E-Mail Verschickt eine E-Mail über den SMTP-Zugang der Instanz: An (mehrere mit Komma), Betreff, Text (leer = msg.payload), Antwort an, optional als HTML – überall mit {{ msg.… }}. Die Nachricht geht unverändert weiter
HTTP-Antwort Antwortet dem HTTP-Eingang: msg.payload als Body (Objekt → JSON, Text → text/plain), Status und Header einstellbar
Debug Zeigt msg.payload, die ganze msg oder einen Pfad in der Debug-Leiste
Eigene Deine eigenen Nodes – wiederverwendbare Skripte mit Parametern und benannten Ausgängen

Funktion mit mehreren Ausgängen

Der Code der Funktion bekommt msg und entscheidet mit return, wohin es weitergeht:

// Einstellung: 3 Ausgänge
if (msg.payload.preis > 5) return [msg, null, null]   // nur Ausgang 1
if (msg.payload.preis > 2) return [null, msg, null]   // nur Ausgang 2
return [null, null, msg]                               // nur Ausgang 3
  • return msg – weiter über Ausgang 1
  • return null – hier endet dieser Zweig
  • return [a, null, b] – je Ausgang eine Nachricht oder null
  • return [[a, b, c]] – mehrere Nachrichten über denselben Ausgang (z. B. eine je Eintrag)

So bildest du viel Logik in einem Node ab, statt viele Weichen zu verketten. Im Code stehen außerdem bereit:

Name Bedeutung
node.warn(…), console.log(…) Ausgabe in die Debug-Leiste
store.get(key, standard), store.set(key, wert), store.delete(key), store.keys() Speicher des Workflows – bleibt über Läufe erhalten
fetch(url, { method, headers, body }) HTTP-Anfrage, synchron (kein await): Antwort mit status, ok, headers, text(), json()
ic.items(slug), ic.item(slug, id) Einträge einer Collection (auch Entwürfe – Feld published)
ic.createItem(slug, data, { published }) Eintrag anlegen; Werte werden wie in der API gegen das Schema geprüft
ic.updateItem(slug, id, data), ic.deleteItem(slug, id) Ändern (nur übergebene Felder) bzw. löschen
ic.data(slug, params) Wie data() im Template – Collections und REST-Quellen
ic.api(method, path, body) Beliebiger Endpunkt der REST-API
idb.records(…), idb.find(…), idb.create(…) … Datensätze in IllusionDB lesen und schreiben

Die JavaScript-Engine (goja) kennt ES5.1 und die gängigen Neuerungen danach – const/let, Pfeilfunktionen, Template-Strings, Destrukturierung, Array-Methoden –, aber kein Intl und kein await. Zahlen und Datumswerte formatierst du von Hand, etwa betrag.toFixed(2).replace('.', ',') oder mit einem regulären Ausdruck für Tausenderpunkte.

Schlägt ein ic.*- oder idb.*-Aufruf fehl (z. B. Pflichtfeld fehlt), wirft er einen Fehler, den du mit try { … } catch (e) { … } abfangen kannst. fetch() wirft nur bei Netzwerkfehlern – den Status prüfst du über ok.

Beispiele

Getränkekarte für ein Bar-Board – HTTP-Eingang (GET, Pfad karte, öffentlich) → Collection (lesen, alle, nur veröffentlichte) → Funktion → HTTP-Antwort:

msg.payload = msg.payload.map((d) => ({ name: d.data.name, preis: d.data.preis.toFixed(2) + ' €' }))
return msg

https://deine-domain.de/api/v1/flows/karte liefert dann JSON, auch per fetch() von einer anderen Domain. Mit einer Weiche auf msg.req.query.format kannst du z. B. bei ?format=text einen zweiten Zweig mit reinem Text beantworten.

Kontaktformular – HTTP-Eingang (POST, Pfad kontakt, öffentlich) → Funktion mit 2 Ausgängen → Ausgang 1: Collection (anlegen in „anfragen“, Entwurf) → Funktion „Weiterleiten“ → HTTP-Antwort; Ausgang 2 direkt zu „Weiterleiten“:

<form method="post" action="/api/v1/flows/kontakt">
  <input name="name" required> <textarea name="nachricht" required></textarea>
  <input name="website" style="display:none" tabindex="-1" autocomplete="off"> <!-- Falle für Bots -->
  <button>Senden</button>
</form>
// Prüfen: Bots füllen das versteckte Feld aus
if (msg.payload.website) return [null, msg]
msg.payload = { name: msg.payload.name, nachricht: msg.payload.nachricht }
return [msg, null]
// „Weiterleiten“ (vor der HTTP-Antwort, dort Status 303 eintragen)
msg.payload = ''
return msg

In der HTTP-Antwort zusätzlich den Header Location: /danke setzen. Bei der Collection „anfragen“ im Schema Live-Daten anhaken – sonst überschreibt das nächste Bundle von lokal die eingegangenen Anfragen.

Nächtlicher Abgleich – Start (Zeitplan 0 3 * * *) → HTTP-Anfrage (https://kasse.example/api/artikel, Header Authorization: Bearer k123, Fehler ab 400) → Funktion:

const vorhanden = ic.items('drinks')
let neu = 0, geaendert = 0
for (const a of msg.payload) {
  const d = vorhanden.find((x) => x.data.name === a.name)
  if (!d) { ic.createItem('drinks', { name: a.name, preis: a.preis }); neu++ }
  else if (d.data.preis !== a.preis) { ic.updateItem('drinks', d.id, { preis: a.preis }); geaendert++ }
}
node.warn(neu + ' neu, ' + geaendert + ' geändert')
return null

Im Template aufrufen

Ein Flow mit HTTP-Eingang (GET oder ANY) lässt sich direkt aus einer Seite oder einem Snippet aufrufen – intern, ohne Netzwerk und ohne Token (auch geschützte Eingänge):

<ul>
{% for d in workflow("karte", "format", "kurz") %}
  <li>{{ d.name }} – {{ d.preis }}</li>
{% endfor %}
</ul>
  • Der erste Wert ist der Pfad des HTTP-Eingangs (karte für /api/v1/flows/karte; Platzhalter wie text/{wer} gehen auch: workflow("text/Ada")).
  • Weitere Werte kommen paarweise als Parameter in msg.req.query und msg.payload an – hier { "format": "kurz" }.
  • Zurück kommt, was der HTTP-Antwort-Node schicken würde: JSON als Daten (Listen, Objekte), sonst Text. Ohne HTTP-Antwort gibt es nichts.
  • Fehler im Flow erscheinen wie andere Template-Fehler. Zeitlimit hier: 10 Sekunden – langsame externe Dienste machen die Seite langsam.
  • Seiten mit workflow() hält der Seiten-Cache höchstens eine Minute, damit externe Daten frisch bleiben. In der Vorschau des Editors wird das Ergebnis 30 Sekunden gemerkt, damit der Flow nicht bei jedem Tastendruck läuft.
  • Im Protokoll des Workflows erscheinen diese Läufe als Auslöser Seite.

Mehrere Parameter, z. B. aus der URL: Weitere Paare einfach anhängen – workflow("pfad", "a", wert1, "b", wert2, …). Die Parameter der aufgerufenen Seite stehen im Template in route.query. Beispiel: eine Getränkekarte mit Suche und Höchstpreis unter /karte?suche=mate&max=4.

Seite /karte:

<form>
  <input name="suche" value="{{ route.query.suche }}" placeholder="Name">
  <input name="max" type="number" step="0.5" value="{{ route.query.max }}" placeholder="max. Preis">
  <button>Filtern</button>
</form>
<ul>
{% for d in workflow("karte-filter", "suche", route.query.suche, "max", route.query.max) %}
  <li>{{ d.name }} – {{ d.preis }} €</li>
{% empty %}
  <li>Nichts gefunden.</li>
{% endfor %}
</ul>

Workflow: HTTP-Eingang (GET, Pfad karte-filter; öffentlich ist für den Aufruf aus dem Template nicht nötig) → Collection (lesen, alle, nur veröffentlichte) → Funktion → HTTP-Antwort. Der Collection-Node überschreibt msg.payload mit den Einträgen – die Parameter liest die Funktion deshalb aus msg.req.query:

// Werte aus der URL sind Text (oder fehlen) – vor der Verwendung prüfen
const suche = String(msg.req.query.suche || '').trim().toLowerCase()
const max = parseFloat(String(msg.req.query.max || '').replace(',', '.'))
msg.payload = msg.payload
  .filter((d) => !suche || d.data.name.toLowerCase().includes(suche))
  .filter((d) => isNaN(max) || d.data.preis <= max)
  .map((d) => ({ name: d.data.name, preis: d.data.preis }))
return msg
  • Fehlt ein Parameter in der URL, kommt er leer an – hier zeigt die Karte dann alles.
  • Werte aus der URL sind Eingaben von Besuchern: im Workflow prüfen, bevor sie z. B. in eine HTTP-Anfrage an einen anderen Dienst wandern. Im Template werden Ausgaben automatisch escaped.
  • Der Seiten-Cache unterscheidet nach der vollen Adresse samt Parametern – jede Kombination läuft höchstens einmal pro Minute durch den Workflow.
  • Bei mehrfach vorkommenden Parametern (?a=1&a=2) enthält route.query nur den ersten Wert.

Zeitplan

Der Zeitplan am Start-Node ist ein Cron-Ausdruck mit fünf Feldern – Minute Stunde Tag Monat Wochentag (0 = Sonntag). Es gibt Vorlagen; oben neben Aktiv steht der nächste Lauf.

Ausdruck Bedeutung
*/15 * * * * alle 15 Minuten
0 * * * * stündlich zur vollen Stunde
0 3 * * * täglich um 3 Uhr
30 7 * * 1-5 werktags um 7:30 Uhr
0 6 1 * * am Ersten jedes Monats um 6 Uhr

Es gilt die Zeitzone des Servers (im Docker-Image Europe/Berlin); eine andere per Präfix: CRON_TZ=Europe/Berlin 0 3 * * *. Läuft ein Start-Node beim nächsten Termin noch, wird dieser übersprungen; Termine, an denen der Server aus war, werden nicht nachgeholt.

HTTP-Eingänge

  • Pfad hinter /api/v1/flows/, mit Platzhaltern: drinks/{id} passt auf /api/v1/flows/drinks/5 (msg.req.params.id ist "5"), dateien/{rest...} am Ende nimmt den Rest auf. Ein Pfad mit derselben Methode gehört genau einem aktiven Workflow.
  • Geschützt (Standard): nur mit API-Token (Authorization: Bearer … oder bei GET ?token=…) oder als angemeldeter Admin. Öffentlich: ohne Anmeldung, auch per JavaScript von anderen Domains (CORS).
  • Die Antwort kommt vom ersten erreichten HTTP-Antwort-Node. Endet der Flow ohne einen, antwortet der Server mit 204; bei einem Fehler mit 500 und der Meldung.
  • Aus Seiten und Snippets rufst du einen HTTP-Eingang mit workflow() auf.
  • Im Editor testest du einen HTTP-Eingang über Testlauf in seinen Einstellungen – mit selbst gewählter payload, ohne echte Anfrage.

Collection-Ereignisse

Ereignisse laufen nach dem Speichern im Hintergrund – das Speichern im Admin oder per API wartet nicht darauf. Sie greifen bei jeder Änderung über Admin, API und Workflows. Ändert ein Workflow selbst Einträge, die wieder Ereignisse auslösen, bricht die Kette nach drei Stufen ab.

Speicher und E-Mail

Speicher: Jeder Workflow hat einen kleinen Schlüssel-Wert-Speicher, der über Läufe hinweg erhalten bleibt – für Zähler, „zuletzt verarbeitet“ oder ein Zwischenergebnis. Werte sind beliebige JSON-Werte (je Wert bis 64 KB, bis 1.000 Schlüssel). Der Reiter Speicher in der Seitenleiste zeigt den Inhalt; dort löschst du einzelne Schlüssel oder alles. Der Speicher gehört zur Instanz: Er wandert nicht mit Bundles, der Stand auf dem Live-Server bleibt beim Hochladen also erhalten. Mit dem Workflow wird auch sein Speicher gelöscht.

Beispiel – ein Zeitplan meldet nur Anfragen, die seit dem letzten Lauf dazugekommen sind (Start mit Zeitplan → Collection „lesen (alle)“ → Funktion → E-Mail):

const zuletzt = store.get("zuletzt", 0)
const neu = msg.payload.filter((e) => e.id > zuletzt)
if (!neu.length) return null
store.set("zuletzt", Math.max(...neu.map((e) => e.id)))
msg.payload = neu.length + " neue Anfrage(n): " + neu.map((e) => e.data.name).join(", ")
return msg

E-Mail-Versand: Den SMTP-Zugang trägst du unter Einstellungen → System → E-Mail-Versand ein (Server, Port, Benutzer, Passwort, Absender wie Website <web@firma.de>) und prüfst ihn mit Testmail senden. Port 465 verbindet per TLS, andere Ports per STARTTLS; ohne Verschlüsselung wird kein Passwort gesendet. Der Zugang gilt nur für diese Instanz – lokal und live richtest du ihn getrennt ein (im Remote-Modus direkt für den Server).

IllusionDB

IllusionDB verwaltet bestehende Datenbanken (PostgreSQL, MySQL/MariaDB, SQLite) mit Oberfläche und REST-API. Der Node IllusionDB holt Datensätze direkt von dort – Produkte, Teams, Termine, Detailseiten –, ohne sie in eine Collection zu kopieren.

Verbindung einrichten: Einstellungen → IllusionDB: Name, Adresse (z. B. https://db.firma.de) und API-Token (in IllusionDB unter Einstellungen → Mein Konto; Leserecht auf die Tabellen genügt zum Lesen). Verbindung testen zeigt die Version und wie viele Tabellen lesbar sind. Ohne Token sind nur Tabellen erreichbar, die in IllusionDB öffentlich lesbar sind.

  • Workflows verweisen über den Namen der Verbindung, Tabellen über ihren Namen (bei gleichnamigen Tabellen in mehreren Datenquellen Datenquelle/tabelle). Lege lokal und live je eine Verbindung mit demselben Namen an – derselbe Workflow nutzt dann die jeweils passende Datenbank.
  • Die Verbindungen gehören zur Instanz, nicht ins Bundle; der Token wird nie ausgeliefert. Im Live-Modus bearbeitest du die des Servers.

Der Node: Er erscheint in der Palette erst, wenn mindestens eine Verbindung eingerichtet ist. Verbindung, Tabelle, Spalten und gespeicherte Ansichten kommen als Auswahl live vom IllusionDB-Server (↻ lädt das Schema neu, z. B. nach neuen Spalten).

Aktion Ergebnis in msg.payload
Datensätze lesen Liste – mit Filter, Sortierung, Gruppierung, Zusammenfassung, Anzahl und Seite, optional ausgehend von einer gespeicherten Ansicht
einen lesen ein Datensatz über den Primärschlüssel oder über eine Spalte (z. B. slug); nicht gefunden: null oder auf Wunsch ein Fehler
anlegen / ändern / löschen Spalten aus msg.payload, Primärschlüssel standardmäßig {{ msg.payload.id }}; Ergebnis: der gespeicherte Datensatz
Schema die Tabelle mit allen Spalten (Name, Typ, Primärschlüssel, …)
Tabellen auflisten alle lesbaren Tabellen aller Datenquellen
  • Filter: Regeln Spalte – Vergleich – Wert (=, ≠, >, ≥, <, ≤, enthält, eins von a, b, c, keins von, ist leer, ist nicht leer), verknüpft mit alle Regeln oder mindestens eine. Werte dürfen {{ msg.… }} enthalten; Zahlen und Ja/Nein werden passend zum Spaltentyp umgewandelt. Regeln, deren Wert leer bleibt, entfallen – ein leeres Suchfeld filtert also nicht (abschaltbar).
  • Filter als JSON ersetzt die Regeln, für verschachtelte Und/Oder-Bedingungen im Format der IllusionDB-API: {"or": [{"col": "name", "op": "like", "val": "{{ msg.payload.q }}"}, {"col": "preis", "op": "lt", "val": {{ msg.payload.max }}}]}. Ein Platzhalter in Anführungszeichen wird als Text eingesetzt, ohne Anführungszeichen als JSON-Wert (Zahl, true, Liste).
  • Nur diese Spalten (beim Lesen von Listen und einzelnen Datensätzen): z. B. slug, name, preis – IllusionDB schickt nur diese Spalten und überspringt die Berechnung nicht benötigter Formel-, Lookup- und Verknüpfungsfelder; das spart bei breiten oder großen Tabellen Datenvolumen und Zeit. Ein unbekannter Spaltenname ist ein Fehler. Leer = alle (ältere IllusionDB-Versionen liefern immer alle).
  • Seiten: Anzahl (höchstens 1000) und Seite (z. B. {{ msg.payload.seite }}) – oder alle Seiten laden (bis 10 000 Datensätze).
  • Ausgabe: die Liste der Datensätze; alles = records, total, page, pages, has_more, summary, groups; nur die Gruppen; oder der erste Datensatz.
  • Gruppen: je Gruppe value, count (über alle Treffer, nicht nur die geladene Seite) und records – bei mehreren Gruppierungs-Spalten stattdessen groups eine Ebene tiefer.
  • Zusammenfassung: SUM, AVG, COUNT, MIN, MAX über alle Treffer, in summary.<spalte>. SUM und AVG rechnet IllusionDB nur für Spalten vom Typ Zahl oder Dezimalzahl.
  • Ergebnis merken: so viele Sekunden wird ein Leseergebnis zwischengespeichert – entlastet die Datenbank bei viel Verkehr (0 = jedes Mal fragen). Tabellen und Spalten merkt sich das CMS eine Minute.
  • Spaltennamen sind die Namen, die IllusionDB in seiner API verwendet (Anzeigename, falls gesetzt).

Liste und Detailseiten direkt aus der Datenbank – wie der Blog in Kapitel 10, nur mit IllusionDB statt Collection. Zwei Workflows:

  • HTTP-Eingang (GET, Pfad produkte) → IllusionDB (Datensätze lesen, Tabelle produkte, Regel aktiv = true, Sortierung name, Gruppierung kategorie, Anzahl 20, Seite {{ msg.payload.seite }}, Ausgabe alles) → HTTP-Antwort
  • HTTP-Eingang (GET, Pfad produkt) → IllusionDB (einen lesen, Tabelle produkte, suchen über Spalte slug, Wert {{ msg.payload.slug }}) → HTTP-Antwort

Dazu eine Seite /produkte mit Catch-All und Cache deaktivieren:

{% if route.slug %}
  {% set p = workflow("produkt", "slug", route.slug) %}
  {% if not p %}{{ not_found() }}{% endif %}
  <h1>{{ p.name }}</h1>
  <p>{{ p.preis }} € · {{ p.kategorie }}</p>
  <a href="/produkte">← Alle Produkte</a>
{% else %}
  {% set r = workflow("produkte", "seite", route.query.seite|default:"1") %}
  {% for g in r.groups %}
    <h2>{{ g.value }} ({{ g.count }})</h2>
    {% for p in g.records %}<a href="/produkte/{{ p.slug }}">{{ p.name }}</a>{% endfor %}
  {% endfor %}
  <p>Seite {{ r.page }} von {{ r.pages }} · {{ r.total }} Produkte</p>
  {% if r.has_more %}<a href="?seite={{ r.page + 1 }}">weiter</a>{% endif %}
{% endif %}
  • /produkte/mate zeigt den Datensatz mit slug = mate, ein unbekannter Slug die 404-Seite (einen lesen liefert dann null).
  • Mit Cache deaktivieren ist jede Änderung in IllusionDB sofort sichtbar; ohne hält der Seiten-Cache die Seite höchstens eine Minute. Bei viel Verkehr: Cache an lassen und im Node Ergebnis merken einstellen.
  • Sitemap: In den Einstellungen der Seite unter Detailseiten für Sitemap und statischen Export die Quelle IllusionDB-Tabelle wählen (Verbindung shop, Tabelle produkte, Spalte slug, Regel aktiv = true) – siehe Kapitel 10. Mit Prüfen siehst du sofort, wie viele Seiten es werden.

Tausende Landingpages: Das Muster trägt auch große Mengen – etwa eine Tabelle landingpages mit Spalten slug (berlin/zahnarzt), stadt, leistung, text, aktiv, geaendert, gepflegt in IllusionDB (oder per CSV-Import dorthin). Eine Catch-All-Seite /leistungen mit einem Template, ein Workflow einen lesen über slug, die Sitemap-Quelle auf dieselbe Tabelle – fertig sind alle Seiten samt Sitemap. Neue Zeilen in IllusionDB sind sofort online (mit Cache deaktivieren) und spätestens nach 10 Minuten in der Sitemap.

Im Function-Node steht idb bereit – für alles, was sich mit dem Node nicht zusammenklicken lässt:

const r = idb.records('shop', 'produkte', {
  where: { and: [{ col: 'aktiv', op: 'eq', val: true }, { col: 'preis', op: 'lt', val: 5 }] },
  sort: '-preis', // mehrere: 'kategorie,-preis'
  limit: 50,
})
msg.payload = r.records.map((p) => `${p.name}: ${p.preis} €`)
return msg

idb.records(verbindung, tabelle, { where, sort, group_by, summary, fields, view, limit, page, all }) liefert dasselbe wie die Ausgabe alles; dazu idb.record(v, t, pk) (oder null), idb.find(v, t, spalte, wert) (erster Treffer oder null), idb.create(v, t, daten), idb.update(v, t, pk, daten), idb.delete(v, t, pk), idb.schema(v, t), idb.tables(v) und idb.api(v, methode, pfad, body) für jeden anderen Endpunkt der IllusionDB-API (Pfad ohne /api/v1). Fehler von IllusionDB brechen den Lauf mit der Meldung ab.

Eigene Nodes

Schreibst du dasselbe Skript immer wieder (Preise formatieren, Text kürzen, eine Benachrichtigung an einen Dienst), machst du daraus einen eigenen Node: einmal geschrieben, in jedem Workflow aus der Palette (Gruppe Eigene) ziehbar – mit Einstellungen je Einsatz und benannten Ausgängen. Ändert sich das Skript, wirkt das in allen Workflows.

Angelegt und bearbeitet werden sie unter Workflows → Eigene Nodes (oben rechts):

  • Name, Beschreibung, Farbe – so erscheint der Node in Palette und Fläche; die Beschreibung ist sein Hilfetext.
  • Eingang: mit Eingang verarbeitet Nachrichten wie ein Function-Node. Ohne Eingang ist ein Start-Node – mit ▶ und optionalem Zeitplan wie Start, z. B. „Feed abrufen“.
  • Ausgänge: 0 bis 10, jeder mit optionaler Bezeichnung („ok“, „Fehler“), die am Node steht.
  • Parameter: Felder, die im Workflow je Einsatz ausgefüllt werden – Text, Text, mehrzeilig, Zahl, Ja/Nein oder Auswahl (mit Optionen), jeweils mit Standardwert und Hilfetext. Im Code stehen sie in config.<name>; in Texten wird {{ msg.… }} vorher ersetzt.
  • Code: JavaScript wie im Function-Node – mit msg und config, Rückgabe msg, [a, null, b] je Ausgang oder null. Alles andere (fetch, store, ic.*, idb.*, node.warn) steht ebenfalls bereit.
  • Testen: Parameter und eine Beispiel-Nachricht eingeben, Ausführen zeigt, was an jedem Ausgang herauskommt – auch ungespeichert, ohne Eintrag in den Läufen.

Beispiel Preis formatieren – Parameter waehrung (Auswahl €, $, Standard €) und stellen (Zahl, Standard 2), Ausgänge „ok“ und „ohne Preis“:

const preis = Number(msg.payload.preis)
if (msg.payload.preis == null || isNaN(preis)) return [null, msg]
msg.payload.preisText = preis.toFixed(config.stellen).replace('.', ',') + ' ' + config.waehrung
return [msg, null]
  • Workflows verweisen über den Key (aus dem Namen beim Anlegen, z. B. preis-formatieren) – Umbenennen ist also gefahrlos.
  • Ein Node lässt sich erst löschen, wenn ihn kein Workflow mehr verwendet; Verlauf zeigt frühere Stände des Codes.
  • Eigene Nodes gehören zum Inhalt: Sie kommen mit ins Bundle und damit beim Hochladen auf den Live-Server.

Grenzen und Sicherheit

  • Zeitlimits: 30 Sekunden für Flows mit HTTP-Eingang, 5 Minuten sonst; höchstens 1.000 Node-Ausführungen je Lauf (Schutz vor Schleifen im Graph). Ruft ein Flow sich über seinen eigenen Hook selbst auf, ist nach drei Ebenen Schluss.
  • Workflows laufen mit vollen Rechten: Sie können alle Inhalte ändern und per HTTP-Anfrage auch Adressen im internen Netz erreichen. Wer Workflows bearbeitet, braucht dasselbe Vertrauen wie bei HTML und Templates.
  • Das SMTP-Passwort steht in der Datenbank der Instanz (nicht in Bundles, im Admin nie wieder angezeigt).
  • Zugangsdaten stehen im Node oder im Skript – und damit auch in Bundles und in der Versionshistorie. Gib Bundles und Backups deshalb nicht aus der Hand. Umgebungsvariablen des Servers sind in Workflows bewusst nicht verfügbar.
  • Öffentliche Eingänge sind für jeden erreichbar – Eingaben prüfen, keine internen Daten zurückgeben.
  • Collection-Einträge haben keine Versionshistorie. Wer per Workflow viele Einträge ändert, lädt vorher ein Bundle als Sicherung herunter.