Agentic workflows en Scala: orquestando decisiones de larga duración con Workflows4s y Pekko
En los últimos meses, el término agentic workflows se ha popularizado para describir sistemas que toman decisiones de forma iterativa, interactúan con servicios externos y mantienen estado a lo largo del tiempo. En muchos casos, este concepto se asocia casi exclusivamente a bucles alrededor de modelos de lenguaje (LLMs), lo que puede dar la impresión de que estamos ante un paradigma completamente nuevo.
Sin embargo, si miramos más allá del hype, muchos de estos sistemas se parecen bastante a algo que ya conocemos bien: workflows de larga duración que modelan procesos complejos, con decisiones intermedias, reintentos y dependencias externas.
En este artículo presento un ejemplo concreto implementado en Scala: un proceso de búsqueda de viajes orquestado como un workflow duradero utilizando Workflows4s y Apache Pekko. Aquí no buscamos un “agente autónomo” en el sentido tradicional del término, sino mostrar cómo modelar comportamiento agentic de forma explícita, observable y mantenible.
Qué entendemos aquí por agentic workflow#
Antes de entrar en el ejemplo, conviene aclarar términos.
Aunque muchas descripciones de agentic workflows asocian directamente el concepto con LLMs y herramientas, lo esencial sigue siendo la coordinación explícita entre capacidades especializadas y un flujo con estado persistente —más que el uso de un modelo concreto1.
En este contexto, un agentic workflow no es un agente genérico que ejecuta un bucle infinito tomando decisiones opacas, sino un proceso dirigido por objetivos que:
- Mantiene estado a lo largo del tiempo
- Incorpora decisiones y feedback
- Interactúa con herramientas externas
- Puede detenerse, reanudarse y evolucionar
La diferencia clave está en el control del flujo. El comportamiento “agentic” emerge de la estructura del workflow, no de una lógica implícita escondida en un bucle.
Dicho de otra forma: el workflow actúa como la memoria y el sistema nervioso del agente.
Por qué la búsqueda de viajes es un buen ejemplo#
La búsqueda de un viaje es un problema aparentemente sencillo, pero con suficiente complejidad como para justificar este enfoque:
- No se resuelve con una única llamada a una API
- Involucra múltiples pasos dependientes
- Requiere comparar resultados parciales
- Puede necesitar volver atrás y replantear decisiones
Un flujo típico incluye recopilar preferencias, consultar proveedores de vuelos y alojamiento, evaluar combinaciones y aplicar heurísticas para priorizar opciones. Además, es un proceso naturalmente asíncrono y potencialmente largo, que no debería depender de que una JVM esté viva durante todo su ciclo de vida.
El caso de uso: una agencia de viajes “agentic”#
El caso de uso que veremos a continuación está basado en un ejemplo original del proyecto Akka. Se trata de una aplicación que representa una agencia capaz de buscar vuelos y alojamientos a partir de una petición realizada por un usuario mediante una llamada HTTP.
A grandes rasgos, la aplicación combina un modelo de lenguaje (LLM) con un conjunto de herramientas especializadas que permiten consultar vuelos, encontrar alojamientos y enviar correos electrónicos. Una vez iniciada la búsqueda, el sistema recopila las opciones disponibles, evalúa distintas combinaciones y envía al solicitante un correo con varias alternativas, incluyendo la oferta con mejor relación calidad-precio.
Este ejemplo encaja de forma natural en la categoría de agentic workflows. La petición del usuario actúa como punto de entrada del flujo y, a partir de ahí, el sistema coordina de manera automática distintas capacidades especializadas: la búsqueda de vuelos, la búsqueda de alojamientos y la evaluación de las opciones resultantes. Cada una de estas tareas puede entenderse como la responsabilidad de una herramienta o componente concreto dentro del sistema.
El modelo de lenguaje participa como apoyo en determinadas decisiones —por ejemplo, priorizando opciones o manteniendo el contexto de la solicitud—, pero el control del proceso reside en el workflow. Es el flujo de trabajo el que define qué pasos se ejecutan, en qué orden y bajo qué condiciones.
El resultado no es solo una llamada a un modelo, sino un flujo con estado, decisión y recuperación que combina razonamiento, uso de herramientas y toma de decisiones para cumplir un objetivo bien definido: encontrar y proponer opciones de viaje relevantes para el usuario sin necesidad de intervención humana constante.
A partir de este caso de uso, veamos cómo modelar este comportamiento de forma explícita como un workflow de larga duración en Scala.
Modelando el proceso como un workflow de larga duración#
En lugar de implementar esta lógica como una cadena de futures, callbacks o efectos encadenados, el proceso se modela explícitamente como un workflow de larga duración.
Cada paso y cada decisión forman parte del modelo. El estado se persiste y el sistema puede pausar y reanudar la ejecución sin perder información, incluso tras reinicios o despliegues.
El siguiente diagrama BPMN muestra el flujo completo de búsqueda de viaje modelado en el ejemplo. Lejos de ser una metáfora visual, este diagrama refleja directamente la estructura del workflow implementado con Workflows4s: estados explícitos, paralelismo declarado y coordinación de capacidades especializadas.
Estos estados no representan simples pasos técnicos, sino puntos de decisión. Cada transición implica evaluar resultados parciales, aplicar reglas de negocio y decidir si avanzar, repetir un paso o finalizar el proceso.
Vale, ¿cómo se ve esto realmente en código?#
Estructura del workflow de búsqueda de viajes#
El corazón de la aplicación es el Trip Search Workflow, definido en el fichero TripSearchWorkflow.scala del repositorio. Puedes explorar todo el proyecto en GitHub: Repositorio trip‑agent en GitHub.
Este workflow orquesta todo el proceso de búsqueda de un viaje a partir de la petición inicial del usuario, y sirve como caso de estudio claro de un agentic workflow.
Estados, señales y eventos en el código#
El workflow en el repositorio modela explícitamente los conceptos que mencionamos antes: estado, señales y eventos. Estos se definen como case classes en TripSearchWorkflow, lo que permite aprovechar el sistema de tipos de Scala para razonar sobre la ejecución.
Por ejemplo, algunos de los estados principales son:
Started: estado inicial tras validar la petición del usuario.Found: tras encontrar vuelos y alojamientos.Sent: una vez que los resultados ya se han enviado al usuario.Booked: cuando el viaje ha sido reservado.
Estos estados son tipos en el código que describen momentos significativos en el flujo —no simples indicadores numéricos o cadenas— lo que favorece la claridad y el control estático del flujo.
Modelar estados con ADTs evita estados inválidos en tiempo de compilación, reduce bugs por estados inconsistentes y facilita la migración del workflow.
Definidos junto al workflow en 📁 TripSearchWorkflow.scala
enum TripSearchState:
/** El workflow ha sido iniciado y la petición del usuario ha sido validada */
case Started(request: TripRequest)
/** Se han encontrado vuelos y alojamientos que cumplen los criterios */
case Found(
request: TripRequest,
flights: List[Flight],
accommodations: List[Accommodation]
)
/** Los resultados han sido enviados al usuario por correo */
case Sent(
request: TripRequest,
options: List[TripOption]
)
/** El viaje ha sido finalmente reservado */
case Booked(
booking: BookingConfirmation
)El workflow de búsqueda de viajes progresa a través de una serie de estados explícitos, modelados como tipos algebraicos. Cada estado representa un momento significativo del proceso, no simplemente un paso técnico.
A diferencia de un enfoque basado en variables mutables o flags, aquí el estado del sistema queda capturado por el tipo que lo representa. Esto permite razonar de forma precisa sobre qué información está disponible en cada punto del flujo y qué transiciones son posibles.
Por ejemplo, el estado Started indica que la petición del usuario ha sido validada y que el proceso de búsqueda ha comenzado. El estado Found encapsula los resultados parciales —vuelos y alojamientos— mientras que Sent refleja que las opciones ya han sido comunicadas al usuario. Finalmente, Booked representa la finalización exitosa del proceso.
Este modelado hace explícita una idea clave de los agentic workflows: el agente no mantiene su “memoria” de forma implícita, sino que su estado es parte del modelo y puede persistirse, inspeccionarse y evolucionar a lo largo del tiempo.
Este enfoque recuerda a modelos de máquinas de estados o event-sourced workflows, pero aplicado a procesos de larga duración en los que las decisiones y los efectos externos forman parte del dominio.
Las señales que impulsan el workflow también están definidas en el mismo fichero:
FindTrip: inicia una nueva búsqueda.BookTrip: solicita formalizar una reserva.
Definidas junto al workflow en 📁 TripSearchWorkflow.scala
enum TripSearchSignal:
/** Inicia una nueva búsqueda de viaje a partir de la petición del usuario */
case FindTrip(request: TripRequest)
/** Solicita la reserva de una de las opciones propuestas */
case BookTrip(selection: TripSelection)El workflow no avanza de forma autónoma ni ejecuta un bucle continuo de decisiones. Su evolución está impulsada por señales, que representan intenciones externas claras y bien delimitadas.
En este ejemplo, la señal FindTrip actúa como punto de entrada al sistema: encapsula la petición inicial del usuario y desencadena la ejecución del workflow. Más adelante, la señal BookTrip permite reactivar el proceso para formalizar la reserva de una opción concreta.
Esta separación entre estado y señales introduce un modelo de interacción muy explícito. El workflow mantiene su estado interno, pero solo cambia como respuesta a una señal concreta y conocida. No hay transiciones implícitas ni efectos colaterales difíciles de rastrear.
Señales vs llamadas directas#
Desde el punto de vista conceptual, una señal no es lo mismo que una llamada directa a un método. Enviar una señal expresa una intención —quiero iniciar una búsqueda, quiero reservar este viaje—, pero no asume cómo ni cuándo se ejecutará la lógica asociada.
Este desacoplamiento resulta especialmente útil en workflows de larga duración, donde el proceso puede estar detenido, persistido o ejecutándose en otro nodo del sistema cuando se recibe la señal.
Este patrón apunta a una forma de diseño similar a event sourcing, en el que los eventos actúan como fuente de verdad para la evolución del estado. Esto permite reiniciar procesos, reconstruir historial y mantener trazabilidad sin lógica oculta.
Relación con el enfoque agentic#
En muchos sistemas “agentic”, la entrada al agente se modela como texto libre o como una llamada genérica al modelo de lenguaje. En este enfoque, en cambio, las interacciones posibles están tipadas y forman parte del contrato del sistema.
Esto no limita la flexibilidad del agente, sino que la hace explícita y controlable: sabemos exactamente qué puede solicitar un usuario y qué efectos puede tener cada señal sobre el workflow.
A medida que el workflow avanza, se emiten eventos que actualizan el estado interno de forma persistente. Este patrón de señal → evento → estado recuerda a modelos de event sourcing, pero con un diseño adaptado a procesos de larga duración como este.
Pasos del workflow (con rutas de código)#
La lógica del workflow se organiza en pasos bien delimitados, cada uno de los cuales corresponde a una responsabilidad concreta. Estos pasos están definidos como funciones o métodos en el código del workflow.
Los pasos principales, con indicación de dónde encontrarlos, son:
-
startSearchActivado por la señal
FindTrip. Valida la petición del usuario e inicializa el workflow en el estadoStarted. -
findInParallelEste paso coordina dos operaciones concurrentes:
-
findAccommodationsUsa el agente
AccommodationsSearchAgentpara buscar alojamientos compatibles con los criterios del usuario. -
findFlightsUsa el agente
FlightsSearchAgentpara buscar vuelos.
Ambos están en el mismo archivo de definición del workflow, siguiendo la estructura de pasos de Workflows4s.
-
-
sendEmailUsa el agente
MailWriterAgentpara componer y enviar un correo con los resultados obtenidos al usuario. Implementa la lógica de comunicación final del proceso. -
processBookingActivado por la señal
BookTrip, implementa la reserva del viaje seleccionado según criterios adicionales de negocio.
Estos pasos están implementados en el propio workflow de Scala y pueden leerse directamente en el repositorio, lo que hace que el ejemplo sea ideal para entender cómo un agentic workflow se materializa en código del mundo real.
El primer paso del workflow: startSearch#
Implementado como parte del 📁 TripSearchWorkflow.scala
def startSearch(
request: TripRequest
): WorkflowStep[TripSearchState] =
for
_ <- validateRequest(request)
_ <- emitEvent(TripSearchEvent.SearchStarted(request))
yield TripSearchState.Started(request)(La forma exacta puede variar en el código, pero el patrón es el mismo.)
El paso startSearch representa el punto de entrada efectivo al workflow. Se activa como respuesta a la señal FindTrip y cumple dos funciones fundamentales: validar la petición del usuario y mover el workflow a su primer estado significativo.
En este paso no se realiza ninguna búsqueda todavía. Su responsabilidad es preparar el terreno: comprobar que la solicitud es válida y dejar constancia de que el proceso ha comenzado. Esta separación puede parecer trivial, pero es clave para mantener el flujo claro y extensible.
¿Qué está ocurriendo realmente aquí?#
-
Validación explícita
La petición del usuario se valida antes de avanzar. Si algo falla, el workflow no progresa y el estado no cambia.
-
Emisión de un evento
En lugar de mutar el estado directamente, el paso emite un evento (
SearchStarted) que describe lo que ha ocurrido. -
Transición de estado clara
El workflow pasa al estado
Started, que ahora contiene la petición validada y puede ser persistido.
Este patrón —validar, emitir un evento y transicionar de estado— se repite a lo largo de todo el workflow y facilita tanto la observabilidad como la recuperación ante fallos.
En muchos ejemplos de agentes, el sistema empieza a “razonar” inmediatamente tras recibir una petición. Aquí, en cambio, el primer paso es estructural: asegurarse de que el proceso tiene una base sólida antes de avanzar.
Esto refuerza una idea central del enfoque workflow-first: el comportamiento agentic no consiste en reaccionar impulsivamente, sino en progresar de forma controlada a través de estados bien definidos.
En contraste, existen otras arquitecturas que mantienen enfoques LLM-first como LangGraph, donde el modelo a menudo mantiene tanto estado como control del flujo (y esto puede llevar a menos observabilidad).
Parte del 📁 TripSearchWorkflow.scala
def findInParallel(
request: TripRequest
): WorkflowStep[TripSearchState] =
for
results <- parallel(
findFlights(request),
findAccommodations(request)
)
(flights, accommodations) = results
_ <- emitEvent(
TripSearchEvent.ResultsFound(
request,
flights,
accommodations
)
)
yield TripSearchState.Found(
request,
flights,
accommodations
)(El código se ha simplificado para resaltar el patrón de orquestación.)
Una vez que el workflow se encuentra en el estado Started, el siguiente paso consiste en buscar vuelos y alojamientos. Estas dos tareas son independientes entre sí, por lo que el workflow las ejecuta en paralelo.
El paso findInParallel expresa esta concurrencia de forma declarativa: no hay threads, futures ni sincronización manual. El workflow se limita a indicar que ambas búsquedas pueden ejecutarse simultáneamente y a combinar sus resultados cuando ambas han finalizado.
¿Qué aporta este enfoque?#
-
Paralelismo explícito y legible
La intención es clara: vuelos y alojamientos se buscan al mismo tiempo.
-
Aislamiento de responsabilidades
Cada búsqueda está encapsulada en su propio agente (
FlightsSearchAgentyAccommodationsSearchAgent). -
Orquestación sin acoplamiento
El workflow no conoce los detalles de ejecución ni cómo se gestionan los recursos.
Desde el punto de vista del workflow, ejecutar en paralelo es una decisión de alto nivel, no un detalle de implementación.
Esta conciencia de paralelismo está en el modelo y no en el runtime: no hay magia oculta detrás del comportamiento concurrente —es una decisión de diseño clara del workflow.
Concurrencia y durabilidad#
En un workflow de larga duración, la concurrencia no consiste solo en ejecutar tareas más rápido. También implica poder persistir el estado intermedio, recuperarse de fallos parciales y continuar el proceso sin repetir trabajo innecesario.
Al expresar el paralelismo como parte del modelo del workflow, estas garantías pasan a ser responsabilidad del runtime y no del código de negocio.
Relación con el enfoque agentic#
En muchos sistemas “agentic”, el uso de herramientas se realiza de forma secuencial dentro de un bucle de razonamiento. Aquí, en cambio, el workflow coordina varias capacidades especializadas de forma concurrente, optimizando el tiempo de respuesta sin perder control ni trazabilidad.
El agente no “decide” buscar vuelos antes que alojamientos: reconoce que ambas tareas son independientes y las ejecuta como tal.
Agentes como capacidades especializadas#
Implementado en el módulo de agentes del proyecto 📁
trait FlightsSearchAgent:
def search(
criteria: FlightSearchCriteria
): Task[List[Flight]](La implementación concreta puede usar APIs externas, mocks o adaptadores, pero el workflow no depende de esos detalles.)
En este ejemplo, un “agente” no es una entidad autónoma que decide qué hacer a continuación. Es una capacidad especializada con una responsabilidad bien definida.
El FlightsSearchAgent encapsula la lógica necesaria para buscar vuelos que cumplan ciertos criterios. Su interfaz es deliberadamente simple: recibe unos parámetros de búsqueda y devuelve una lista de resultados, sin conocer nada sobre el contexto global del workflow.
Esta separación es intencionada. El agente sabe cómo buscar vuelos, pero no cuándo hacerlo ni qué hacer con los resultados.
Es el workflow quien decide cuándo invocar al agente, cómo combinar sus resultados con los de otros agentes y cómo avanzar el estado del proceso.
Además, esta separación resulta muy útil de cara al testing y al mantenimiento, algo que en la práctica se traduce en menos sorpresas en producción.
Al desacoplar los agentes del workflow, es posible probarlos de forma unitaria y aislada, sin necesidad de ejecutar el flujo completo. Un ejemplo concreto puede verse en el test de AccommodationsSearchAgent, definido en el fichero AccommodationsSearchAgentSuite.scala del repositorio.
Agentes vs “agentes autónomos”#
En muchos frameworks de agentes, cada componente tiene cierto grado de autonomía y participa en un bucle de razonamiento compartido. En este enfoque, en cambio, los agentes son deterministas y predecibles.
El comportamiento agentic emerge de la orquestación de estas capacidades dentro de un workflow duradero, no de la autonomía individual de cada agente.
¿Dónde encaja el LLM?#
Un modelo de lenguaje puede implementarse siguiendo el mismo patrón: como un agente que recibe contexto estructurado y devuelve una decisión, un ranking o una explicación.
De nuevo, el LLM no controla el flujo; es una herramienta más dentro del sistema.
Un flujo explícito, no un bucle implícito#
Este diseño evita uno de los problemas habituales en muchos sistemas “agentic”: la lógica de control no está escondida en un bucle genérico, sino declarada explícitamente en la estructura del workflow.
El resultado es un sistema en el que se puede responder con claridad a preguntas como:
¿en qué punto del proceso estamos?, ¿qué decisiones se han tomado? o ¿qué ocurre si el proceso se interrumpe y se reanuda horas después?
El papel de Workflows4s#
Workflows4s proporciona las abstracciones necesarias para describir este tipo de procesos sin acoplar la lógica de negocio a los detalles de ejecución.
El workflow define:
- Qué pasos existen
- Qué estado se mantiene
- Cómo se transita entre estados
El runtime se encarga de aspectos que, de otro modo, acabarían dispersos por el código: persistencia, reintentos, recuperación ante fallos o reanudación tras una interrupción.
Esto permite centrar el diseño en el dominio y razonar sobre el proceso como una unidad coherente, en lugar de como una secuencia de efectos difíciles de seguir.
Requisitos del modelo de ejecución#
Workflows4s asume que cada paso de la ejecución es idempotente porque en caso de fallo reintenta el mismo paso hasta completarse. Esto es parte del modelo de ejecución at-least-once, donde las operaciones externas deben ser diseñadas para soportar re-ejecución segura.
Lejos de ser una limitación accidental, esta decisión permite construir workflows durables y recuperables, a costa de exigir un diseño cuidadoso de los efectos externos.
Un ejemplo seguro es findFlights(criteria). Si se ejecuta dos o diez veces, el resultado puede variar ligeramente, pero no rompe invariantes.
El workflow de búsqueda de viajes tiene dos puntos críticos desde el punto de vista de idempotencia:
-
sendEmailSi el paso falla después de enviar el correo: el workflow reintentará y el usuario recibe el mismo email varias veces. Para evitarlo podemos:
- Usar un
messageIddeterminista (por ejemplo,workflowId) - Guardar en estado/evento que el email ya fue enviado
- Delegar en un servicio de email idempotente
- Usar un
-
processBookingEste es todavía más delicado ya que de fallar se podría duplicar la reserva. Igual que en el caso anterior:
- La reserva debe ser idempotente
- O debe protegerse con un
bookingId - O debe producir un evento durable antes del efecto externo
En resumen, para diseñar workflows seguros en Workflows4s:
- Utilizar pasos pequeños
- Efectos externos protegidos por identificadores
- Eventos antes de efectos irreversibles
- Agentes idempotentes
- Evitar side effects “silenciosos”
¿Por qué esto encaja con el mensaje “agentic sin hype”?
En el enfoque workflow-first no hay garantías implícitas, todo está modelado: construir agentic workflows en producción es ingeniería, no prompting.
Ejecución con Apache Pekko#
La ejecución del workflow se apoya en Apache Pekko, que aporta un modelo probado para concurrencia y sistemas distribuidos.
Desde el punto de vista del workflow, Pekko es un detalle de infraestructura: proporciona aislamiento, resiliencia y ejecución concurrente sin contaminar el código del dominio con preocupaciones de bajo nivel.
Esta separación es clave para mantener el sistema comprensible. El workflow describe el qué y el por qué; Pekko se encarga del cómo.
Dónde encaja el comportamiento “agentic”#
Visto así, el comportamiento “agentic” no aparece como una capa adicional, sino como una consecuencia natural del diseño:
- El objetivo del sistema está claro
- El estado es explícito y persistente
- Las decisiones forman parte del modelo
- El proceso puede reaccionar a resultados intermedios
El “agente” no es una entidad opaca, sino el propio workflow en ejecución.
Esto tiene una ventaja importante: el sistema es observable y depurable. Se puede inspeccionar en qué estado se encuentra, por qué tomó una decisión concreta y cómo llegó hasta allí.
Integración de modelos de lenguaje (sin perder el control)#
Los modelos de lenguaje pueden integrarse de forma natural en este enfoque, por ejemplo para:
- Priorizar o rankear opciones
- Generar explicaciones o resúmenes
- Asistir en decisiones heurísticas
Sin embargo, el control del flujo permanece siempre en el workflow. El modelo de lenguaje aporta información o sugerencias, pero no decide qué paso se ejecuta a continuación.
Esto evita convertir el sistema en una caja negra difícil de razonar y permite mantener garantías sobre su comportamiento, algo especialmente relevante en entornos de producción.
Qué aporta Scala a este enfoque#
Scala encaja especialmente bien en este tipo de sistemas:
- Tipos algebraicos para modelar estados y transiciones
- Separación clara entre lógica pura y efectos
- Capacidad para expresar flujos complejos de forma declarativa
Lejos de ser una desventaja frente a enfoques más dinámicos, esta explicitud resulta una fortaleza cuando el sistema crece y evoluciona.
Conclusión#
Implementar agentic workflows en Scala no requiere adoptar paradigmas radicalmente nuevos ni depender de comportamientos emergentes difíciles de controlar. En muchos casos, basta con aplicar principios clásicos de ingeniería de software —modelado explícito, durabilidad y separación de responsabilidades— a problemas que implican decisiones complejas y de larga duración.
Workflows4s y Apache Pekko proporcionan una base sólida para este tipo de sistemas, permitiendo construir agentes que no solo toman decisiones, sino que lo hacen de forma comprensible, observable y mantenible.
El código completo del ejemplo está disponible en GitHub2.
-
Foto de Immo Wegmann en Unsplash ↩︎