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) |
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 1return null– hier endet dieser Zweigreturn [a, null, b]– je Ausgang eine Nachricht odernullreturn [[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 (
kartefür/api/v1/flows/karte; Platzhalter wietext/{wer}gehen auch:workflow("text/Ada")). - Weitere Werte kommen paarweise als Parameter in
msg.req.queryundmsg.payloadan – 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ältroute.querynur 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.idist"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 beiGET?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 mit500und 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 vona, 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) undrecords– bei mehreren Gruppierungs-Spalten stattdessengroupseine Ebene tiefer. - Zusammenfassung:
SUM,AVG,COUNT,MIN,MAXüber alle Treffer, insummary.<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, Pfadprodukte) → IllusionDB (Datensätze lesen, Tabelleprodukte, Regelaktiv=true, Sortierungname, Gruppierungkategorie, Anzahl20, Seite{{ msg.payload.seite }}, Ausgabe alles) → HTTP-Antwort - HTTP-Eingang (
GET, Pfadprodukt) → IllusionDB (einen lesen, Tabelleprodukte, suchen über Spalteslug, 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/matezeigt den Datensatz mitslug = mate, ein unbekannter Slug die 404-Seite (einen lesen liefert dannnull).- 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, Tabelleprodukte, Spalteslug, Regelaktiv=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
msgundconfig, Rückgabemsg,[a, null, b]je Ausgang odernull. 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.