Introducción
Hasta ahora hemos visto cómo instalar cómo instalar Trilium, sincronizarlo entre dispositivos, organizar el árbol con clones y workspaces y usar plantillas y metadatos para que cada nota nazca con una estructura. El siguiente paso es mover y actualizar tus notas de manera automática en lugar de hacerlo a mano.
En este artículo te voy a enseñar dos automatizaciones que uso a diario:
- Mi sistema de tareas en el workspace Personal, donde un script “vigila” los cambios en los atributos de las notas ( attributeChanged) y se encarga de moverlas entre carpetas, sincronizarlas con el diario y generar tareas recurrentes.
- Un conjunto de scripts en el workspace Homelab que generan informes HTML con el estado de los contenedores, IPs y puertos utilizados.
La idea no es que copies mi sistema tal cual, sino que veas hasta dónde puedes llegar combinando atributos, plantillas y scripts.
Mi sistema de tareas en el workspace Personal
En mi workspace Personal tengo un árbol de tareas bastante clásico: una rama `TODO` con subcarpetas para tareas sin fecha, planificadas, pendientes y hechas, y, por otro lado, mi diario personal organizado por año/mes/día.

Cada tarea es una nota normal con varios atributos, entre ellos:
- todoDate: fecha planificada.
- doneDate: fecha en la que termino la tarea.
- repetir: número de días para que la tarea se vuelva a repetir (tareas recurrentes).
- cssClass: para pintar la tarea de un color u otro en el árbol.
- Otros como area, tipo, tag, etc., para una mejor clasificación.

La gracia es que yo ya no muevo esas notas a mano. Cuando cambio alguno de esos atributos, un script se dispara y decide qué hacer con la tarea.
El script attributeChanged
Se ejecuta automáticamente cada vez que cambio un atributo en una nota y se encarga de mover, clonar y generar tareas según su estado y fecha.
El script solo actúa si:
- El cambio se hace sobre los atributos
todoDate(fecha planificada),doneDate(fecha de finalización) orepetir(intervalo de frecuencia). - La tarea pertenece al Workspace Personal (
#workspaceRoot=personal).
Mover las tareas por el árbol TODO / Pendientes / Done
Lo primero que hace el script después de las comprobaciones es decidir en qué carpeta debería estar la tarea.
A grandes rasgos, sigue estas reglas:
- Si la tarea tiene
doneDate(es decir, está terminada):- Se asegura de que esté dentro de la carpeta
Done. - La saca de la carpeta
Pendientes. - La quita de cualquier subcarpeta de
TODOdonde pudiera seguir como “abierta”.
- Se asegura de que esté dentro de la carpeta
- Si la tarea no está terminada pero tiene
**todoDate**:- Si la fecha ya ha pasado o está dentro de la semana actual, la coloca (o mantiene) en
Pendientes. - Si la fecha es de semanas futuras, la saca de
Pendientes: sigue planificada, pero no debe aparecer aún como “urgente”.
- Si la fecha ya ha pasado o está dentro de la semana actual, la coloca (o mantiene) en
- Si la tarea no está terminada y no tiene fecha:
- Se asegura de que no esté en
Done. - También la saca de
Pendientespara que no se mezcle con las urgentes.
- Se asegura de que no esté en
La clave es que tú solo te preocupas de poner o quitar doneDate y todoDate. El resto (mover notas entre carpetas) lo hace Trilium por ti.

Sincronizar tareas con el diario personal
La segunda parte del script se encarga de sincronizar las tareas con el diario, para que cada día tengas una vista de lo que hay que hacer.
El proceso es este:
- El script localiza la raíz del calendario en el workspace personal usando una nota marcada con
#workspaceCalendarRoot=personal. - A partir de una fecha (
todoDateodoneDate), genera (si hace falta) la estructura año → mes → día dentro del diario. - Dentro del día correspondiente crea o mueve clones de la tarea original:
- Si la tarea tiene
todoDatey no está hecha:- Crea un clon en la nota del día planificado.
- Le pone el prefijo
TODO -al título del clon.
- Si la tarea pasa a hecha (
doneDateexiste):- Mueve el clon al día en que se terminó.
- Cambia el prefijo a
DONE -.
- Si la tarea tiene
- Si cambias la fecha planificada, el script borra los clones antiguos y los vuelve a crear en la nueva fecha para que no se acumule basura en el calendario.

Esto hace que puedas revisar tu día desde el diario y ver una lista de tareas TODO y DONE de un vistazo.
Tareas recurrentes con el atributo repetir
El tercer bloque del script maneja las tareas recurrentes usando el atributo repetir.
La idea es sencilla:
- Si una tarea tiene
repetir=7y se marca como hecha (doneDate):- El script calcula la próxima fecha sumando esos 7 días a la fecha de finalización.
- Crea una nueva tarea con el mismo título (limpiando el prefijo
TODO/DONE), en la carpeta original de la tarea (por ejemplo,Ciclismo,Casa, etc.). - Le aplica la plantilla de tareas personales (buscando una nota con
#workspaceTemplate #contexto=personal). - Copia atributos importantes (
area,tipo,repetir). - Le pone un nuevo
todoDatecon esa próxima fecha calculada.

Tú marcas la tarea como hecha, y Trilium se encarga de “crear la siguiente” para dentro de X días. Perfecto para cosas tipo mantenimiento del coche, limpieza recurrente, copias de seguridad, etc.
Colorear tareas con cssClass
El script también cambia el atributo cssClass según si la tarea está hecha o no:
- Si está terminada (
doneDateexiste), ponecssClass=done. - Si no está terminada,
cssClass=todo.
Con eso puedes hacer un CSS para que las tareas se vean de diferente color. Por ejemplo:
span.fancytree-node.todo .fancytree-title {
color: red !important;
}
span.fancytree-node.done .fancytree-title {
color: green !important;
}
De esta forma , tus tareas abiertas son de un color rojo y las terminadas en verde.
Tablero Personal
En el día a día me interesa más ver un tablero que una carpeta con listas.
Para eso uso una nota de tipo búsqueda a la que llamo Tablero Personal. Esta nota no guarda tareas fijas, sino que es el resultado de una consulta que muestra todas las notas que encajan con ciertos criterios.
La cadena de búsqueda que uso en los parámetros de la nota es esta:
#contexto="personal" (note.parents.title="Pendientes" OR note.parents.title="Sin fecha")

Con esto consigo una vista de mis tareas activas sin importar en qué carpeta estén dentro de mi rama personal. Cada vez que cambio fechas o estados el tablero se actualiza solo.
El script completo
Así quedaría el script completo:
if (!["todoDate", "doneDate", "repetir"].includes(api.originEntity.name)) {
return;
}
const note = api.originEntity.getNote();
if (note.isDeleted) return;
const ancestors = note.getAncestors();
const workspaceNote = ancestors.find(n => n.hasLabel('workspaceRoot'));
if (!workspaceNote) return;
const workspaceValue = workspaceNote.getLabelValue('workspaceRoot');
if (workspaceValue !== 'personal') {
return;
}
const flagAttr = '_scriptRunning';
if (note.hasLabel(flagAttr)) return;
note.addLabel(flagAttr);
try {
const doneRootNote = api.searchForNote(`#taskDoneRoot=${workspaceValue}`);
const todoRootNote = api.searchForNote(`#taskTodoRoot=${workspaceValue}`);
const pendientesNote = api.searchForNote(`#taskPendientesRoot=${workspaceValue}`) || todoRootNote;
if (!doneRootNote || !todoRootNote) {
api.log(`Falta taskDoneRoot o taskTodoRoot para workspace: ${workspaceValue}`);
return;
}
const todoDate = note.getLabelValue('todoDate');
const doneDate = note.getLabelValue('doneDate');
function isWithinExpectedRange(date) {
if (!date) return true;
const year = parseInt(date.substr(0, 4));
return year >= 2010 && year < 2050;
}
if (!isWithinExpectedRange(todoDate) || !isWithinExpectedRange(doneDate)) {
api.log(`Fechas fuera de rango: ${todoDate}, ${doneDate}`);
return;
}
const isTaskDone = !!doneDate;
// -------------------- Helpers --------------------
const calRoot = api.searchForNote(`#workspaceCalendarRoot=${workspaceValue}`);
function isCalendarParent(p) {
if (!p.hasLabel("dateNote")) return false;
return calRoot && p.getAncestors().some(a => a.noteId === calRoot.noteId);
}
function isInsideTodoTree(parentNote) {
if (parentNote.noteId === todoRootNote.noteId) return true;
return parentNote.getAncestors().some(a => a.noteId === todoRootNote.noteId);
}
const realParents = note.getParentNotes().filter(p => !isCalendarParent(p));
const originalParent = realParents.find(p => {
if (p.noteId === doneRootNote.noteId) return false;
if (p.noteId === pendientesNote.noteId) return false;
return isInsideTodoTree(p);
}) || realParents.find(p => {
if (p.noteId === doneRootNote.noteId) return false;
return !isCalendarParent(p);
}) || todoRootNote;
function isDatePending(dateStr) {
if (!dateStr) return false;
const taskDate = new Date(`${dateStr}T12:00:00`);
const today = new Date();
today.setHours(12, 0, 0, 0);
if (taskDate < today) return true;
const currentDayOfWeek = today.getDay();
const daysUntilSunday = currentDayOfWeek === 0 ? 0 : 7 - currentDayOfWeek;
const endOfWeek = new Date(today);
endOfWeek.setDate(today.getDate() + daysUntilSunday);
return taskDate <= endOfWeek;
}
// -------------------- DONE/TODO/PENDIENTES --------------------
if (isTaskDone) {
api.toggleNoteInParent(true, note.noteId, doneRootNote.noteId);
api.toggleNoteInParent(false, note.noteId, pendientesNote.noteId);
for (const parent of realParents) {
if (isInsideTodoTree(parent)) {
api.toggleNoteInParent(false, note.noteId, parent.noteId);
}
}
} else {
api.toggleNoteInParent(false, note.noteId, doneRootNote.noteId);
if (todoDate && isDatePending(todoDate)) {
api.toggleNoteInParent(true, note.noteId, pendientesNote.noteId);
} else {
api.toggleNoteInParent(false, note.noteId, pendientesNote.noteId);
}
}
note.toggleLabel(isTaskDone, "cssClass", "done");
note.toggleLabel(!isTaskDone, "cssClass", "todo");
// -------------------- Diario --------------------
function getDayNoteInWorkspace(dateStr) {
if (!dateStr) return null;
const MESES = ["Enero","Febrero","Marzo","Abril","Mayo","Junio",
"Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
const DIAS = ["Domingo","Lunes","Martes","Miércoles","Jueves","Viernes","Sábado"];
const [year, monthNum, dayNum] = dateStr.split('-');
const date = new Date(`${dateStr}T12:00:00`);
const monthIndex = parseInt(monthNum, 10) - 1;
const month = `${monthNum} - ${MESES[monthIndex]}`;
const dayTitle = `${dayNum} - ${DIAS[date.getDay()]}`;
if (!calRoot) return null;
function getOrCreate(parentNote, title, labelName, labelValue) {
let child = parentNote.getChildNotes().find(n => n.title === title);
if (!child) {
child = api.createTextNote(parentNote.noteId, title, "").note;
child.addLabel(labelName, labelValue);
child.addLabel("sorted");
}
return child;
}
function getOrCreateDay(parentNote, title, labelValue) {
let child = parentNote.getChildNotes().find(n => n.title === title);
if (!child) {
child = api.createTextNote(parentNote.noteId, title, "").note;
child.addLabel("dateNote", labelValue);
const template = api.getNoteWithLabel("dayNoteTemplate");
if (template) child.addRelation("template", template.noteId);
}
return child;
}
const yearNote = getOrCreate(calRoot, year, "yearNote", year);
const monthNote = getOrCreate(yearNote, month, "monthNote", `${year}-${monthNum}`);
const dayNote = getOrCreateDay(monthNote, dayTitle, `${year}-${monthNum}-${dayNum}`);
return dayNote.noteId;
}
if (calRoot) {
for (const parent of note.getParentNotes()) {
if (isCalendarParent(parent)) {
api.toggleNoteInParent(false, note.noteId, parent.noteId);
}
}
}
if (isTaskDone && doneDate) {
const doneDayId = getDayNoteInWorkspace(doneDate);
if (doneDayId) {
const cleanTitle = note.title.replace(/^(TODO|DONE) - /, "");
if (note.title !== cleanTitle) { note.title = cleanTitle; note.save(); }
api.toggleNoteInParent(true, note.noteId, doneDayId);
const branch = note.getBranches().find(b => b.parentNoteId === doneDayId);
if (branch) { branch.prefix = "DONE"; branch.save(); }
}
} else if (!isTaskDone && todoDate) {
const todoDayId = getDayNoteInWorkspace(todoDate);
if (todoDayId) {
const cleanTitle = note.title.replace(/^(TODO|DONE) - /, "");
if (note.title !== cleanTitle) { note.title = cleanTitle; note.save(); }
api.toggleNoteInParent(true, note.noteId, todoDayId);
const branch = note.getBranches().find(b => b.parentNoteId === todoDayId);
if (branch) { branch.prefix = "TODO"; branch.save(); }
}
} else {
const cleanTitle = note.title.replace(/^(TODO|DONE) - /, "");
if (note.title !== cleanTitle) { note.title = cleanTitle; note.save(); }
}
// -------------------- Recurrentes --------------------
const repetir = note.getLabelValue("repetir");
if (isTaskDone && repetir) {
const dias = parseInt(repetir);
if (isNaN(dias) || dias <= 0) {
api.log(`Valor de "repetir" no válido: ${repetir}`);
} else {
const baseDate = new Date(`${doneDate}T12:00:00`);
baseDate.setDate(baseDate.getDate() + dias);
const pad = n => String(n).padStart(2, "0");
const nextDateStr = `${baseDate.getFullYear()}-${pad(baseDate.getMonth() + 1)}-${pad(baseDate.getDate())}`;
const cleanTitle = note.title.replace(/^(TODO|DONE) - /, "");
const newTask = api.createTextNote(originalParent.noteId, cleanTitle, "").note;
const plantilla = api.searchForNote(`#workspaceTemplate #contexto=personal`);
if (plantilla) newTask.addRelation("template", plantilla.noteId);
newTask.addLabel("repetir", String(dias));
const area = note.getLabelValue("area");
if (area) newTask.addLabel("area", area);
const tipo = note.getLabelValue("tipo");
if (tipo) newTask.addLabel("tipo", tipo);
newTask.addLabel("todoDate", nextDateStr);
api.log(`Tarea repetida: "${cleanTitle}" → próxima en ${dias} días: ${nextDateStr}`);
}
}
} finally {
note.removeLabel(flagAttr);
}
Scripts de Homelab: inventario y estados de contenedores
En mi workspace Homelab tengo documentado el servidor, las VLAN, los contenedores y los runbooks. Cada contenedor es una nota con atributos como tipo=contenedor, estado=activo/pausado, iplocal o puertoInt.

Para no tener que revisar nota por nota qué puertos tengo ocupados, tengo un script que recorre todos los contenedores y me genera un mini-dashboard en HTML.

La lógica del script es:
- Localiza la carpeta
Inventariodentro deHomelab. - Busca (o crea) una nota de reporte.
- Busca todas las notas que tengan
#tipo=contenedory#estado=activoopausado. - Mapea estos datos, los ordena por IP y por puerto, y construye una tabla HTML.
- Guarda esa tabla dentro de la nota de reporte.
A partir de ahí, solo tengo que abrir esa nota para ver un listado siempre actualizado con:
- El nombre del contenedor (con enlace a su nota).
- Un icono de estado (🟢 o 🔴).
- Su IP local.
- El puerto interno usado.

Tengo otros dos scripts casi idénticos que filtran los contenedores los activos o los pausados.
Script completo
Al igual que antes, te dejo el script completo:
// Script Muestra activos y pausados (incluso sin puerto asignado)
// 1. Busca la carpeta Inventario
const inventarioNote = api.searchForNote('note.title = "Inventario" note.ancestors.title = "Homelab"');
if (!inventarioNote) {
throw new Error('No encuentro la carpeta Inventario dentro de Homelab.');
}
// 2. Busca/crea nota de reporte
let reportNote = api.searchForNote('#reportePuertos=true');
if (!reportNote) {
const created = api.createTextNote(inventarioNote.noteId, 'Puertos internos usados', '');
reportNote = created.note;
reportNote.setLabel('reportePuertos', 'true');
}
// 3. Busca activos Y pausados
const activos = api.searchForNotes('#tipo=contenedor #estado=activo');
const pausados = api.searchForNotes('#tipo=contenedor #estado=pausado');
const todos = [...activos, ...pausados];
// 4. Procesa datos (maneja puertoInt=null)
const rows = todos
.map(n => {
let pInt = n.getLabelValue('puertoInt');
const ip = n.getLabelValue('iplocal') || '-';
const estado = n.getLabelValue('estado') || 'desconocido';
// Convierte null/"null" a cadena vacía, pero ahora lo permitiremos
if (pInt === null || pInt === 'null' || !pInt) pInt = '';
return {
noteId: n.noteId,
title: n.title,
iplocal: ip,
estado: estado,
puertoInt: pInt
};
})
// ELIMINADO: .filter(r => r.puertoInt !== '') -> Ahora mostramos TODOS, tengan puerto o no
.sort((a, b) => {
// 1. Primero ordenamos por IP
if (a.iplocal !== b.iplocal) {
return a.iplocal.localeCompare(b.iplocal, undefined, { numeric: true });
}
// 2. Manejo especial para ordenar puertos (si están vacíos, van al final o al principio)
const pA = a.puertoInt === '' ? Infinity : parseInt(a.puertoInt);
const pB = b.puertoInt === '' ? Infinity : parseInt(b.puertoInt);
return pA - pB;
});
// 5. Genera tabla HTML
let html = '<p>Reporte (Activos+pausados): <strong>' + new Date().toLocaleString('es-ES') + '</strong></p>';
html += '';
// 6. Guarda
if (reportNote.setContent) {
reportNote.setContent(html);
} else {
reportNote.content = html;
reportNote.save();
}
api.log('✅ Reporte: ' + rows.length + ' contenedores mostrados');
Conclusión
Te he mostrado que, una vez que tienes workspaces, plantillas y atributos, puedes ir una un paso lejos: automatizar tu forma de trabajar.
En mi caso en concreto:
- En el workspace personal, dejo que un script “vigile” los cambios en mis tareas y se encargue de moverlas, clonarlas en el diario y crear la siguiente repetición cuando toca.
- Y, en el homelab para convierto mis notas de contenedores en un pequeño dashboard con IPs, puertos y estados.
En el próximo artículo veremos como utilizo Trilium para escribir y publicar en el blog.
Espero que te haya gustado, pasa un buen día. 🐧
∞ Comentarios en Mastodon
Cargando respuestas…
→ Responder en Mastodon