Nota 01 · 2026-07-24Note 01 · 2026-07-24

El diseño del motor de decisión de CouncilHow Council's Decision Engine Is Designed

Por qué Council decide sin usar un LLM, y cómo un pipeline de scoring determinista puede ser tan personal como uno que aprende con redes neuronales. Why Council decides without an LLM, and how a deterministic scoring pipeline can feel as personal as one trained on your data.

El diseño del motor de decisión de Council

Council es el primer producto que sale de Witara: una lista de tareas, eventos y pagos que decide por ti cuál es lo más importante ahora mismo. No es un asistente que conversa. Es un consejero, en el sentido literal de la palabra que le da nombre a la empresa: te dice qué mirar primero y por qué.

Esta nota es sobre esa segunda parte: el por qué. Específicamente, sobre cómo construimos el motor que decide, y sobre la decisión, deliberada, de no ponerle un modelo de lenguaje adentro.

El problema

La mayoría del software de productividad resuelve la priorización de dos maneras, y ninguna nos convenció.

La primera es la caja negra: un modelo (clásico o un LLM) mira tus datos y te dice qué hacer. Cuando funciona, se siente mágico. Cuando falla, no hay manera de preguntarle por qué. No puedes auditar un peso aprendido dentro de una red neuronal de la misma forma en que puedes leer una función. Y si el modelo vive en la nube de un tercero, tu lista de tareas depende de la disponibilidad, la latencia y el costo de ese tercero.

La segunda salida es no decidir nada: una lista plana, ordenada por fecha de creación o por una prioridad que tú mismo asignaste y que nunca se actualiza sola. Es honesta, pero es dejarte solo con el problema que la herramienta debería ayudar a resolver.

Queríamos un tercer camino: que el software tenga criterio (que compare, pondere y ordene) y que ese criterio se pueda leer entero, línea por línea, como se lee el código de una librería. Que cuando Council te diga “esto va primero”, pueda mostrarte exactamente por qué, y que si no estás de acuerdo, puedas corregirlo y ver el efecto de esa corrección la próxima vez.

La restricción como diseño

La decisión de no usar un LLM en el camino de decisión no fue una limitación de presupuesto disfrazada de filosofía. Fue al revés: partimos de la premisa de que la industria confunde “más capacidad” con “más cómputo, más nube, más tokens”, y quisimos comprobar si esa premisa era falsa en un caso concreto.

Un LLM en el camino crítico de decidir qué tarea mostrarte primero introduce tres costos que preferimos no pagar. Costo económico, porque cada carga del dashboard dispararía una llamada. Costo de latencia, porque un modelo remoto es órdenes de magnitud más lento que una función pura. Y costo de opacidad, porque un modelo generativo no es determinista por diseño: el mismo input puede producir salidas distintas, lo que hace casi imposible escribir un test que diga “esta tarea debe puntuar más que esta otra, siempre”.

A cambio de resignar esa flexibilidad, ganamos tres cosas que sí nos importan. Determinismo: el mismo UserData produce siempre el mismo Decision[], sin excepción. Explicabilidad: cada score viene acompañado de al menos una razón (Reason) legible, generada por la misma regla que movió el número. Y testabilidad real: no la testabilidad de “probamos que no tira error”, sino mutation testing sobre el núcleo del motor con Stryker, que no mide si el código corre sino si los tests notarían si alguien cambiara una constante o invirtiera una comparación. Eso es verificable en el repositorio: no es una promesa de calidad, es un umbral que el CI hace cumplir.

La arquitectura del criterio

El motor de decisión recibe todas las tareas, eventos y pagos próximos del usuario, además de su perfil de aprendizaje, y devuelve una lista ordenada de Decision, cada una con un puntaje entre 1 y 20 y al menos una razón. El pipeline es una cadena de modificadores puros que se aplican en secuencia sobre un puntaje base.

Todo empieza con un puntaje base según el tipo de ítem: la prioridad que el usuario le puso a una tarea (1 a 10), o un valor fijo para eventos y pagos próximos, que por defecto compiten en un rango medio de esa escala.

Sobre ese base actúan, en orden, las siguientes etapas:

  • Proximidad de fecha límite, en dos capas. Una función escalonada da un salto de puntaje según el ítem esté vencido, sea hoy, mañana o esta semana, categorías que un humano reconoce de un vistazo. Encima de eso corre una curva de presión continua: en vez de que una tarea “salte” de golpe al cruzar el umbral de una semana, el puntaje empieza a subir suavemente varios días antes, así una tarea que vence en cuatro días ya se nota un poco más urgente que una que vence en diez, aunque las dos caigan en el mismo balde de la función escalonada.
  • Peso del contexto. Cada tarea pertenece a un contexto, un área de vida como “trabajo” o “salud”, y ese contexto tiene un peso aprendido que multiplica el puntaje entero. Un contexto que el usuario marcó como prioritario, o que el sistema aprendió que necesita atención, empuja hacia arriba todo lo que vive ahí.
  • Patrones conductuales. Si el usuario postergó tareas de este contexto de forma consistente, o si una tarea puntual ya fue postergada varias veces, el motor lo nota y ajusta el puntaje: a veces hacia abajo, para no insistir en algo que genuinamente no se está haciendo; a veces hacia arriba, cuando el patrón es completar cosas de ese contexto temprano en el día.
  • Aging boost. Una tarea que nadie tocó en semanas no debería quedar enterrada para siempre solo porque no tiene fecha límite. Pasado un período de gracia, el puntaje empieza a subir gradualmente cuanto más tiempo pasa sin actividad, hasta un tope.
  • Time-of-day matching. El trabajo que exige concentración puntúa mejor en las horas en que la mayoría de las personas tiene más foco; el trabajo liviano puntúa mejor hacia el final del día. Esto se evalúa en la zona horaria real del usuario, no en la del servidor, un detalle chico que importa mucho si tu usuario y tu infraestructura no comparten continente.
  • Boost de compromiso semanal. Si el usuario comprometió explícitamente una tarea para esta semana, esa tarea recibe un empujón fijo y se reordena por encima de ítems que puntúan parecido pero no fueron elegidos a propósito.

No vamos a publicar las constantes exactas: cuánto exactamente vale un “vencido hoy” frente a un “compromiso semanal”, o el ancho de la ventana de la curva de presión. Las ideas del pipeline se abren enteras; los números afinados, calibrados con datos reales de uso, son nuestro oficio. Es la misma distinción que hace cualquier receta seria: la técnica se enseña, la mano se gana.

Aprender sin ML

Council personaliza sin entrenar ningún modelo. Todo lo aprendido vive como pares clave-valor planos en un campo JSON del perfil del usuario. Nada de pesos de red neuronal, nada de embeddings. Cada contexto acumula números simples: qué tan seguido se completan sus tareas, cuánta actividad tuvo en los últimos días, cuántos días lleva sin movimiento, qué tan estable lo percibe el propio usuario.

Esos números se combinan con una fórmula legible para producir el peso final de un contexto: no una sola señal, sino varias corrigiéndose entre sí. Un contexto con baja estabilidad recibe un empujón, porque un área en crisis necesita más atención, no menos. Un contexto con bajo desempeño reciente también sube un poco, como ayuda a la recuperación en vez de castigo. Y si el usuario declaró ese contexto como su foco de la semana, el empujón es mayor todavía.

Cada acción del usuario (completar, posponer, descartar una recomendación, aceptarla) actualiza estos números en una dirección chica y predecible. Postergar una tarea de “trabajo” tres mañanas seguidas no entrena nada oculto: incrementa un contador de posposición y baja levemente un puntaje de desempeño, ambos visibles. Eso significa que el usuario puede pedirle a Council que le muestre su estado aprendido completo (cada peso, cada tasa, cada contador) y corregirlo directamente si el sistema entendió mal la situación. No hay una caja que abrir porque nunca hubo una caja cerrada.

Lo que no resuelve

Ser explicable no es lo mismo que ser completo, y preferimos decir esto en voz alta antes de que alguien lo descubra por su cuenta.

Council no predice. No sabe si vas a terminar una tarea a tiempo ni infiere intención a partir de texto libre. No entiende lenguaje natural en ningún punto del pipeline de decisión, solo estructura: fechas, contextos, prioridades numéricas, contadores. Si dos ítems terminan con el mismo puntaje exacto, el desempate es simplemente por fecha más próxima: una regla razonable, no una inteligencia extra. Y un usuario nuevo, sin historial, arranca con pesos neutros: el sistema todavía no tiene nada que aprender de él, así que por un tiempo Council decide con menos criterio propio del que tendrá después. El cold start es real y no lo disimulamos.

Tampoco resolvemos la pregunta de fondo de si postergar algo es siempre malo. El motor asume que sí, en general, y penaliza levemente la posposición repetida. Pero hay posposiciones sabias, y por ahora Council no distingue entre las dos. Es una de las cosas que seguimos investigando.

Coda

Lo que sigue es afinar curvas, no inventar más de ellas: entender mejor cómo debería decaer el momentum de un contexto entre ciclos, qué otras señales conductuales valen la pena capturar sin volverse invasivas, y cómo comunicar mejor la incertidumbre cuando el motor está, literal, empatando entre dos opciones igual de razonables.

Esta nota es en sí misma un ejercicio de consejo, no de control: publicamos el diseño para que se pueda cuestionar. Si algo de lo que describimos te parece un error de criterio y no solo un número que falta calibrar, escríbenos. Es exactamente el tipo de corrección que el sistema, y nosotros, estamos diseñados para recibir.

How Council’s Decision Engine Is Designed

Council is the first product to come out of Witara: a list of tasks, events, and upcoming payments that decides, on your behalf, what matters most right now. It isn’t a chatty assistant. It’s an advisor, in the literal sense the company’s name points to: it tells you what to look at first, and why.

This note is about the why. Specifically, about how we built the engine that decides, and about the deliberate choice not to put a language model inside it.

The problem

Most productivity software solves prioritization in one of two ways, and neither sat right with us.

The first is the black box: a model (classical or an LLM) looks at your data and tells you what to do. When it works, it feels like magic. When it doesn’t, there’s no way to ask it why. You can’t audit a learned weight buried inside a neural network the way you can read a function. And if the model lives in a third party’s cloud, your task list now depends on that third party’s uptime, latency, and pricing.

The second way out is to not decide at all: a flat list, sorted by creation date or by a priority you set once and that never updates itself. It’s honest, but it leaves you alone with exactly the problem the tool was supposed to help with.

We wanted a third path: software with actual judgment (something that compares, weighs, and ranks) where that judgment can be read in full, line by line, the way you’d read a library’s source. When Council tells you “this comes first,” it should be able to show you exactly why, and if you disagree, you should be able to correct it and see that correction take effect next time.

The constraint as design

Skipping the LLM on the decision path wasn’t a budget limitation dressed up as philosophy. It went the other way around: we started from the premise that the industry conflates “more capability” with “more compute, more cloud, more tokens,” and we wanted to test whether that premise was false in one concrete case.

An LLM on the critical path of deciding what to show you first carries three costs we’d rather not pay. Economic cost, because every dashboard load would trigger a call. Latency cost, because a remote model is orders of magnitude slower than a pure function. And opacity cost, because a generative model isn’t deterministic by design: the same input can produce different outputs, which makes it nearly impossible to write a test that says “this task must always score higher than that one.”

In exchange for giving up that flexibility, we got three things we actually care about. Determinism: the same UserData always produces the same Decision[], no exceptions. Explainability: every score ships with at least one human-readable Reason, generated by the very rule that moved the number. And real testability: not the “we confirmed it doesn’t throw” kind, but mutation testing on the engine’s core with Stryker, which doesn’t check whether the code runs but whether the tests would notice if someone changed a constant or flipped a comparison. That’s verifiable in the repository: not a quality claim, a threshold CI actually enforces.

The architecture of judgment

The decision engine takes in all of a user’s tasks, events, and upcoming payments, plus their learning profile, and returns a ranked list of Decision objects, each with a score from 1 to 20 and at least one reason. The pipeline is a chain of pure modifiers applied in sequence on top of a base score.

Everything starts with a base score set by item type: the priority the user assigned to a task (1 to 10), or a fixed value for events and upcoming payments that, by default, land somewhere in the middle of that same range.

On top of that base, in order:

  • Deadline proximity, in two layers. A step function gives a jump in score depending on whether the item is overdue, due today, tomorrow, or this week, categories a person recognizes at a glance. Layered on top of that runs a continuous pressure curve: instead of a task suddenly jumping in score the moment it crosses the one-week threshold, the score starts climbing smoothly several days earlier, so a task due in four days already reads a bit more urgent than one due in ten, even if both land in the same bucket of the step function.
  • Context weight. Every task belongs to a context, a life area like “work” or “health”, and that context carries a learned weight that multiplies the whole score. A context the user marked as a priority, or one the system learned needs attention, pulls everything that lives there upward.
  • Behavioral patterns. If the user has consistently postponed tasks in a given context, or if a specific task has already been postponed more than once, the engine notices and adjusts the score: sometimes down, so it doesn’t keep insisting on something that genuinely isn’t happening; sometimes up, when the pattern is finishing things in that context early in the day.
  • Aging boost. A task nobody has touched in weeks shouldn’t stay buried forever just because it has no due date. Past a grace period, the score starts climbing gradually the longer it sits untouched, up to a cap.
  • Time-of-day matching. Work that demands focus scores better during the hours most people have the most of it; lighter work scores better toward the end of the day. This is evaluated in the user’s actual local time zone, not the server’s, a small detail that matters a great deal when your users and your infrastructure don’t share a continent.
  • Weekly commitment boost. If the user explicitly committed to a task for this week, that task gets a flat bump and is re-sorted above items that score similarly but weren’t chosen on purpose.

We’re not publishing the exact constants: how much an “overdue today” is worth relative to a “weekly commitment,” or how wide the pressure curve’s window is. The ideas in the pipeline are fully open; the tuned numbers, calibrated against real usage data, are our craft. It’s the same distinction any serious recipe makes: the technique is teachable, the hand is earned.

Learning without ML

Council personalizes without training any model. Everything it learns lives as flat key-value pairs in a JSON field on the user’s profile. No neural network weights, no embeddings. Each context accumulates simple numbers: how often its tasks get completed, how much activity it’s seen recently, how many days it’s gone untouched, how stable the user themselves says it feels.

Those numbers combine through a readable formula to produce a context’s final weight: not one signal, but several correcting each other. A context with low stability gets a boost, because an area in crisis needs more attention, not less. A context with recent poor performance also gets a small lift, as recovery support rather than punishment. And if the user declared that context their focus for the week, the boost is larger still.

Every action the user takes (completing something, postponing it, dismissing a recommendation, accepting one) nudges these numbers in a small, predictable direction. Postponing a “work” task three mornings in a row doesn’t train anything hidden: it increments a postpone counter and lightly lowers a performance score, both visible. That means a user can ask Council to show its entire learned state (every weight, every rate, every counter) and correct it directly if the system misread the situation. There’s no box to open, because there was never a closed one.

What it doesn’t solve

Being explainable isn’t the same as being complete, and we’d rather say that out loud before someone finds out on their own.

Council doesn’t predict. It doesn’t know whether you’ll actually finish a task on time, and it doesn’t infer intent from free text. It doesn’t understand natural language at any point in the decision pipeline, only structure: dates, contexts, numeric priorities, counters. If two items end up with the exact same score, the tiebreaker is simply the earlier due date: a reasonable rule, not extra intelligence. And a brand-new user, with no history, starts with neutral weights: the system has nothing to learn from yet, so for a while Council decides with less judgment of its own than it will have later. Cold start is real, and we’re not hiding it.

We also haven’t resolved the deeper question of whether postponing something is always bad. The engine assumes it mostly is, and lightly penalizes repeated postponement. But some postponements are wise, and right now Council can’t tell the two apart. It’s one of the things we’re still studying.

Coda

What’s next is tuning the curves we have, not inventing more of them: understanding better how a context’s momentum should decay between cycles, which other behavioral signals are worth capturing without becoming invasive, and how to better communicate uncertainty when the engine is, literally, tied between two equally reasonable options.

This note is itself an exercise in advice, not control: we’re publishing the design so it can be questioned. If something we described here reads to you as a judgment error rather than just a number that needs tuning, tell us. That’s exactly the kind of correction the system, and we, are built to take.