Manual · 11

Workflows

Los Workflows conectan pequeños pasos en un proceso, de forma parecida a Node-RED, pero simplificado. Un flow empieza en un node de inicio (botón, programación, endpoint HTTP o evento de collection); después obtienes datos, los transformas con JavaScript, ramificas, los guardas en una Collection o los devuelves como respuesta HTTP. Casos típicos: un endpoint JSON para un panel de información, un formulario de contacto que acaba en una Collection, una sincronización nocturna con otro sistema.

El editor

En Workflows, cada Workflow aparece como pestaña en la parte superior. Debajo:

  • Paleta (izquierda): arrastra los Nodes al lienzo o haz clic en ellos.
  • Lienzo (centro): conecta arrastrando desde la salida (a la derecha del node) hasta la entrada (a la izquierda). Una salida puede llevar a varios Nodes: el mensaje recorre entonces todas las ramas. Supr elimina los Nodes y cables seleccionados; la rueda del ratón hace zoom, y arrastrar sobre una zona libre desplaza la vista.
  • Barra lateral (derecha): Node muestra los ajustes del node en el que has hecho clic, Debug las salidas de la ejecución, Ejecuciones las últimas 50 ejecuciones y Almacén el almacén del Workflow.
  • Copiar y pegar: seleccionas varios Nodes con Ctrl (Mac: ⌘) + clic o con ⇧ + arrastrar un marco. Ctrl+C los copia junto con los cables entre ellos, Ctrl+V los pega, también en otro Workflow o en otra pestaña del navegador; Ctrl+X corta. Las copias reciben ID nuevos y quedan seleccionadas, para que puedas moverlas enseguida.

▶ en el node de inicio dispara el flow, con el estado del lienzo, aunque no esté guardado. Guardar activa las entradas HTTP, las programaciones y los eventos; el interruptor Activo los desactiva todos juntos (▶ funciona igualmente).

Tras una ejecución, cada node muestra cuántas veces se ha ejecutado (en rojo si hubo error). Si haces clic en él, ves abajo en la barra lateral su entrada y lo que salió por cada salida: así encuentras rápidamente el paso que falla.

Mensajes

Por los cables circula un mensaje msg, un objeto JSON. Los datos propiamente dichos están en msg.payload; los Nodes pueden añadir más campos (msg.req en la entrada HTTP, msg.statusCode tras una petición HTTP). Cada node recibe su propia copia, así que las ramas no se influyen entre sí.

En los campos de texto de los Nodes (URL, cabecera, body, ID de entrada), {{ msg.pfad }} inserta valores, p. ej. https://api.example/artikel/{{ msg.payload.id }}. Las variables de entorno del servidor no están disponibles en los Workflows: introduce credenciales y ajustes directamente en el Node o en el script.

Los Nodes

Node Qué hace
Inicio Arranca con ▶ y/o según una programación. msg.payload = el valor configurado (JSON o texto)
Entrada HTTP Endpoint propio en /api/v1/flows/<pfad>. msg.payload = body JSON o de formulario; sin body, los parámetros de consulta; msg.req = method, path, params, query, headers, body
Evento de collection Arranca cuando en una Collection se crea, modifica o elimina una entrada. msg.payload = la entrada, msg.event, msg.collection
Función JavaScript con tantas salidas como quieras (ver más abajo)
Desvío Comprueba un valor (msg.payload.preis, msg.req.query.format, …) contra reglas, con una salida por regla. Reglas: ==, !=, <, >, contiene, coincide con RegExp, es verdadero/falso/vacío, si no. Normalmente el mensaje va a la primera regla que coincide; si quieres, a todas
Petición HTTP Llama a una URL (método, cabeceras, body). La respuesta acaba en msg.payload (el JSON se interpreta), el estado en msg.statusCode y las cabeceras en msg.headers. Las rutas como /api/v1/… van a este CMS. Opcional: error con estado 400 o superior
Collection Leer entradas (todas o una), crear, modificar, eliminar. Para crear y modificar, los campos vienen de msg.payload y el ID, por defecto, de {{ msg.payload.id }}. Resultado en msg.payload
IllusionDB Leer registros de IllusionDB (filtro, orden, agrupación, páginas), leer uno por clave primaria o por columna, crear, modificar, eliminar, consultar el esquema y las tablas. Resultado en msg.payload
Retrasar Espera el tiempo configurado (segundos, también fracciones) antes de que el mensaje continúe. Mientras tanto, las demás ramas siguen ejecutándose. Como máximo hasta el tiempo límite de la ejecución
Unir Reúne mensajes de varias ramas y pasa uno en cuanto han llegado todos; por defecto, tantos como cables entran. msg.payload se convierte en la lista de payloads (en orden de llegada) o en un objeto con msg.topic como clave. Si no llega todo, no sigue nada (aviso en Debug)
Correo Envía un correo electrónico mediante el acceso SMTP de la instancia: Para (varios separados por comas), asunto, texto (vacío = msg.payload), Responder a, opcionalmente como HTML; en todos los campos se puede usar {{ msg.… }}. El mensaje continúa sin cambios
Respuesta HTTP Responde a la entrada HTTP: msg.payload como body (objeto → JSON, texto → text/plain), con estado y cabeceras configurables
Debug Muestra msg.payload, el msg completo o una ruta en el panel de depuración
Propios Tus Nodes propios: scripts reutilizables con parámetros y salidas con nombre

Función con varias salidas

El código de la función recibe msg y decide con return por dónde sigue:

// Ajuste: 3 salidas
if (msg.payload.preis > 5) return [msg, null, null]   // solo salida 1
if (msg.payload.preis > 2) return [null, msg, null]   // solo salida 2
return [null, null, msg]                               // solo salida 3
  • return msg: sigue por la salida 1
  • return null: esta rama termina aquí
  • return [a, null, b]: un mensaje o null por salida
  • return [[a, b, c]]: varios mensajes por la misma salida (p. ej. uno por entrada)

Así reflejas mucha lógica en un solo node, en lugar de encadenar muchos desvíos. En el código también tienes disponibles:

Nombre Significado
node.warn(…), console.log(…) Salida al panel de depuración
store.get(key, standard), store.set(key, wert), store.delete(key), store.keys() Almacén del Workflow: se conserva entre ejecuciones
fetch(url, { method, headers, body }) Petición HTTP, síncrona (sin await): respuesta con status, ok, headers, text(), json()
ic.items(slug), ic.item(slug, id) Entradas de una Collection (también borradores: campo published)
ic.createItem(slug, data, { published }) Crear una entrada; los valores se validan contra el esquema como en la API
ic.updateItem(slug, id, data), ic.deleteItem(slug, id) Modificar (solo los campos pasados) o eliminar
ic.data(slug, params) Como data() en el template: Collections y fuentes REST
ic.api(method, path, body) Cualquier endpoint de la API REST
idb.records(…), idb.find(…), idb.create(…) … Leer y escribir registros en IllusionDB

El motor de JavaScript (goja) conoce ES5.1 y las novedades habituales posteriores (const/let, funciones flecha, plantillas de texto, desestructuración, métodos de Array), pero no Intl ni await. Los números y las fechas los formateas a mano, por ejemplo con betrag.toFixed(2).replace('.', ',') o con una expresión regular para los separadores de miles.

Si falla una llamada ic.* o idb.* (p. ej. falta un campo obligatorio), lanza un error que puedes capturar con try { … } catch (e) { … }. fetch() solo lanza errores en fallos de red; el estado lo compruebas con ok.

Ejemplos

Carta de bebidas para la pantalla de un bar: Entrada HTTP (GET, ruta karte, pública) → Collection (leer, todas, solo publicadas) → Función → Respuesta HTTP:

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 devuelve entonces JSON, también mediante fetch() desde otro dominio. Con un Desvío sobre msg.req.query.format puedes, por ejemplo, responder con ?format=text mediante una segunda rama con texto plano.

Formulario de contacto: Entrada HTTP (POST, ruta kontakt, pública) → Función con 2 salidas → salida 1: Collection (crear en «anfragen», borrador) → Función «Redirigir» → Respuesta HTTP; salida 2 directamente a «Redirigir»:

<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"> <!-- Trampa para bots -->
  <button>Enviar</button>
</form>
// Comprobar: los bots rellenan el campo oculto
if (msg.payload.website) return [null, msg]
msg.payload = { name: msg.payload.name, nachricht: msg.payload.nachricht }
return [msg, null]
// «Redirigir» (antes de la respuesta HTTP; allí, poner el estado 303)
msg.payload = ''
return msg

En la Respuesta HTTP, pon además la cabecera Location: /danke. En la Collection «anfragen», marca Datos en vivo en el esquema; si no, el siguiente Bundle desde local sobrescribe las solicitudes recibidas.

Sincronización nocturna: Inicio (programación 0 3 * * *) → Petición HTTP (https://kasse.example/api/artikel, cabecera Authorization: Bearer k123, error a partir de 400) → Función:

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 + ' nuevos, ' + geaendert + ' modificados')
return null

Llamar desde el template

Un flow con entrada HTTP (GET o ANY) se puede llamar directamente desde una página o un Snippet: internamente, sin red y sin token (también las entradas protegidas):

<ul>
{% for d in workflow("karte", "format", "kurz") %}
  <li>{{ d.name }} – {{ d.preis }}</li>
{% endfor %}
</ul>
  • El primer valor es la ruta de la entrada HTTP (karte para /api/v1/flows/karte; también funcionan marcadores como text/{wer}: workflow("text/Ada")).
  • Los demás valores llegan por pares como parámetros en msg.req.query y msg.payload; aquí, { "format": "kurz" }.
  • Se devuelve lo que enviaría el node Respuesta HTTP: el JSON como datos (listas, objetos) y, si no, texto. Sin Respuesta HTTP no se devuelve nada.
  • Los errores del flow aparecen como otros errores de template. Tiempo límite aquí: 10 segundos; los servicios externos lentos hacen lenta la página.
  • La caché de páginas guarda las páginas con workflow() como máximo un minuto, para que los datos externos se mantengan frescos. En la vista previa del editor, el resultado se recuerda 30 segundos, para que el flow no se ejecute con cada pulsación de tecla.
  • En el registro del Workflow, estas ejecuciones aparecen con el disparador Página.

Varios parámetros, p. ej. de la URL: simplemente añade más pares: workflow("pfad", "a", wert1, "b", wert2, …). Los parámetros de la página solicitada están en el template en route.query. Ejemplo: una carta de bebidas con búsqueda y precio máximo en /karte?suche=mate&max=4.

Página /karte:

<form>
  <input name="suche" value="{{ route.query.suche }}" placeholder="Nombre">
  <input name="max" type="number" step="0.5" value="{{ route.query.max }}" placeholder="precio máx.">
  <button>Filtrar</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>No se encontró nada.</li>
{% endfor %}
</ul>

Workflow: Entrada HTTP (GET, ruta karte-filter; pública no es necesario para la llamada desde el template) → Collection (leer, todas, solo publicadas) → Función → Respuesta HTTP. El node Collection sobrescribe msg.payload con las entradas; por eso la función lee los parámetros de msg.req.query:

// Los valores de la URL son texto (o faltan): compruébalos antes de usarlos
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
  • Si falta un parámetro en la URL, llega vacío; en ese caso, la carta lo muestra todo.
  • Los valores de la URL son entradas de los visitantes: compruébalos en el Workflow antes de que pasen, por ejemplo, a una petición HTTP a otro servicio. En el template, las salidas se escapan automáticamente.
  • La caché de páginas distingue por la dirección completa, parámetros incluidos: cada combinación pasa por el Workflow como máximo una vez por minuto.
  • Con parámetros repetidos (?a=1&a=2), route.query contiene solo el primer valor.

Programación

La programación del node de inicio es una expresión cron de cinco campos: minuto hora día mes día de la semana (0 = domingo). Hay plantillas; arriba, junto a Activo, aparece la próxima ejecución.

Expresión Significado
*/15 * * * * cada 15 minutos
0 * * * * cada hora en punto
0 3 * * * cada día a las 3:00
30 7 * * 1-5 días laborables a las 7:30
0 6 1 * * el día 1 de cada mes a las 6:00

Se aplica la zona horaria del servidor (en la imagen Docker, Europe/Berlin); otra distinta se indica con un prefijo: CRON_TZ=Europe/Berlin 0 3 * * *. Si un node de inicio todavía se está ejecutando en la siguiente fecha, esta se omite; las fechas en las que el servidor estaba apagado no se recuperan.

Entradas HTTP

  • Ruta tras /api/v1/flows/, con marcadores: drinks/{id} coincide con /api/v1/flows/drinks/5 (msg.req.params.id es "5"), y dateien/{rest...} al final recoge el resto. Una ruta con el mismo método pertenece exactamente a un Workflow activo.
  • Protegida (predeterminado): solo con token de API (Authorization: Bearer … o, en GET, ?token=…) o como administrador con sesión iniciada. Pública: sin iniciar sesión, también mediante JavaScript desde otros dominios (CORS).
  • La respuesta la da el primer node Respuesta HTTP alcanzado. Si el flow termina sin ninguno, el servidor responde con 204; si hay un error, con 500 y el mensaje.
  • Desde páginas y Snippets llamas a una entrada HTTP con workflow().
  • En el editor pruebas una entrada HTTP mediante Ejecución de prueba en sus ajustes, con un payload elegido por ti, sin una petición real.

Eventos de collection

Los eventos se ejecutan en segundo plano después de guardar: el guardado en el admin o por API no los espera. Se activan con cualquier cambio realizado mediante el admin, la API o Workflows. Si un Workflow modifica a su vez entradas que vuelven a disparar eventos, la cadena se interrumpe tras tres niveles.

Almacén y correo

Almacén: cada Workflow tiene un pequeño almacén clave-valor que se conserva entre ejecuciones: para contadores, «lo último procesado» o un resultado intermedio. Los valores son cualquier valor JSON (hasta 64 KB por valor, hasta 1.000 claves). La pestaña Almacén de la barra lateral muestra el contenido; allí eliminas claves individuales o todo. El almacén pertenece a la instancia: no viaja con los Bundles, así que el estado del servidor en producción se conserva al subir. Al eliminar el Workflow se elimina también su almacén.

Ejemplo: una programación que solo notifica las solicitudes llegadas desde la última ejecución (Inicio con programación → Collection «leer (todas)» → Función → Correo):

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 + " solicitud(es) nueva(s): " + neu.map((e) => e.data.name).join(", ")
return msg

Envío de correo: el acceso SMTP lo introduces en Ajustes → Sistema → Envío de correo (servidor, puerto, usuario, contraseña, remitente como Website <web@firma.de>) y lo compruebas con Enviar correo de prueba. El puerto 465 conecta por TLS y los demás puertos por STARTTLS; sin cifrado no se envía ninguna contraseña. El acceso solo vale para esta instancia: en local y en producción lo configuras por separado (en modo remoto, directamente para el servidor).

IllusionDB

IllusionDB gestiona bases de datos existentes (PostgreSQL, MySQL/MariaDB, SQLite) con interfaz y API REST. El node IllusionDB obtiene los registros directamente de allí (productos, equipos, citas, páginas de detalle) sin copiarlos a una Collection.

Configurar la conexión: Ajustes → IllusionDB: nombre, dirección (p. ej. https://db.firma.de) y token de API (en IllusionDB, en Ajustes → Mi cuenta; para leer basta con permiso de lectura en las tablas). Probar conexión muestra la versión y cuántas tablas son legibles. Sin token solo son accesibles las tablas que en IllusionDB son legibles públicamente.

  • Los Workflows hacen referencia a la conexión por su nombre, y a las tablas por el suyo (si hay tablas con el mismo nombre en varios orígenes de datos, Datenquelle/tabelle). Crea en local y en producción una conexión con el mismo nombre en cada caso: el mismo Workflow usará entonces la base de datos correspondiente.
  • Las conexiones pertenecen a la instancia, no al Bundle; el token nunca se entrega. En el modo en vivo editas las del servidor.

El node: aparece en la paleta solo cuando hay al menos una conexión configurada. La conexión, la tabla, las columnas y las vistas guardadas se ofrecen como selección en vivo desde el servidor de IllusionDB (↻ vuelve a cargar el esquema, p. ej. tras añadir columnas).

Acción Resultado en msg.payload
Leer registros Lista, con filtro, orden, agrupación, resumen, cantidad y página, opcionalmente partiendo de una vista guardada
Leer uno un registro por la clave primaria o por una columna (p. ej. slug); si no se encuentra: null o, si quieres, un error
Crear / modificar / eliminar columnas de msg.payload, clave primaria por defecto {{ msg.payload.id }}; resultado: el registro guardado
Esquema la tabla con todas sus columnas (nombre, tipo, clave primaria, …)
Listar tablas todas las tablas legibles de todos los orígenes de datos
  • Filtro: reglas columna – comparación – valor (=, ≠, >, ≥, <, ≤, contiene, uno de a, b, c, ninguno de, está vacío, no está vacío), combinadas con todas las reglas o al menos una. Los valores pueden contener {{ msg.… }}; los números y los sí/no se convierten según el tipo de columna. Las reglas cuyo valor queda vacío se omiten: un campo de búsqueda vacío, por tanto, no filtra (se puede desactivar).
  • Filtro como JSON sustituye a las reglas, para condiciones Y/O anidadas en el formato de la API de IllusionDB: {"or": [{"col": "name", "op": "like", "val": "{{ msg.payload.q }}"}, {"col": "preis", "op": "lt", "val": {{ msg.payload.max }}}]}. Un marcador entre comillas se inserta como texto; sin comillas, como valor JSON (número, true, lista).
  • Solo estas columnas (al leer listas y registros individuales): p. ej. slug, name, preis. IllusionDB envía solo esas columnas y se salta el cálculo de los campos de fórmula, lookup y vínculo no necesarios; en tablas anchas o grandes ahorra volumen de datos y tiempo. Un nombre de columna desconocido es un error. Vacío = todas (las versiones antiguas de IllusionDB siempre devuelven todas).
  • Páginas: Cantidad (como máximo 1000) y Página (p. ej. {{ msg.payload.seite }}), o cargar todas las páginas (hasta 10 000 registros).
  • Salida: la lista de registros; todo = records, total, page, pages, has_more, summary, groups; solo los grupos; o el primer registro.
  • Grupos: por cada grupo, value, count (sobre todos los resultados, no solo la página cargada) y records; con varias columnas de agrupación, en su lugar groups un nivel más abajo.
  • Resumen: SUM, AVG, COUNT, MIN, MAX sobre todos los resultados, en summary.<spalte>. SUM y AVG solo los calcula IllusionDB para columnas de tipo número o número decimal.
  • Recordar resultado: durante tantos segundos se guarda en caché un resultado de lectura; alivia la base de datos con mucho tráfico (0 = preguntar cada vez). El CMS recuerda las tablas y columnas durante un minuto.
  • Los nombres de columna son los que IllusionDB usa en su API (el nombre visible, si está definido).

Lista y páginas de detalle directamente desde la base de datos: como el blog del capítulo 10, pero con IllusionDB en lugar de una Collection. Dos Workflows:

  • Entrada HTTP (GET, ruta produkte) → IllusionDB (leer registros, tabla produkte, regla aktiv = true, orden name, agrupación kategorie, cantidad 20, página {{ msg.payload.seite }}, salida todo) → Respuesta HTTP
  • Entrada HTTP (GET, ruta produkt) → IllusionDB (leer uno, tabla produkte, buscar por la columna slug, valor {{ msg.payload.slug }}) → Respuesta HTTP

Además, una página /produkte con Catch-All y Desactivar caché:

{% 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">← Todos los productos</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>Página {{ r.page }} de {{ r.pages }} · {{ r.total }} productos</p>
  {% if r.has_more %}<a href="?seite={{ r.page + 1 }}">siguiente</a>{% endif %}
{% endif %}
  • /produkte/mate muestra el registro con slug = mate, y un slug desconocido, la página 404 (leer uno devuelve entonces null).
  • Con Desactivar caché, cada cambio en IllusionDB es visible de inmediato; sin ello, la caché de páginas guarda la página como máximo un minuto. Con mucho tráfico: deja la caché activada y configura Recordar resultado en el node.
  • Sitemap: en los ajustes de la página, en Páginas de detalle para el sitemap y la exportación estática, elige la fuente Tabla de IllusionDB (conexión shop, tabla produkte, columna slug, regla aktiv = true); consulta el capítulo 10. Con Comprobar ves al instante cuántas páginas resultan.

Miles de landing pages: el patrón soporta también grandes cantidades, por ejemplo una tabla landingpages con las columnas slug (berlin/zahnarzt), stadt, leistung, text, aktiv, geaendert, mantenida en IllusionDB (o importada allí por CSV). Una página Catch-All /leistungen con un template, un Workflow leer uno por slug, la fuente del sitemap apuntando a la misma tabla, y ya están listas todas las páginas con su sitemap. Las filas nuevas en IllusionDB están en línea de inmediato (con Desactivar caché) y, como mucho al cabo de 10 minutos, en el sitemap.

En el node Function está disponible idb, para todo lo que no se puede montar con el node:

const r = idb.records('shop', 'produkte', {
  where: { and: [{ col: 'aktiv', op: 'eq', val: true }, { col: 'preis', op: 'lt', val: 5 }] },
  sort: '-preis', // varios: '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 }) devuelve lo mismo que la salida todo; además están idb.record(v, t, pk) (o null), idb.find(v, t, spalte, wert) (primer resultado o null), idb.create(v, t, daten), idb.update(v, t, pk, daten), idb.delete(v, t, pk), idb.schema(v, t), idb.tables(v) e idb.api(v, methode, pfad, body) para cualquier otro endpoint de la API de IllusionDB (ruta sin /api/v1). Los errores de IllusionDB interrumpen la ejecución con el mensaje.

Nodes propios

Si escribes el mismo script una y otra vez (formatear precios, acortar texto, una notificación a un servicio), conviértelo en un node propio: se escribe una vez y se puede arrastrar desde la paleta (grupo Propios) en cualquier Workflow, con ajustes para cada uso y salidas con nombre. Si cambia el script, el cambio se aplica en todos los Workflows.

Se crean y editan en Workflows → Nodes propios (arriba a la derecha):

  • Nombre, descripción, color: así aparece el node en la paleta y en el lienzo; la descripción es su texto de ayuda.
  • Entrada: con entrada procesa mensajes como un node Function. Sin entrada es un node de inicio, con ▶ y programación opcional como Inicio, p. ej. «Obtener feed».
  • Salidas: de 0 a 10, cada una con un nombre opcional («ok», «error») que aparece en el node.
  • Parámetros: campos que se rellenan en cada uso dentro del Workflow: Texto, Texto, varias líneas, Número, Sí/No o Selección (con opciones), cada uno con valor predeterminado y texto de ayuda. En el código están en config.<name>; en los textos, {{ msg.… }} se sustituye antes.
  • Código: JavaScript como en el node Function, con msg y config; devuelve msg, [a, null, b] por salida o null. Todo lo demás (fetch, store, ic.*, idb.*, node.warn) también está disponible.
  • Probar: introduce los parámetros y un mensaje de ejemplo; Ejecutar muestra lo que sale por cada salida, también sin guardar y sin entrada en las ejecuciones.

Ejemplo Formatear precio: parámetros waehrung (selección €, $, predeterminado €) y stellen (número, predeterminado 2), salidas «ok» y «sin precio»:

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]
  • Los Workflows hacen referencia al node por su clave (derivada del nombre al crearlo, p. ej. preis-formatieren), así que renombrarlo no supone ningún riesgo.
  • Un node solo se puede eliminar cuando ningún Workflow lo usa; Historial muestra versiones anteriores del código.
  • Los Nodes propios forman parte del contenido: van en el Bundle y, con él, al servidor en producción al subirlo.

Límites y seguridad

  • Tiempos límite: 30 segundos para flows con entrada HTTP y 5 minutos en los demás casos; como máximo 1.000 ejecuciones de Nodes por ejecución (protección contra bucles en el grafo). Si un flow se llama a sí mismo a través de su propio hook, se detiene tras tres niveles.
  • Los Workflows se ejecutan con permisos completos: pueden modificar todo el contenido y, mediante peticiones HTTP, alcanzar también direcciones de la red interna. Quien edita Workflows necesita la misma confianza que con el HTML y los templates.
  • La contraseña SMTP está en la base de datos de la instancia (no en los Bundles, y en el admin nunca se vuelve a mostrar).
  • Las credenciales están en el Node o en el script, y por tanto también en los Bundles y en el historial de versiones. No entregues por ello Bundles ni copias de seguridad. Las variables de entorno del servidor no están disponibles en los Workflows a propósito.
  • Las entradas públicas son accesibles para cualquiera: comprueba las entradas y no devuelvas datos internos.
  • Las entradas de Collection no tienen historial de versiones. Si vas a modificar muchas entradas mediante un Workflow, descarga antes un Bundle como copia de seguridad.