# Integración con Genesys Source: https://docs.jelou.ai/connect/panel-multi-agente/genesys Conecta Jelou con Genesys para gestionar conversaciones y habilitar la interoperabilidad entre ambas plataformas. Jelou integra los canales de comunicación con Genesys, permitiendo a los usuarios: * Gestionar conversaciones desde Genesys utilizando los canales de comunicación conectados en Jelou. * Transferir conversaciones y contexto de interacción entre Jelou y Genesys para garantizar una atención continua y eficiente. * Escalar conversaciones hacia agentes de Genesys cuando sea necesario. * Centralizar la gestión de conversaciones en un ecosistema integrado entre ambas plataformas. ## Requisitos previos Para utilizar la integración con Genesys, necesitas: * Una cuenta **Enterprise** activa en Jelou. * El nodo de **Genesys** habilitado en la cuenta. * Una cuenta activa de **Genesys Cloud** con permisos administrativos. * Un cliente **OAuth** con permisos para consumir las APIs de Genesys Cloud. * Los canales y flujos que participarán en la interoperabilidad previamente definidos. *** ## Instala la aplicación Inicia sesión en Jelou. Dirígete al módulo **Brain** y selecciona la sección **Canvas**. Canvas en Brain Dentro del Canvas, crea un nuevo flujo según tu necesidad. Para este ejemplo: 1. Agrega un **nodo de texto** y configura un mensaje de bienvenida para el canal. 2. Desde la barra lateral de nodos, arrastra el **nodo de Genesys** hacia el flujo. Se recomienda agregar: * Un mensaje de confirmación para transferencias exitosas. * Un mensaje de manejo de errores en caso de falla de integración. Nodo Genesys en Brain Antes de configurar la integración, valida que existan los roles necesarios para el cliente OAuth. Ruta: **Menu > User Management > Roles and Permissions** Los roles permitirán que el cliente OAuth tenga acceso a los recursos y APIs utilizados por la integración. Roles en Genesys Cloud Selecciona el rol correspondiente para revisar los permisos habilitados. Ruta: **Menu > User Management > Roles and Permissions > \[Rol] > Edit Role** El rol **Developer** puede utilizarse con los permisos predeterminados de Genesys Cloud. Permisos del rol en Genesys Cloud Dirígete a: **Menu > IT and Integrations > OAuth** Crea un cliente OAuth que permita autenticar el middleware o servicio externo encargado de la interoperabilidad con Genesys Cloud. Cliente OAuth en Genesys Cloud Una vez creado, abre la aplicación para revisar su configuración. Ruta: **Menu > IT and Integrations > OAuth > \[Cliente OAuth] > Edit Application** Verifica la siguiente información: * Nombre de la aplicación. * Tipo de autenticación. * Grant Type. * Configuración general. Configuración del cliente OAuth Selecciona la pestaña **Roles** del cliente OAuth. Ruta: **Menu > IT and Integrations > OAuth > \[Cliente OAuth] > Roles** Asigna los roles previamente configurados para que el cliente OAuth pueda consumir las APIs requeridas por la integración. Verifica que las divisiones asociadas a los roles correspondan a todas las interacciones que utilizará el conector. Una configuración incorrecta puede generar errores de permisos durante la operación. Roles asignados al cliente OAuth Dirígete a: **Menu > Digital and Telephony > Message > Platform Integrations** Crea una integración de tipo **Open Messaging** para permitir el intercambio de mensajes entre Genesys Cloud y Jelou. Integración Open Messaging Abre la integración creada para revisar su configuración. Detalle de Open Messaging Una vez creada la integración Open Messaging, identifica el **Integration ID**. Este identificador corresponde al valor único de la integración y será utilizado posteriormente para configurar el Trigger. El **Integration ID** se encuentra al final de la URL cuando accedes al detalle de la integración Open Messaging. Dirígete a: **Menu > Orchestration > Triggers** Crea un Trigger que escuche los eventos asociados a las conversaciones de Open Messaging. Este Trigger será el encargado de ejecutar automáticamente el Workflow cuando ocurra el evento configurado. Trigger configurado en Genesys Abre el Trigger creado y configura una condición utilizando el **Integration ID** obtenido anteriormente. Esta condición garantiza que únicamente se ejecuten los eventos correspondientes a la integración configurada. Condición del Trigger El Trigger debe ejecutar el Workflow de Architect encargado de notificar a Jelou que la atención en Genesys finalizó. Dirígete a: **Menu > Orchestration > Architect > Flows** Crea un Workflow que reciba la información enviada por el Trigger. Este Workflow será responsable de procesar los datos de la conversación y ejecutar el Data Action que notifica la finalización a Jelou. Workflow en Architect Dentro del Workflow, configura las variables que recibirán la información enviada desde el Trigger. Estas variables permitirán identificar la conversación y construir la solicitud hacia el Data Action. Variables del Workflow Dentro del Workflow, agrega una tarea para ejecutar el **Data Action** encargado de notificar a Jelou que la atención en Genesys terminó. Invocación del Data Action Dirígete a: **Menu > IT and Integrations > Data Actions** Crea un nuevo Data Action que será utilizado por el Workflow. Data Action en Genesys Abre el Data Action creado y configura la petición hacia el webhook de Jelou. Este Data Action notifica a Jelou que la atención en Genesys Cloud terminó. Al recibirlo, Jelou retoma el control y el canal continúa atendiendo al usuario final, sin que este perciba el cambio. Ruta: **Menu > IT and Integrations > Data Actions > \[Data Action] > Configuration** **Método y endpoint** | Campo | Valor | | -------------------- | --------------------------------------------- | | Método HTTP | `POST` | | Request URL Template | `https://chatbot.jelou.ai/v1/genesys/webhook` | **Headers** | Header | Valor | | -------------- | ------------------ | | `Content-Type` | `application/json` | **Body del request** El webhook espera un evento de Open Messaging con la siguiente estructura: ```json theme={null} { "id": "EVENT_ID", "type": "Event", "direction": "Outbound", "conversationId": "CONVERSATION_ID", "channel": { "from": { "id": "FROM_ID" }, "to": { "id": "CUSTOMER_ID", "idType": "Phone" } }, "events": [ { "eventType": "CustomerEnd" } ] } ``` **Descripción de los campos** | Campo | Tipo | Descripción | | -------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Identificador único del evento enviado. | | `type` | string | Tipo de payload. Para notificar la finalización siempre es `Event`. | | `direction` | string | Dirección del evento respecto a Genesys Cloud. Debe ser `Outbound` (sale de Genesys hacia Jelou). | | `conversationId` | string | Identificador de la conversación en Genesys Cloud. Es el valor con el que Jelou identifica la transferencia activa. | | `channel.from.id` | string | Identificador del participante de Genesys que emite el evento (por ejemplo, el agente o el flujo de Architect). | | `channel.to.id` | string | Identificador del usuario final en el canal. En WhatsApp es el número de teléfono en formato E.164. | | `channel.to.idType` | string | Tipo de identificador del usuario final. Para canales de WhatsApp usa `Phone`. | | `events[].eventType` | string | Evento a notificar. Usa `CustomerEnd` para indicar que la atención en Genesys Cloud finalizó y el control vuelve al canal. | El campo `direction` se nombra desde el punto de vista de Genesys Cloud: `Outbound` significa que el evento **sale de Genesys** hacia Jelou. El webhook rechaza los payloads marcados como `Inbound`. **Mapeo de variables** En la sección **Input Contract** declara las variables que recibirá el Data Action desde el Workflow y referéncialas en el body mediante la sintaxis `${input.nombreVariable}`. Los nombres del ejemplo son ilustrativos: usa los que hayas definido en tu Workflow. ```json theme={null} { "id": "${input.eventId}", "type": "Event", "direction": "Outbound", "conversationId": "${input.conversationId}", "channel": { "from": { "id": "${input.fromId}" }, "to": { "id": "${input.customerId}", "idType": "Phone" } }, "events": [ { "eventType": "CustomerEnd" } ] } ``` El `conversationId` debe ser el mismo que Genesys Cloud asignó a la conversación transferida desde Jelou. Si no corresponde a una transferencia activa, Jelou no podrá identificarla y el control no se devolverá al canal. Configuración del Data Action *** ## Configura la aplicación Después de completar la configuración en Genesys Cloud: Regresa al módulo **Brain** y selecciona el nodo de **Genesys** dentro del flujo. Completa la configuración del nodo con los parámetros correspondientes de la integración. Verifica que la conexión con Genesys Cloud sea exitosa antes de continuar. Cuando la configuración esté lista, **guarda y publica** el flujo. *** ## Usa la aplicación Una vez configurada la integración: * Las conversaciones podrán transferirse automáticamente entre Jelou y Genesys Cloud. * Los canales creados en Jelou automatizarán la atención inicial de los clientes. * Los agentes podrán continuar la conversación directamente desde Genesys Cloud. * Cuando la atención finalice en Genesys Cloud, el Trigger y el Workflow notificarán a Jelou y el canal retomará la conversación con el usuario final. * Los eventos de conversación serán procesados automáticamente mediante Open Messaging, Trigger, Workflow y Data Actions. No se requieren acciones manuales adicionales una vez publicada la automatización. # Integración con HubSpot Source: https://docs.jelou.ai/connect/panel-multi-agente/hubspot Conecta Jelou con HubSpot para gestionar conversaciones, automatizar flujos y centralizar la atención al cliente. Jelou integra los canales de comunicación y automatizaciones conversacionales con HubSpot, permitiendo a los usuarios: * Gestionar conversaciones desde HubSpot utilizando canales de comunicación conectados en Jelou. * Automatizar flujos conversacionales mediante canales configurados desde el módulo Brain. * Transferir conversaciones y atención de clientes hacia Inbox de HubSpot o canales personalizados. * Centralizar la atención al cliente y automatizaciones en una única plataforma. ## Requisitos previos Para utilizar la integración con HubSpot, necesitas: * Una cuenta **Enterprise** activa en Jelou. * El nodo de **HubSpot** habilitado en la cuenta. *** ## Instala la aplicación Inicia sesión en Jelou. Dirígete al módulo **Brain** y selecciona la sección **Canvas**. Canvas en Brain Dentro del Canvas, crea un nuevo flujo según tu necesidad. Para este ejemplo: 1. Agrega un **nodo de texto** y configura un **mensaje de bienvenida** para el canal de prueba. 2. Desde la barra lateral de nodos, arrastra el **nodo de HubSpot** hacia el flujo. Se recomienda agregar: * Un mensaje de confirmación para asignaciones exitosas. * Un mensaje de manejo de error en caso de falla de integración. Nodo en Brain Dentro del nodo de HubSpot, haz clic en **Continuar configuración en HubSpot**. La plataforma redireccionará automáticamente a HubSpot para completar la instalación y autorización de la aplicación. Configuración del nodo de HubSpot HubSpot te pedirá seleccionar la cuenta que deseas conectar con Jelou. Elige la cuenta correspondiente y haz clic en **Elegir cuenta**. Seleccionar cuenta de HubSpot A continuación, revisa los permisos que la aplicación solicita para enviar y recibir mensajes, gestionar conversaciones y acceder a la información de tu cuenta. Permisos de la integración con HubSpot Haz clic en **Conectar aplicación**. Una vez finalizada la autorización, regresarás automáticamente a Jelou. *** ## Configura la aplicación Después de instalar la integración: Regresa al módulo **Brain** y selecciona el **nodo de HubSpot** dentro de tu flujo. Configura el canal de comunicación que utilizarás: * **Inbox de HubSpot** — para gestionar conversaciones directamente desde HubSpot. * **Canal personalizado** — para canales configurados de forma independiente. Configuración de bandeja de entrada en HubSpot Configura las acciones deseadas para la atención de clientes y automatizaciones dentro del flujo. Cuando la configuración esté lista, **guarda y publica** el flujo. *** ## Usa la aplicación Una vez configurada la integración: * Las conversaciones recibidas desde los canales conectados podrán ser transferidas y gestionadas desde HubSpot. * Los canales creados en Jelou podrán automatizar la atención inicial de clientes. * Los agentes podrán continuar la conversación directamente desde HubSpot Inbox o desde el canal configurado. * Las sincronizaciones y transferencias de conversaciones se realizan automáticamente según la configuración establecida en el flujo. No se requieren acciones manuales adicionales una vez publicada la automatización. *** ## Desinstala la aplicación Para desinstalar la aplicación de Jelou desde HubSpot: Inicia sesión en tu cuenta de HubSpot. Haz clic en el ícono de **configuración** ubicado en la barra de navegación superior. En el menú lateral izquierdo, dirígete a **Integraciones > Aplicaciones conectadas**. Ubica la aplicación de **Jelou**. Haz clic en **Acciones** y luego selecciona **Desinstalar**. En la ventana de confirmación, escribe `desinstalar` y haz clic en **Desinstalar** para confirmar la acción. Una vez desinstalada la aplicación: * Jelou dejará de tener acceso a la cuenta de HubSpot. * Las automatizaciones y transferencias configuradas mediante el nodo de HubSpot dejarán de funcionar. * Para eliminar completamente la integración del flujo, debes borrar el nodo de HubSpot dentro del módulo Brain en Jelou. # Guía general para construir con IA Source: https://docs.jelou.ai/guides/agentes-ia/index Configura instrucciones, modelos, temperatura, bases de conocimiento, MCPs y herramientas para tus AI Agents ## Instrucción La instrucción define el comportamiento base del agente: qué rol adopta, qué tono utiliza y qué pasos sigue antes de responder. Asegúrate de que sea breve, concreta y libre de ambigüedades. Si necesitas profundizar en la redacción, consulta la guía de buenas prácticas en `@prompting.mdx`. [Ver recomendaciones y ejemplos de prompting](/guides/agentes-ia/prompting) ## Modelo Escoge el modelo que mejor se ajuste al tipo de interacción que buscas. Considera latencia, costo y complejidad de las tareas antes de seleccionarlo. ### Modelos disponibles * **Llama 4 Scout**: modelo ágil y de baja latencia, ideal para ideas rápidas e interacciones ligeras. * **GPT 4.1 Mini**: variante simplificada de GPT-4.1 optimizada para respuestas rápidas con menor demanda de recursos. * **Claude 3.5 Sonnet**: excelente para tareas complejas que requieren textos más elaborados y contextos extensos. * **GPT 4-o (Azure)**: versión de GPT 4-o alojada en Azure, enfocada en estabilidad y rendimiento en entornos empresariales. * **Llama 4 Maverick**: modelo de alto rendimiento diseñado para razonamientos exigentes y resolución multistep. * **GPT 4.1**: evolución refinada de GPT-4, con mejor comprensión, razonamiento y precisión. * **GPT 4-o Mini**: versión más rápida y ligera de GPT 4-o, orientada a casos donde prima la velocidad. ## Temperatura La temperatura controla el nivel de creatividad del modelo. Utiliza valores entre `0` y `1`: en `0` obtendrás respuestas deterministas y consistentes; conforme te acerques a `1`, las respuestas serán más creativas y variadas. Ajusta este parámetro según necesites precisión o exploración. ## Bases de conocimiento Refuerza al agente con documentación específica para que responda con información actualizada y verificable. * **Archivos**: carga archivos de hasta 2 MB en formatos `.pdf` o `.csv`. * **URLs**: añade enlaces cuya descarga no supere los 5 MB. Verifica que el contenido sea accesible públicamente. ## MCPs Conecta Model Context Protocols (MCPs) para ampliar las capacidades del agente. Cada integración requiere: * **URL del endpoint** al que se realizará la consulta. * **Headers opcionales** con credenciales o metadatos necesarios para la autenticación. * **Nombre identificador** para reconocer el MCP dentro del estudio. * **Descripción** que resuma qué información o acciones ofrece. ## Tools Las herramientas permiten que el agente ejecute acciones externas. Puedes configurarlas de dos formas: * **Tools creadas en la plataforma**: defínelas directamente en Brain Studio para integraciones reutilizables y gestionadas. * **Llamadas HTTP**: especifica un endpoint, método y esquema de entrada/salida para realizar peticiones puntuales a tus servicios. Combina estos elementos para que el agente seleccione automáticamente la herramienta adecuada durante la conversación. ## Configuración avanzada En la pestaña de configuración avanzada del nodo AI Agent encontrarás opciones adicionales que amplían las capacidades del agente para procesar diferentes tipos de contenido y mensajes interactivos. ### Soporte de lectura payload en mensajes entrantes Habilita esta opción para permitir que el AI Agent procese payloads de respuesta rápida de mensajes interactivos. Cuando está activada, el agente puede interpretar y responder a las acciones del usuario en botones de respuesta rápida, mejorando la experiencia en flujos conversacionales que utilizan elementos interactivos. **Cuándo habilitarla:** * Cuando tu flujo incluye mensajes con botones de respuesta rápida (quick replies). * Si necesitas que el agente procese la información enviada a través de payloads en lugar de solo el texto visible del botón. * Para flujos que requieren capturar datos estructurados desde interacciones con botones. ### Soporte de documentos PDF Activa esta configuración para que el AI Agent pueda leer y procesar documentos PDF enviados por los usuarios durante la conversación. El agente extraerá el contenido del documento y podrá responder preguntas o realizar acciones basadas en la información contenida. **Nota importante:** Esta funcionalidad solo procesa el texto del PDF. Las imágenes contenidas dentro del documento no serán procesadas ni analizadas por el agente. **Cuándo habilitarla:** * Cuando esperas que los usuarios envíen documentos PDF como parte de la interacción. * Si necesitas que el agente analice, resuma o extraiga información de archivos PDF. * Para casos de uso como validación de documentos, extracción de datos o análisis de contenido. ### Leer imagen con URL y pie de foto Esta configuración permite al AI Agent procesar imágenes que incluyen un pie de foto (caption). El agente podrá acceder tanto a la imagen como al texto descriptivo asociado, procesando ambos elementos de forma conjunta. **Cuándo habilitarla:** * Cuando los usuarios envían imágenes con descripciones textuales. * Si necesitas que el agente procese tanto el contenido visual como el contexto textual de la imagen. * Para flujos que requieren análisis combinado de imagen y texto. ## Buenas prácticas * Se recomienda activar siempre la sección de Base de Conocimiento (Knowledge) para anclar las respuestas del agente a la documentación oficial de tu negocio o a la documentación básica que necesite el prompt. Esto reduce drásticamente el riesgo de alucinaciones, ya que el modelo priorizará la información de tus archivos sobre su conocimiento general. Para maximizar su efectividad, asigna descripciones claras a cada archivo cargado, permitiendo que el agente sepa exactamente cuándo debe buscar información en ellos. # Prompting Source: https://docs.jelou.ai/guides/agentes-ia/prompting Buenas prácticas y ejemplos para redactar prompts efectivos en AI Agents Esta guía se centra exclusivamente en el diseño y la redacción de prompts efectivos para AI Agents y AI Tasks. Para conceptos generales de agentes, estructura, herramientas y configuraciones, revisa la `Guía general de AI Agents`. ## Ejemplos de prompts para un AI Agent ### Preguntas Frecuentes Cuando necesitemos darle contexto adicional sobre una empresa al AI Agent para que pueda responder a las consultas del usuario. Deben usar el nodo AI Agent. #### Prompt ```text theme={null} Eres un asesor de [nombre cliente], [descripción de cliente], que debe responder consultas al usuario. Siempre responde a las consultas de manera [tono, personalidad] Cuando el usuario te haga una consulta, debes siempre seguir el siguiente proceso: 1. Busca contexto sobre [nombre cliente] en los archivos que tienes disponible. 2. Determinar si la información que obtienes es relevante para la consulta del usuario. Se muy cuidadoso en este paso, ya que debes definir de manera clara si esta información es relevante o no. 3. Si y solo si el usuario expresa deseo de salir o terminar la interacción, ejecuta la función end_function con los siguientes parámetros: {"salir":true} ``` Incluso, se puede incluir guías para manejar la interacción, como por ejemplo: ```text theme={null} Ten en cuenta las siguientes indicaciones para tu interacción: - Si no encuentras información sobre un servicio, asume que [nombre comercio] no brinda ese servicio. - Eres parte de [nombre cliente], por lo tanto puedes hablar en primera persona. ``` ### Redireccionar al usuario Cuando se necesite redireccionar al usuario a una parte del flujo según su intención. Si es el único propósito (como un router) pueden usar un AI Task. Si esto es parte de una interacción, como por ejemplo redireccionar al usuario a un flujo cuando lo solicita, pueden usar un AI Agent. Esto se puede conseguir con una combinación de AI y nodo condicional. Deben guardar la respuesta del AI, que la pueden acceder con `{{$memory.nombreVariable}}`. #### Prompt ```text theme={null} Eres un router de AI que debe procesar un mensaje del cliente, para decidir a que agente especializado lo envías. Existen 5 agentes: 1. Tarjetas de crédito (emisión, información general sobre tarjetas o American Express /AMEX) 2. Cuentas de ahorro y cuentas corrientes (aperturas e información general) 3. Documentos (certificados, estados de cuenta) 4. Consultas generales (consultas generales sobre el banco) 5. Otro Intenta en lo posible ubicarlo en algún agente, y solo si el mensaje es muy genérico, puedes elegir el 5 (otro). En caso de que no se entienda el mensaje, o sea muy ambigüo, vuelve a preguntarle al usuario en palabras amables. Siempre tienes que terminar tu ejecución llamando a end_function con un esquema en formato JSON: `{flujo:[flujo]}`. Flujo puede ser igual a "tarjetas", "cuentas", "documentos", "consultas" y "otro". Ejemplos: - Si usuario hace un consultas sobre tarjetas de crédito, terminas con end_function `{flujo:"tarjetas"}` - Si usuario te pide un certificado bancario, terminas con end_function `{flujo:"documentos"}` ``` ### Solicitar datos al usuario Para procesos de registro o flujos en los que se debe pedir información al usuario. #### Prompt ```text theme={null} Debes solicitar al usuario su nombre y correo electrónico. Cuando el usuario te proporcione su nombre y correo, ejecuta tu función end_function con el esquema en formato JSON: {"nombre":[nombre],"correo":[correo]} ``` Luego, se puede acceder con `{{$memory.nombreVariable}}` para guardarla en un nodo Datum. ## Ejemplos de prompts para un AI Task Los AI Task son peticiones a OpenAI/LLM preferido. Su prompt es más fácil de escribir y no requiere de utilizar palabras claves para terminar como `end_function`. Su objetivo siempre será cumplir lo que le dice el prompt y guardar la respuesta en formato JSON. ### Redireccionar al usuario ```text theme={null} Debes procesar el mensaje del usuario, para decidir a que flujo se envía. El mensaje del usuario es: {{$message.text}} Existen 5 flujos: 1. Tarjetas de crédito (emisión, información general sobre tarjetas o American Express / AMEX) 2. Cuentas de ahorro y cuentas corrientes (aperturas e información general) 3. Documentos (certificados, estados de cuenta) 4. Consultas generales (consultas generales sobre el banco) 5. Otro Intenta en lo posible ubicarlo en algún flujo, y solo si el mensaje es muy genérico, puedes elegir el 5 (otro). Genera el siguiente JSON: {flujo:[flujo]}. Donde flujo puede ser igual a "tarjetas", "cuentas", "documentos", "consultas" y "otro". Ejemplos: - Si usuario hace un consultas sobre tarjetas de crédito, terminas con {flujo:"tarjetas"} - Si usuario te pide un certificado bancario, terminas con {flujo:"documentos"} ``` La diferencia es que no se hace uso de `end_function`. ## Resumen de Buenas Prácticas | Aspecto | Recomendación | | ----------- | ------------------------------------------------------------------------------------------------- | | Claridad | Usa prompts directos y estructurados | | Lenguaje | Sé consistente en los términos (ej: "herramienta") | | Uso de JSON | Especifica con claridad el formato esperado | | Terminación | En AI Agents, usa `end_function` correctamente. En AI Task solo asegúrate de especificar el JSON. | | Contexto | Indica cómo evaluar entradas o referencias relevantes. | ## Tipos de contenido y formato en prompts * Mantén un lenguaje consistente en todo el prompt. * Especifica claramente el formato de salida (JSON recomendado). * Si el flujo requiere terminar, define explícitamente el uso de `end_function`.ar ## Diseño Inteligente de Prompts Condicionales Para obtener mejores resultados, es clave minimizar la cantidad de decisiones que debe tomar la IA, especialmente cuando no están directamente relacionadas con su objetivo principal. En lugar de cargar el prompt con múltiples instrucciones condicionales, es preferible preprocesar la lógica fuera del AI Agent, manteniendo el prompt limpio, enfocado y más efectivo. ### Qué evitar Evita escribir prompts como este: > Si tienes datos en esta variable `{{$memory.variable}}`, entonces haz tal cosa, de lo contrario, sigue estas otras instrucciones. Este tipo de lógica agrega ambigüedad y ruido al prompt, lo cual puede dificultar la comprensión del modelo y disminuir la precisión de la respuesta. ### Alternativa recomendada Usa lógica condicional previa en un nodo de tipo código para definir qué instrucciones debe seguir la IA antes de enviarle el prompt. Ejemplo: ```javascript theme={null} let promptCondicional = "Haz tal cosa primero"; if($memory.get("variable")){ promptCondicional = "Mejor haz esto"; } $memory.set("promptCondicional", promptCondicional); ``` Luego, en tu AI Agent, simplemente haz referencia a la variable procesada: ```text theme={null} Sigue estas instrucciones: {{$memory.promptCondicional}} ``` ### Beneficios * Reduce la carga cognitiva del modelo * Disminuye el uso de tokens innecesarios * Aumenta la claridad del prompt * Mejora la consistencia de las respuestas Este enfoque ayuda a diseñar agentes más confiables y predecibles, especialmente cuando se combinan múltiples fuentes de entrada o condiciones dinámicas. > Para consideraciones sobre modelos (GPT-4.1, contextos largos, CoT y recordatorios de agente), consulta la `Guía general de AI Agents`. ## Herramienta de Validación de Prompts Para facilitar la creación de prompts efectivos, puedes usar una herramienta en beta que actúa como asistente de pruebas. Si tu prompt no está funcionando como esperas o necesitas una guía rápida para comenzar, simplemente accede al siguiente enlace, escribe tu prompt u objetivo y sigue las sugerencias del asistente: [ChatGPT - Corrector de Prompts AI Agents](https://chatgpt.com/g/g-68127b80d72481918eca6f5b97bea57e-corrector-de-prompts-ai-agents) Esta herramienta está en fase beta, por lo que aún se encuentra en desarrollo. Sin embargo, es útil para detectar errores comunes, refinar instrucciones y validar si el prompt está bien estructurado antes de implementarlo en Brain Studio. ## Recursos adicionales * [OpenAI Platform](https://platform.openai.com/playground) - Explora recursos de desarrollador, tutoriales, documentación de API y ejemplos dinámicos * [AI Playground](https://ai-sdk.dev/playground) - Compara modelos de IA lado a lado: OpenAI GPT, Anthropic Claude, Google Gemini, Llama, Mistral y más # Seguridad y Guardrails Source: https://docs.jelou.ai/guides/agentes-ia/seguridad Guía sobre guardrails y configuración de seguridad para AI Agents ## Guardrails en el prompt Los guardrails mantienen al AI Agent dentro de límites seguros, predecibles y útiles. Deben definirse y utilizarse como un bloque único y coherente dentro de la caja de instrucciones, considerando los siguientes aspectos: **Identidad y alcance:** Define expícitamente el rol y lo que soporta. El agente debe comprender con precisión el dominio para el que fue diseñado y conocer las instrucciones a seguir ante cualquier solicitud fuera de ese alcance, por ejemplo, rechazándola de forma cortés y redirigiendo la interacción hacia los temas que sí soporta. **Anclaje al contexto:** Utilizar Knowledge como fuente principal. Asegurarse de seleccionar los documentos necesarios para el agente y proporcionar el contexto adecuado en el prompt. Indicar claramente qué documento debe usarse en cada parte, explicitar el uso exclusivo de la función de knowledge `search` y especificar que el agente debe responder únicamente con la información presente en el contexto, o devuelta de dicha función. Se debe definir el camino o la acción a seguir en caso de que la información no esté disponible. **Protección de datos:** Indicar explícitamente qué tipos de datos sensibles no deben ser expuestos, de acuerdo con las políticas y limitaciones de cada agente. Incluir ejemplos claros como: identificadores internos, tokens, credenciales y datos personales (PII) de terceros. Asimismo, definir el comportamiento esperado del agente en caso de detectar o recibir solicitudes relacionadas con datos sensibles, estableciendo una salida segura (por ejemplo, rechazo de la solicitud, anonimización o redirección). **Manejo de errores:** Definir explícitamente los pasos que el agente debe seguir y el tipo de respuesta que debe proporcionar cuando una tool falle, devuelva resultados incompletos o no se comporte como se esperaba. Incluir ejemplos de mensajes de error, criterios para reintentos y alternativas o acciones de contingencia cuando corresponda. ### Ejemplo ### Seguridad Responde únicamente sobre configuración de chatbots en Jelou; si te preguntan algo fuera de ese alcance, dilo cortésmente y cambia la conversación al tema soportado. Responde solo con base en la respuesta de tu función `search` y/o la base de conocimientos; si la respuesta no está en el contexto, di: "No tengo esa información." No reveles IDs internos, tokens, credenciales ni PII de terceros; si te solicitan algo sensible, recházalo o redáctalo. Si una herramienta falla o expira, discúlpate, explica brevemente lo ocurrido y ofrece escalar a un agente humano; no inventes resultados ni hagas reintentos silenciosos. ## Configuración de seguridad Además de los guardrails que defines en el prompt, puedes activar una **capa de protección automática** directamente desde la configuración avanzada del nodo AI Agent. Este sistema de seguridad **filtra contenido, detecta amenazas y protege contra intentos de manipulación**, sin que tengas que escribir una sola línea adicional en tus instrucciones. Para activarla, ve a la pestaña de **configuración avanzada** del nodo y habilita el interruptor de **Habilitar Seguridad**. Interruptor para habilitar seguridad y selector de nivel de seguridad en la configuración del AI Agent ### Nivel de seguridad Una vez habilitada la seguridad, selecciona el nivel que mejor se ajuste a tu caso de uso. Te recomendamos **comenzar con el nivel Bajo** e ir subiendo progresivamente a medida que identifiques las necesidades reales de tu flujo. | Nivel | Qué incluye | Caso de uso | | ----------- | -------------------------------------------------------------------- | ------------------------ | | **Bajo** | Validación básica de entradas, filtrado ligero de contenido | Pruebas, flujos internos | | **Medio** | + Detección de inyección de prompts, protección PII, auditoría | Producción general | | **Alto** | + Sensibilidad aumentada, moderación estricta, validación avanzada | Datos sensibles | | **Crítico** | + Bloqueo de amenazas medias, prevención de fuga de datos, sin caché | Financiero, regulado | **Protección básica para comenzar.** Ideal para flujos internos o de prueba donde el riesgo es mínimo. Activa validación básica de entradas y filtrado ligero de contenido. Protege contra inyección de prompts y jailbreak, pero no habilita protección avanzada ni auditoría. Es un buen punto de partida para familiarizarte con la funcionalidad sin impactar el rendimiento. **Cuándo usarlo:** flujos internos, entornos de desarrollo, pruebas iniciales o agentes con un público controlado. **El balance ideal para producción.** Incluye todo lo del nivel Bajo y agrega detección de inyección de prompts más robusta, sanitización de respuestas, protección del prompt del sistema y registro de auditoría. También habilita la **protección avanzada de datos personales (PII)**, reemplazando automáticamente información sensible antes de enviarla al modelo. **Cuándo usarlo:** flujos en producción con usuarios reales, agentes de atención al cliente, consultas generales. **Protección reforzada para datos delicados.** Aumenta la sensibilidad de detección de amenazas, aplica moderación de contenido más estricta y activa validación avanzada de entradas. La detección de datos personales opera con **mayor precisión**, reduciendo la probabilidad de que información sensible pase sin ser detectada. **Cuándo usarlo:** agentes que manejan datos personales, flujos de cobranza, consultas médicas o cualquier escenario donde la exposición de datos tenga consecuencias significativas. **Máxima seguridad, sin excepciones.** Activa todas las funciones de protección disponibles: **prevención de fuga de datos, detección exhaustiva de contenido sensible y auditoría completa** de cada interacción. Las amenazas de gravedad alta y media se bloquean automáticamente. El caché de seguridad se desactiva para garantizar que cada mensaje se analice de forma independiente. **Cuándo usarlo:** flujos financieros, datos médicos confidenciales, información regulada o cualquier escenario donde una fuga de datos podría tener consecuencias legales o regulatorias. Comienza con el nivel **Bajo** y ve subiendo según lo que necesites. No es necesario saltar directamente al nivel más alto; cada nivel agrega protecciones sobre el anterior, así puedes ajustar la seguridad de forma gradual. ## Protección avanzada Al habilitar la seguridad, se activa una capa de protección que trabaja en **dos momentos** de cada conversación: analiza los mensajes entrantes del usuario **antes** de enviarlos al modelo de IA, y revisa las respuestas del agente **antes** de entregarlas al usuario. De esta forma, se cubre tanto lo que entra como lo que sale. ### Qué detecta * **Inyección de prompts:** intentos de manipular al agente para que ignore sus instrucciones o se comporte de forma no deseada. * **Jailbreak:** técnicas para evadir las restricciones de seguridad del modelo. * **Contenido dañino:** filtro de contenido responsable (violencia, discurso de odio, contenido sexual, etc.). * **URLs maliciosas:** enlaces a sitios conocidos como peligrosos. * **Fuga de datos sensibles (PII):** detección automática de información personal como correos electrónicos, números de teléfono, tarjetas de crédito, documentos de identidad y más. * **Fuga del prompt del sistema:** intentos de extraer las instrucciones internas del agente. ### Protección de datos personales Cuando se detecta información personal en un mensaje, se reemplaza automáticamente por **marcadores seguros** (por ejemplo, `[EMAIL_ADDRESS]` o `[PHONE_NUMBER]`) antes de enviarlo al modelo de IA. Esto significa que el modelo **nunca ve los datos reales** del usuario. Si tu flujo necesita enviar esos datos reales a **herramientas externas** (como una API de consulta o un sistema de pagos), la plataforma puede restaurar los valores originales de forma segura únicamente para esas herramientas, sin exponerlos en la conversación. ### Cómo responde ante amenazas Dependiendo del nivel de seguridad configurado y la gravedad de la amenaza detectada, el sistema puede: * **Bloquear** la solicitud y mostrar un mensaje de error al usuario. * **Sanitizar** el contenido eliminando las partes problemáticas y dejando pasar el resto. * **Registrar** el evento en el log de auditoría para revisión posterior. En el nivel **Crítico**, las amenazas de gravedad alta y media se bloquean automáticamente. En los niveles **Medio** y **Alto**, solo las amenazas de gravedad alta se bloquean; las demás se sanitizan y el flujo continúa. ## Buenas prácticas * **Combina ambas capas:** escribe guardrails claros en el prompt **y** habilita la configuración de seguridad. Los guardrails definen el comportamiento esperado del agente; la protección automática cubre amenazas que un prompt por sí solo no puede cubrir. * **Comienza con el nivel Bajo** y sube progresivamente. Esto te permite entender las protecciones de cada nivel sin afectar el rendimiento. Si manejas datos financieros, médicos o altamente sensibles, considera el nivel **Alto** o **Crítico**. * **Define qué hacer cuando algo falla.** La seguridad protege contra amenazas, pero el agente necesita saber cómo responder ante errores inesperados. * **Revisa los logs de auditoría** periódicamente para identificar patrones de amenazas y ajustar tus instrucciones o nivel de seguridad si es necesario. # Tokens Source: https://docs.jelou.ai/guides/agentes-ia/tokens Entiende qué son los tokens, cómo influyen en el consumo de IA y cómo optimizar prompts y flujos en Brain Studio. Los tokens son la **unidad de "combustible"** que usa un modelo para procesar texto. Cada mensaje que entra, cada instrucción del agente, cada variable que incluyes como contexto y cada respuesta que sale se mide (internamente) en tokens. Piénsalo como gasolina: si tu flujo "pesa" más o viaja más lejos, **tiende** a consumir más. > Nota: entender tokens te ayuda a diseñar agentes más rápidos, estables y baratos de operar a escala.\ > Ver: [Precios](/guides/billing/precios) ## Qué es un token Un token es una unidad de texto que el modelo procesa. No equivale 1:1 a "una palabra": puede ser una palabra completa o un fragmento. En una ejecución típica, consumes tokens por: * **Entrada**: el mensaje del usuario + la **instrucción** del agente + el contexto que inyectas. * **Salida**: la respuesta del modelo. * **Herramientas y nodos**: por ejemplo, cuando un nodo trae datos (como un JSON) y esos datos se usan como contexto. ## La regla mental: tokens = gasolina ### 1) Más peso, más consumo Si tu agente carga demasiadas instrucciones, ejemplos, reglas redundantes o datos innecesarios, el prompt "pesa" más. **Causas comunes:** * Escribes instrucciones largas o repetidas. * Incluyes demasiado historial o haces "copypaste" de información en el contexto. * Recibes respuestas de herramientas con payloads grandes. Guías relacionadas: * [Prompting](/guides/agentes-ia/prompting) * [Guía general (Agentes IA)](/guides/agentes-ia/index) ### 2) Mientras más lejos viajas, más gastas Las conversaciones con muchos turnos tienden a acumular contexto útil… y a veces también ruido. Si tu flujo depende de historial largo, considera: * Resumir o normalizar información clave. * Guardar solo lo que necesitas para el siguiente paso (no todo el chat). Guías relacionadas: * [Variables - Guía rápida](/guides/variables/guia-rapida) * [Memory](/guides/variables/memory) * [Contexto](/guides/variables/context) ### 3) El modelo también importa Los modelos más "grandes" o con más capacidades suelen ser más costosos de ejecutar. En general: * Usa un modelo especializado o liviano para tareas simples (routing, validaciones, extracción). * Reserva un modelo más capaz para razonamiento complejo o generación más rica. ## Qué suele disparar el consumo en un flujo * **Prompts extensos** (sobre todo si incluyes texto repetido). * **Integraciones/API** que retornan demasiado contenido (catálogos enormes, logs, JSONs sin filtrar). * **Variables persistidas sin criterio** que vuelves a incluir en cada paso. * **Respuestas largas** cuando el usuario solo necesita una salida corta/estructurada. Relacionado: * [Nodo API](/guides/nodos/api) * [Nodo Variable](/guides/nodos/variable) * [AI Task](/guides/nodos/ai-task) ## Señales de alerta Si notas alguno de estos síntomas, probablemente estés consumiendo más tokens de los necesarios: | Síntoma | Causa probable | Solución | | ------------------------------- | ---------------------------- | ------------------------------------------ | | Latencia alta (>5s) | Contexto muy grande | Reduce historial, filtra datos | | Respuestas cortadas | Límite de salida alcanzado | Pide respuestas más cortas o estructuradas | | Error "context length exceeded" | Entrada supera el límite | Reduce instrucciones o contexto | | Respuestas inconsistentes | Demasiado ruido en el prompt | Limpia y prioriza información relevante | | Costos inesperados | Tokens de herramientas/API | Filtra respuestas de integraciones | ## Ejemplo: antes vs después ```text theme={null} Eres un asistente virtual de atención al cliente para la empresa XYZ Corporation S.A. de C.V. que fue fundada en 1985 y tiene presencia en 15 países de Latinoamérica. Tu objetivo principal es ayudar a los clientes con sus consultas de manera amable, profesional y eficiente. Siempre debes ser cortés y empático. Recuerda que el cliente siempre tiene la razón y debes tratarlo con respeto. Si no sabes algo, admítelo honestamente. Aquí está el historial completo de la conversación: [500 líneas de chat previo] Aquí está el catálogo completo de productos: [2000 productos con todos sus atributos] Responde al cliente de forma detallada y completa. ``` **Problema**: \~15,000+ tokens solo de entrada. ```text theme={null} Asistente de soporte de XYZ. Responde en español, máximo 2 párrafos. Contexto del cliente: - Nombre: {{memory.nombre}} - Último pedido: {{memory.ultimo_pedido}} Productos relevantes (filtrados): {{context.productos_filtrados}} Pregunta: {{input.message}} ``` **Resultado**: \~200-400 tokens. Mismo resultado, 50x más eficiente. Usa el [Tokenizer de OpenAI](https://platform.openai.com/tokenizer) para analizar tus prompts y encontrar oportunidades de optimización. ## Buenas prácticas para optimizar (sin perder calidad) ### Haz prompts "delgados" Un buen prompt suele ser: * Breve * Con reglas claras * Sin ejemplos innecesarios * Con formato de salida explícito (si aplica) Empieza por acá: * [Prompting](/guides/agentes-ia/prompting) ### Decide dónde vive cada dato: Context vs Memory No todo debe persistir. * **Context**: vive solo durante la ejecución actual. Úsalo para cálculos temporales y pasos intermedios.\ Ver: [Contexto](/guides/variables/context) * **Memory**: persiste entre conversaciones/skills con control de tiempo de vida (TTL). Úsalo para datos que realmente reutilizas (por ejemplo, preferencias o identificadores).\ Ver: [Memory](/guides/variables/memory) ### Limita la información que traes de herramientas Si usas API, filtra desde origen: * Pide solo los campos necesarios. * Pagina resultados. * Evita traer blobs o catálogos completos "por si acaso". Relacionado: * [Nodo API](/guides/nodos/api) ## Tokens y facturación en Jelou Conoce más sobre costos en: * [Cómo funciona](/guides/billing/introduccion) * [Precios](/guides/billing/precios) * [Devoluciones y reintentos](/guides/billing/reembolsos) Aunque no pagues "por token" directamente en todos los casos, optimizar tokens sigue siendo valioso porque reduces fricción operativa: latencia, ruido en contexto, respuestas inconsistentes y costo total de infraestructura cuando escalas. ## Idea guía Cada token debería tener trabajo real que hacer. Tokens decorativos = gasolina evaporándose. # Tools nativas Source: https://docs.jelou.ai/guides/agentes-ia/tools-nativas Herramientas nativas disponibles en el nodo AI Agent para consultar productos, manipular fechas, enviar mensajes interactivos y transferir conversaciones Las tools nativas son herramientas predefinidas e integradas en la plataforma que permiten al modelo de IA realizar acciones específicas durante una conversación. Estas herramientas extienden las capacidades del agente de IA más allá de la generación de texto, permitiéndole interactuar con el sistema, consultar información, manipular datos y ejecutar acciones concretas. Las tools nativas se encuentran y configuran en el nodo **AI Agent**. Para acceder a ellas: 1. Selecciona el nodo AI Agent en tu flujo 2. Abre la sección **TOOLS** en la configuración del nodo 3. Haz clic en el botón **"+ Agregar tools"** 4. En el campo "Selecciona un Tool", verás la lista de todas las tools disponibles, incluyendo las nativas (marcadas con el texto "(Nativa)") Configuración de tools nativas en el nodo AI Agent A continuación se describen todas las herramientas nativas disponibles en el nodo AI Agent: ## Búsqueda de productos Busca productos en el catálogo disponible para recomendar al usuario. Utiliza un motor de búsqueda para encontrar coincidencias basadas en el nombre del producto o términos relacionados. **Parámetros:** | Nombre | Tipo | Obligatorio | Descripción | | :-------------------- | :------: | :---------: | :--------------------------------------------------------------------------------- | | `product` | `string` | **Sí** | Término de búsqueda o nombre del producto. | | `top_k` | `number` | No | Cantidad máxima de resultados a retornar. | | `score_threshold` | `number` | No | Puntuación mínima de similitud (0.0 a 1.0) para considerar un resultado relevante. | | `client_reference_id` | `string` | No | ID de referencia del cliente para búsquedas personalizadas. | **Ejemplo de Prompt:** > "Si el usuario pregunta por la disponibilidad de algún artículo o modelo específico, DEBES usar la herramienta `search_products` para consultar el catálogo antes de responder." **Ejemplo de Argumentos:** ```json theme={null} { "product": "iPhone 15 Pro", "top_k": 5, "score_threshold": 0.85 } ``` **Respuesta:** Retorna un listado de productos encontrados o un mensaje indicando que no se encontraron resultados. En caso de error, devuelve un mensaje de sistema descriptivo. **Notas:** * Requiere que el canal tenga un catálogo de productos configurado y accesible. *** ## Transferir a asesor Transfiere la conversación actual del usuario a un agente humano o a una cola de atención. Permite especificar estrategias de enrutamiento y prioridades. **Parámetros:** | Nombre | Tipo | Obligatorio | Descripción | | :---------------- | :------: | :---------------------: | :---------------------------------------------------------------------------- | | `assignment_type` | `enum` | No (Default: `direct`) | Método de asignación: `direct` (inmediato) o `queue` (cola de tickets). | | `assignment_by` | `enum` | No (Default: `shuffle`) | Estrategia de enrutamiento: `shuffle` (auto), `team`, `operators`, `general`. | | `teamId` | `number` | No\* | ID del equipo. \*Requerido solo si `assignment_by` es 'team'. | | `operatorId` | `number` | No\* | ID del operador. \*Requerido solo si `assignment_by` es 'operators'. | | `priority` | `number` | No (Default: 0) | Prioridad en la cola (0-10), solo relevante para `assignment_type='queue'`. | **Ejemplo de Prompt:** > "Cuando el usuario exprese frustración o solicite hablar explícitamente con una persona real, ejecuta la función `transfer_to_agent` inmediatamente." **Ejemplo de Argumentos:** *Caso: Transferencia directa automática (más común)* ```json theme={null} { "assignment_type": "direct", "assignment_by": "shuffle" } ``` *Caso: Transferencia directa a un equipo* ```json theme={null} { "assignment_type": "direct", "assignment_by": "team", "teamId": 4501 } ``` *Caso: Crear ticket en cola general* ```json theme={null} { "assignment_type": "queue", "assignment_by": "general", "priority": 5 } ``` **Respuesta:** Retorna un objeto JSON con el resultado de la operación indicando si la transferencia se inició correctamente. **Notas:** * Puede finalizar el flujo de IA inmediatamente. * Cuando se usa `assignment_type: "queue"` con `assignment_by: "general"`, se crea un ticket en la cola general de atención. * **Importante:** No se deben inventar IDs de equipos u operadores; se deben usar los configurados explícitamente. *** ## Enviar mensajes interactivos Envía un mensaje estructurado o interactivo al usuario. El formato exacto depende del canal (WhatsApp, Web, etc.), pero generalmente incluye botones o listas de opciones. **Parámetros:** | Nombre | Tipo | Obligatorio | Descripción | | :-------- | :------: | :---------: | :--------------------------------------------------------------------------------------------------------------------------------------------- | | `text` | `string` | **Sí** | Contenido principal del mensaje. Máximo 1024 caracteres. | | `title` | `string` | No | Cabecera/título del mensaje. Máximo 60 caracteres. | | `caption` | `string` | No | Subtítulo o descripción complementaria. Máximo 60 caracteres. En WhatsApp con 4+ opciones, se usa como texto del botón que despliega la lista. | | `options` | `array` | **Sí** | Lista de opciones/botones. Ver estructura abajo. | *Estructura de `options` (Array de objetos):* * `title` (string, obligatorio): Texto de la opción/botón. Máximo 20 caracteres. No se permiten títulos duplicados. * `description` (string, opcional): Descripción adicional de la opción. Máximo 72 caracteres. **Ejemplo de Prompt:** > "Si el usuario pregunta qué servicios ofrecemos DEBES usar `send_interactive_message` para mostrar un menú con las opciones disponibles." **Ejemplo de Argumentos (Botones - 3 o menos opciones):** ```json theme={null} { "text": "¿Cómo deseas continuar con tu consulta?", "title": "Opciones de Servicio", "caption": "Selecciona una opción", "options": [ { "title": "Hablar con Agente", "description": "Transferir a soporte humano" }, { "title": "Ver Catálogo", "description": "Explorar productos disponibles" } ] } ``` **Ejemplo de Argumentos (Lista WhatsApp - 4+ opciones):** ```json theme={null} { "text": "Tenemos varios departamentos disponibles para atenderte.", "title": "Departamentos", "caption": "Ver opciones", "options": [ { "title": "Ventas", "description": "Consultas de productos y precios" }, { "title": "Soporte Técnico", "description": "Ayuda con problemas técnicos" }, { "title": "Facturación", "description": "Consultas sobre pagos" }, { "title": "Devoluciones", "description": "Gestionar devoluciones" } ] } ``` En WhatsApp, cuando envías 4 o más opciones, el mensaje se muestra como una lista desplegable. El valor de `caption` se usa como texto del botón que abre la lista (en el ejemplo anterior, "Ver opciones"). **Respuesta:** Retorna `"Message sent successfully."` si el envío fue exitoso o un mensaje de error en caso contrario. **Notas:** * Envía un mensaje real al usuario. * El tipo de mensaje se adapta automáticamente según el canal y la cantidad de opciones. *** ## Enviar Call to Action (CTA) Envía un botón de llamada a la acción que abre una URL externa. Puede configurarse como WebView para formularios o pagos que requieren esperar una respuesta del usuario. **Parámetros:** | Nombre | Tipo | Obligatorio | Descripción | | :------------------ | :------------------: | :---------: | :------------------------------------------------------------------------------------------------------ | | `text` | `string` | **Sí** | Contenido principal del mensaje. | | `url` | `string` | **Sí** | URL que se abre al hacer clic en el botón. | | `display_text` | `string` | **Sí** | Texto del botón CTA. Máximo 20 caracteres. | | `title` | `string` | No | Cabecera/título del mensaje. | | `caption` | `string` | No | Subtítulo para complementar el mensaje. | | `is_webview` | `boolean` | No | Si es `true`, envía como WebView que pausa el flujo y espera callback. Default: `false`. | | `expiration_time` | `number` | No | Segundos para esperar el callback antes de expirar. Default: 300. Solo aplica cuando `is_webview=true`. | | `pending_message` | `string` | No | Mensaje a mostrar si el usuario envía texto mientras se espera el callback del WebView. | | `response_variable` | `string` | No | Nombre de la variable donde guardar la respuesta del callback. | | `input` | `string` \| `object` | No | Datos para pasar al WebView como parámetros de URL. | **Ejemplo de Prompt:** > "Cuando el usuario confirme que desea realizar el pago, usa `send_call_to_action` con `is_webview=true` para enviar el enlace de pago y esperar la confirmación." **Ejemplo de Argumentos (CTA simple):** ```json theme={null} { "text": "Visita nuestra tienda online para ver todos los productos disponibles.", "url": "https://tienda.ejemplo.com/catalogo", "display_text": "Ver Catálogo", "title": "Tienda Online" } ``` **Ejemplo de Argumentos (WebView con espera de respuesta):** ```json theme={null} { "text": "Haz clic en el botón para completar tu pago de forma segura.", "url": "https://pagos.ejemplo.com/checkout", "display_text": "Pagar Ahora", "title": "Completar Pago", "is_webview": true, "expiration_time": 600, "pending_message": "Por favor, completa el pago en la ventana abierta antes de continuar.", "response_variable": "payment_result", "input": { "order_id": "12345", "amount": 99.99 } } ``` **Respuesta:** * CTA normal: `"Call-to-action message sent successfully."` * WebView: `"WebView message sent. Waiting for user action."` **Notas:** * Disponible solo para los proveedores de WhatsApp que soportan este tipo de mensajes. * Cuando `is_webview=true`, el flujo se pausa hasta que el WebView devuelve un callback o expira el tiempo. * El valor de `input` se convierte a parámetros de URL query string. * La respuesta del callback se guarda en la variable especificada en `response_variable`. *** ## Fecha y hora actual Obtiene la fecha y hora actual. Permite especificar una zona horaria para obtener la hora local correcta. **Parámetros:** | Nombre | Tipo | Obligatorio | Descripción | | :--------- | :------: | :---------: | :--------------------------------------------------------------------- | | `timezone` | `string` | No | Zona horaria deseada en formato IANA (ej: "America/Guayaquil", "UTC"). | **Ejemplo de Prompt:** > "Antes de procesar cualquier solicitud que dependa de la hora (como 'buenos días' o citas para 'hoy'), llama a `get_current_date_time` para saber la hora exacta." **Ejemplo de Argumentos:** ```json theme={null} { "timezone": "America/Mexico_City" } ``` **Respuesta:** Retorna un string con la fecha y hora formateada, por ejemplo: `"Monday, December 08, 2025 10:30 AM"`. **Notas:** * No tiene efectos secundarios visibles para el usuario. * Es puramente informativa para el modelo. *** ## Día de la semana Calcula y retorna el día de la semana correspondiente a una fecha específica proporcionada. **Parámetros:** | Nombre | Tipo | Obligatorio | Descripción | | :----- | :------: | :---------: | :-------------------------------------------------------------------- | | `date` | `string` | **Sí** | Fecha a consultar (formato ISO o legible standard, ej: "2024-12-25"). | **Ejemplo de Prompt:** > "Si el usuario quiere agendar para una fecha específica, verifica primero qué día de la semana es usando `get_weekday_of_a_date` para asegurar que sea un día hábil." **Ejemplo de Argumentos:** ```json theme={null} { "date": "2025-12-10" } ``` **Respuesta:** Retorna el nombre del día de la semana, por ejemplo: `"Wednesday"`. **Notas:** * Útil para validar citas o agendas. *** ## Gestión de memoria Las siguientes tools se activan desde la sección **Gestión de memoria** de la pestaña Contexto del nodo AI Agent. Dan al agente memoria de trabajo, acceso al CRM de la bandeja de entrada, variables del contexto del flujo e información de plataforma. Consulta la [sección Gestión de memoria del nodo AI Agent](/guides/nodos/ai-agent#gestión-de-memoria) para ver cómo activarlas y configurar sus permisos. ## Memoria interna Permite al agente guardar, recordar y eliminar información de la conversación para reutilizarla más adelante. Es memoria de trabajo por usuario, con vigencia limitada: recuerda datos del mismo usuario entre conversaciones, pero no es un almacén permanente. **Operaciones:** | Operación | Acción | Descripción | | :----------- | :------- | :---------------------------------------- | | **Crear** | `save` | Guarda un dato en la memoria del usuario. | | **Buscar** | `recall` | Recupera datos previamente guardados. | | **Eliminar** | `delete` | Borra un dato guardado. | | **Listar** | `list` | Lista los datos guardados disponibles. | **Notas:** * Incluye un campo opcional **Espacio de memoria** para aislar o agrupar los guardados, evitando que distintos contextos se mezclen. * Cada operación admite configurar sus parámetros en modo IA, variable o estático, y un comportamiento post-ejecución (Continuar, Terminar o Pausar). *** ## CRM de bandeja de entrada Lee, define y actualiza datos del contacto del CRM (nombre, teléfono, etiquetas, notas y más) directamente desde la bandeja de entrada. **Operaciones:** | Operación | Acción | Descripción | | :------------------- | :------------------- | :--------------------------------------- | | **Leer** | `readStoredParams` | Lee los datos almacenados del contacto. | | **Definir** | `setStoredParams` | Establece datos del contacto. | | **Actualizar** | `updateStoredParams` | Actualiza datos existentes del contacto. | | **Listar etiquetas** | `listTags` | Lista las etiquetas del contacto. | | **Agregar etiqueta** | `addTag` | Agrega una etiqueta al contacto. | | **Quitar etiqueta** | `removeTag` | Quita una etiqueta del contacto. | **Notas:** * Los permisos se administran por grupo (datos del contacto / etiquetas) con lectura y escritura separadas. Puedes restringir a qué etiquetas puede acceder el agente. * Requiere al menos un canal conectado para que existan contactos sobre los cuales operar. *** ## \$context Lee y escribe variables del contexto del flujo para compartir datos entre nodos. **Operaciones:** | Operación | Acción | Descripción | | :------------- | :----- | :--------------------------------------------- | | **Obtener** | `get` | Lee el valor de una variable del contexto. | | **Establecer** | `set` | Escribe o actualiza una variable del contexto. | **Notas:** * En la lista de tools aparece como **\$context** para distinguirla de la pestaña Contexto. * Las variables escritas quedan disponibles para nodos posteriores del flujo, por ejemplo `{{$context.mi_variable}}`. *** ## Información de plataforma Bloques de contexto de **solo lectura** que inyectan datos de plataforma en el prompt del agente. No consumen una llamada de tool: la información se agrega automáticamente como contexto cuando la fuente está activa. | Fuente | Campos | Descripción | | :---------------------------- | :---------------------------------------- | :------------------------- | | **Información del usuario** | `names`, `referenceId`, `botId`, `roomId` | Datos del usuario actual. | | **Información del canal** | `name`, `type` | Datos del canal conectado. | | **Información de la empresa** | `name`, `plan`, `organizationId` | Datos de la empresa. | **Notas:** * Las tres fuentes son de solo lectura; el agente no puede modificarlas. * Se inyectan como bloques de contexto, por lo que no cuentan como una invocación de herramienta. *** ## Bases de datos Conexión nativa a **Bases de datos (Datum v2)** desde la pestaña **Tools** del nodo AI Agent. Permite que el agente busque, cree, actualice y elimine registros en tus colecciones sin encadenar nodos auxiliares. **Operaciones:** buscar, crear, actualizar y eliminar registros sobre las colecciones que habilites. | Parámetro | Tipo | Obligatorio | Descripción | | :------------- | :------: | :---------: | :----------------------------------------------------------------------- | | `operation` | `enum` | **Sí** | `search`, `create`, `update` o `delete`. | | `collectionId` | `string` | **Sí** | Colección de Bases de datos sobre la que se opera. | | `query` | `object` | No\* | Filtros de búsqueda. \*Requerido para `search`. | | `document` | `object` | No\* | Documento a insertar o actualizar. \*Requerido para `create` y `update`. | | `recordId` | `string` | No\* | ID del registro. \*Requerido para `update` y `delete`. | **Notas:** * El agente solo puede operar sobre las colecciones de la **misma compañía** que habilites explícitamente en el popover **Configurar**. * Las operaciones de escritura persisten de inmediato. * El comportamiento post-ejecución (Continuar, Terminar o Pausar) se configura por operación. *** ## AI Routing Envía la solicitud del usuario al workflow que puede atenderla, cuando pide algo que este agente no puede hacer. Usa el [AI Routing](/guides/getting-started/ai-routing) del proyecto para elegir el destino, por lo que el proyecto debe tener AI Routing habilitado. **Parámetros:** | Nombre | Tipo | Obligatorio | Descripción | | :-------- | :------: | :----------------------: | :------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | `enum` | No (Default: `evaluate`) | `evaluate` (elige el destino automáticamente), `direct` (destino indicado por el usuario) o `search` (lista los destinos disponibles). | | `message` | `string` | No\* | Mensaje del usuario tal cual. \*Requerido para `evaluate`. | | `target` | `string` | No\* | Nombre o ID del destino. \*Requerido para `direct`. | **Ejemplo de Prompt:** A diferencia de otras tools, `ai_router` no necesita que le indiques cuándo usarla: al activarla se agregan automáticamente las reglas de derivación a las instrucciones del agente. Lo único que tienes que definir es **el alcance del agente**, porque todo lo que quede fuera de ahí es lo que se deriva. > "Atiendes únicamente consultas de facturación: estados de cuenta, fechas de pago y detalle de cargos." **Ejemplo de Argumentos:** *Caso: el usuario pregunta algo fuera del alcance del agente (más común)* ```json theme={null} { "mode": "evaluate", "message": "quiero reportar un problema con mi pedido" } ``` *Caso: el usuario nombra explícitamente el destino* ```json theme={null} { "mode": "direct", "target": "Soporte" } ``` *Caso: consultar los destinos disponibles antes de decidir* ```json theme={null} { "mode": "search" } ``` **Respuesta:** Retorna un objeto JSON con el destino elegido, si la derivación se ejecutó y una guía para el modelo sobre qué hacer a continuación. En `search` y en `direct` sin coincidencia, retorna los destinos disponibles y **no** deriva nada. **Notas:** * Una derivación exitosa **termina el turno del agente**: la respuesta al usuario llega desde el workflow de destino. El agente no debe responder también. * Si no hay coincidencia o la derivación falla, el agente sigue a cargo de la conversación y debe continuar atendiendo al usuario. * Requiere que el proyecto tenga **AI Routing** habilitado y workflows con nombre y descripción configurados. El comportamiento posterior a la ejecución es fijo: la derivación siempre termina el turno del agente. * El agente solo puede derivar a workflows del mismo proyecto. Las reglas de derivación se agregan solas al activar la tool: el agente ya sabe que debe resolver por su cuenta lo que sus instrucciones cubren, que no debe derivar porque otra tool falló o porque no está seguro, y que no debe responder además de derivar. No necesitas escribir nada de esto en el prompt. # API Keys Source: https://docs.jelou.ai/guides/datum/api-keys Crea y gestiona claves de API para acceder a tus datos de Databases desde aplicaciones externas Las **API Keys** te permiten autenticarte en la API de Databases desde aplicaciones externas, scripts o integraciones. Cada clave puede tener permisos granulares y una fecha de expiración configurable. Tabla de API Keys mostrando claves creadas con columnas Name, Permissions, Key, Created, Expires y botones Regenerate y Delete ## Crear una API Key Ve a **Configuración > API Keys** desde la barra lateral izquierda. El botón se encuentra en la esquina superior derecha. Completa los siguientes campos: * **Name** — Un nombre descriptivo para identificar la clave (ej. "CI Channel", "Backend Production") * **Permissions** — Selecciona los permisos que necesita la clave * **Expiration** — Tiempo de vigencia de la clave La clave se genera y se muestra una única vez. Cópiala y guárdala en un lugar seguro. La clave completa solo se muestra una vez al momento de crearla. Si la pierdes, deberás regenerar la clave o crear una nueva. ## Permisos disponibles Los permisos se dividen en dos categorías: **Records (Registros)** | Permiso | Descripción | | ----------------------- | ------------------------------------------------------ | | Read records | Permite leer y buscar registros en las colecciones | | Create & update records | Permite crear nuevos registros y actualizar existentes | | Delete records | Permite eliminar registros | **Files (Archivos)** | Permiso | Descripción | | ------------ | -------------------------------------- | | Read files | Permite descargar archivos adjuntos | | Upload files | Permite subir archivos a los registros | Aplica el principio de mínimo privilegio: otorga solo los permisos que la aplicación realmente necesita. Por ejemplo, un dashboard de lectura solo requiere **Read records**. ## Opciones de expiración | Opción | Descripción | | ------------- | ---------------------------------------- | | 7 days | La clave expira en 7 días | | 30 days | La clave expira en 30 días (por defecto) | | 60 days | La clave expira en 60 días | | 90 days | La clave expira en 90 días | | 180 days | La clave expira en 180 días | | No expiration | La clave no expira | Las claves sin expiración no se revocan automáticamente. Úsalas solo cuando sea estrictamente necesario y combínalas con permisos mínimos. ## Gestionar API Keys La tabla de API Keys muestra todas las claves creadas con la siguiente información: * **Name** — Nombre de la clave * **Permissions** — Permisos asignados * **Key** — Los últimos caracteres de la clave (enmascarada) * **Created** — Fecha de creación * **Expires** — Fecha de expiración ### Acciones disponibles * **Regenerate** — Genera una nueva clave conservando el mismo nombre, los mismos permisos **y la fecha de expiración original** (no se reinicia el periodo). La clave anterior deja de funcionar inmediatamente. * **Delete** — Elimina la clave permanentemente. Cualquier aplicación que la utilice perderá acceso inmediatamente. ## Usar la API Key Incluye la clave en el header `X-Api-Key` de tus peticiones HTTP: ```bash cURL theme={null} curl -s -X GET 'https://tu-instancia.jelou.cloud/api/collections/tu_coleccion/records' \ -H 'X-Api-Key: YOUR_API_KEY' \ -H 'Accept: application/json' ``` ```javascript JavaScript theme={null} const response = await fetch('https://tu-instancia.jelou.cloud/api/collections/tu_coleccion/records', { headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Accept': 'application/json' } }); const data = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( 'https://tu-instancia.jelou.cloud/api/collections/tu_coleccion/records', headers={ 'X-Api-Key': 'YOUR_API_KEY', 'Accept': 'application/json' } ) data = response.json() ``` # Colecciones Source: https://docs.jelou.ai/guides/datum/colecciones Crea y configura colecciones para organizar tus datos con campos tipados, validaciones e índices Una **colección** es el equivalente a una tabla en una base de datos tradicional. Cada colección tiene campos (columnas) con tipos de datos específicos y almacena registros (filas) que puedes crear, buscar, editar y eliminar. ## Crear una colección Diálogo de Create Collection con nombre, tipo Base, campos id/created/updated y opción de crear índices En la barra lateral izquierda, haz clic en el botón **+ New collection** ubicado en la parte inferior del panel de colecciones. Completa los siguientes campos: * **Name** — Nombre de la colección (obligatorio, ej. "clientes", "productos") * **Type** — Tipo de colección: **Base** (por defecto) o **Vista (solo lectura)**. Toda colección nueva incluye tres campos automáticos: * **id** — Identificador único del registro (generado automáticamente) * **created** — Fecha de creación del registro (Autodate) * **updated** — Fecha de última actualización (Autodate) Haz clic en **+ New field** para agregar tus propios campos. Haz clic en **Create** para confirmar. ## Tipos de campo Cada campo tiene un tipo que determina qué datos puede almacenar y qué validaciones se aplican: | Tipo | Descripción | | ------------ | --------------------------------------------------------------------------------------- | | **Text** | Cadenas de texto (máximo 5000 caracteres por defecto) | | **Editor** | Texto enriquecido con formato (negritas, listas, enlaces, etc.) | | **Number** | Valores numéricos (enteros o decimales) | | **Boolean** | Valores verdadero/falso | | **Email** | Direcciones de correo electrónico con validación de formato | | **URL** | URLs con validación de formato | | **Date** | Fecha y hora que el usuario define manualmente en cada registro | | **Autodate** | Fecha y hora asignada automáticamente por el sistema (ej. campos `created` y `updated`) | | **Select** | Selección de una opción de una lista predefinida | | **Relation** | Referencia a registros de otra colección | | **File** | Archivos adjuntos | | **JSON** | Datos estructurados en formato JSON | | **GeoPoint** | Coordenadas geográficas (latitud y longitud) | | **Password** | Texto enmascarado para almacenar contraseñas o secretos | ## Configurar campos Haz clic en el ícono de engranaje (⚙) de cualquier campo para ver sus opciones de configuración. Las opciones varían según el tipo de campo. Para campos de tipo **Text**: * **Min length / Max length** — Longitud mínima y máxima del texto (máximo 5000 caracteres por defecto) * **Validation pattern** — Expresión regular para validar el formato (ej. `^[a-z0-9]+$`) * **Nonempty** — Marca el campo como obligatorio * **Hidden** — Oculta el campo en la interfaz de usuario. * **Presentable** — Marca el campo como campo representativo del registro Puedes cambiar el tipo de un campo existente desde el dropdown del tipo en la configuración de la colección. Ten en cuenta que cambiar el tipo puede afectar datos existentes. ### Opciones por tipo de campo Cada tipo de campo ofrece sus propias opciones de configuración: **Solo enteros** (rechaza decimales) y valores mínimo/máximo. Lista de opciones permitidas (al menos una). Puedes habilitar **selección múltiple**, que requiere un mínimo de 2 opciones. Tamaño máximo por archivo, archivo único o múltiple, y tipos permitidos mediante presets (Imágenes, Audio, Video, Comprimidos, Documentos). Colección relacionada y selección única o múltiple. También puedes definir qué ocurre cuando se elimina un registro relacionado. Configura la longitud máxima y las opciones de seguridad para proteger los valores almacenados. Dominios permitidos o excluidos para validar el valor. **Date**: fecha mínima y máxima. **Autodate**: define cuándo se asigna automáticamente — al **Crear**, al **Actualizar** o en ambos. Tamaño máximo del contenido. ## Reordenar campos Haz clic en **Reorder fields** dentro de la configuración de la colección para cambiar el orden en que se muestran los campos en la tabla y en los formularios. ## Índices y restricciones únicas En la parte inferior de la configuración de la colección encontrarás la sección **Unique constraints and indexes**. Los índices mejoran el rendimiento de las consultas en campos que se filtran o buscan frecuentemente. Haz clic en **Create index** para agregar un nuevo índice a tu colección. ## Configuración de la colección Panel Collection settings mostrando nombre, tipo, lista de campos con sus tipos y opciones de configuración Haz clic en el ícono de engranaje (⚙) junto al nombre de la colección en la parte superior para acceder a la configuración completa. Desde aquí puedes: * Renombrar la colección * Agregar, editar o eliminar campos * Reordenar campos * Crear índices El menú **⋯** en la esquina superior derecha de la configuración ofrece acciones adicionales: * **Duplicar** — Crea una copia de la colección. * **Vaciar** — Elimina todos los registros y conserva la estructura de la colección. No está disponible en vistas. * **Eliminar** — Borra la colección por completo. **Vaciar** y **Eliminar** son irreversibles. Vaciar borra todos los registros; Eliminar borra además la colección y su estructura. # Importaciones Source: https://docs.jelou.ai/guides/datum/importaciones Importa datos masivos a tus colecciones desde archivos CSV o XLSX La función de importación te permite cargar datos masivos a tus colecciones desde archivos **CSV** o **XLSX** (Excel). Puedes importar datos a una colección existente o crear una nueva colección durante el proceso. Página de importaciones con botón New Import y estado vacío indicando que no hay importaciones ## Importar a una colección existente Haz clic en el ícono de importación en la barra lateral izquierda y luego en **+ New Import** para abrir el asistente. Asistente de importación mostrando paso 1 con opciones Use Existing y Create New, selector de colección y zona de carga de archivos El asistente tiene **5 pasos**: Asegúrate de que **Use Existing** esté seleccionado, elige la colección destino del dropdown y arrastra tu archivo CSV o XLSX a la zona de carga (o haz clic para seleccionarlo). Tamaño máximo de archivo: **500 MB**. Formatos soportados: **CSV** y **XLSX**. Asocia cada campo de la colección con una columna del archivo CSV. El asistente detecta coincidencias automáticamente (ej. un campo `email` se mapea solo). Para cada campo puedes seleccionar un **Subtype** (General Text, ID Number) y un **Country** cuando aplique. Las columnas del CSV que no mapees aparecerán en la sección **Unused CSV columns** y serán ignoradas. Opcionalmente activa **Enable duplicate detection** para evitar registros duplicados. Al activarla se muestran opciones adicionales: * **Campo único para coincidencia** — El campo que identifica un duplicado. * **Cuando se encuentra un duplicado** — Acción a tomar: **Omitir**, **Actualizar** o **Error**. * **Estrategia de actualización** (solo si eliges Actualizar) — **Combinar** (actualiza solo los campos mapeados y conserva el resto) o **Reemplazar** (sobrescribe todos los campos, excepto los del sistema). Revisa una previsualización de las primeras filas. Verás contadores de filas válidas, errores y duplicados. Haz clic en cualquier fila para ver el detalle del error. Revisa el resumen final con los detalles del archivo, la colección destino, los mapeos de columnas y los campos omitidos. Haz clic en **Start Import** para ejecutar la importación. ## Importar creando una nueva colección Este flujo tiene **6 pasos** (uno más que el de una colección existente, porque primero defines los campos): Haz clic en **+ New Import**, selecciona **Create New**, ingresa el nombre de la nueva colección y sube tu archivo CSV o XLSX. Databases inferirá los tipos de campo automáticamente a partir de las columnas del archivo. Puedes ajustar los tipos manualmente si es necesario. Revisa y ajusta la asociación entre las columnas del archivo y los campos que definiste. Para cada campo puedes elegir un **Subtype** (Texto general o Número de identificación) y un **Country** cuando aplique. Configura la detección de duplicados con las mismas opciones descritas en el flujo anterior. Revisa la previsualización de filas y verifica que los datos y tipos sean correctos. Revisa el resumen final y haz clic en **Start Import**. ## Historial de importaciones Todas las importaciones se muestran en la sección de Importaciones con su estado. Desde aquí puedes monitorear el progreso de las importaciones en curso y revisar el resultado de las anteriores. Además puedes: * **Cancelar** una importación en curso (mientras está en proceso o pendiente). * **Eliminar** cualquier importación del historial. * Descargar, desde el panel de detalle de una importación, el **Reporte de rendimiento** (importaciones completadas) y el **Reporte de errores** (cuando hubo filas fallidas), ambos en formato CSV. # Databases Source: https://docs.jelou.ai/guides/datum/introduccion Base de datos gestionada para crear y administrar colecciones de datos sin preocuparte por la infraestructura ## ¿Qué es Databases? **Databases** es la solución de base de datos gestionada de Jelou que te permite crear y administrar colecciones de datos directamente desde tu navegador. No necesitas configurar servidores, gestionar réplicas ni preocuparte por el autoescalamiento — todo está gestionado automáticamente. Con Databases puedes almacenar información estructurada (clientes, productos, pedidos, etc.) y conectarla con tus flujos de Brain Studio, APIs externas o herramientas de IA a través de MCP. Vista de la tabla de registros de una colección en Databases con columnas, búsqueda y acciones ## Acceso a Databases Puedes acceder a Databases de dos formas: 1. Desde el **dashboard principal** de Jelou, haz clic en **Databases** en la sección de acceso rápido 2. Desde la **barra lateral izquierda** de navegación ## Navegación principal La barra lateral izquierda te da acceso a las secciones principales: **Inicio**, **Colecciones**, **Importaciones**, **Monitoreo** y **Configuración**. Los **registros** se gestionan dentro de cada colección, no desde un icono independiente de la barra lateral. Crea y gestiona tus tablas de datos con campos tipados Crea, edita, busca, filtra y exporta registros dentro de una colección Importa datos masivos desde archivos CSV o XLSX Visualiza métricas de uso y logs de actividad API Keys, Triggers, MCP y ajustes generales ## Configuración general Página de configuración general de Databases con nombre de base de datos, operaciones en lote y zona horaria Desde **Configuración > General** puedes ajustar: * **Nombre de la base de datos** — El identificador de tu instancia de Databases * **Operaciones por lote** — Permite la selección y eliminación masiva de registros en colecciones * **Configuración regional** — Zona horaria utilizada para exportaciones y formato de fechas (por defecto UTC) La sección **Zona de peligro** (*Danger Zone*) reúne las acciones sensibles sobre la base de datos: * **Mejorar** — Sube a un plan de cómputo o almacenamiento mayor. No se permiten *downgrades*; la base debe estar activa y el volumen de datos se preserva. * **Reprovisionar** — Reconstruye la base de datos. Tus datos se preservan, pero la base queda fuera de línea unos minutos; verás una pantalla de progreso que se actualiza automáticamente. * **Eliminar** — Borra permanentemente la base de datos y todos sus datos. Eliminar la base de datos es irreversible: se pierden todas las colecciones y registros. ## Estados y ciclo de vida Tu base de datos muestra un estado en todo momento (por ejemplo, activa o en aprovisionamiento). Las bases inactivas entran en estado **Hibernating** (hibernación) para ahorrar recursos y se **reactivan automáticamente** cuando vuelves a usarlas; durante la reactivación verás una breve pantalla de espera. # MCP Source: https://docs.jelou.ai/guides/datum/mcp Conecta Databases con herramientas de IA como ChatGPT, Claude, Cursor y VS Code mediante el protocolo MCP **MCP** (Model Context Protocol) te permite conectar tu base de datos de Databases directamente con herramientas de IA. Una vez conectado, puedes consultar, crear y modificar registros usando lenguaje natural desde tu herramienta favorita. Página de configuración MCP con pestañas ChatGPT, Claude, Cursor, VS Code y Otro, mostrando la guía de instalación paso a paso ## ¿Cómo funciona? Databases crea una **URL MCP** para conectar tu base de datos con una herramienta de IA. A través de ella, la herramienta puede consultar y gestionar tus datos. Para acceder a la configuración MCP, ve a **Configuración > MCP** desde la barra lateral izquierda. ## Configuración por herramienta En ChatGPT, abre **Settings** y selecciona **Connectors** para gestionar conectores personalizados. Habilita **Developer Mode** en la sección de Connectors → Advanced settings para aceptar URLs de conectores personalizados. En la página de MCP de Databases, copia la URL generada. En ChatGPT Connectors: * Selecciona **Create connector** y agrega un nombre * Configura la autenticación como **No Authentication** * Pega la URL MCP y marca **I trust this application** * Haz clic en **Create** Abre un nuevo chat, ve a **More → Developer Mode** para activar el conector. Haz clic en **Add sources**, activa tu base de datos de Databases e interactúa con tus datos. En la página de MCP de Databases, copia la URL generada. En Claude Desktop, ve a **Agregar conectores** (*Add Connectors*). Entra en **Administrar los conectores** (*Manage connectors*). Selecciona **Agregar un conector personalizado** (*Add custom connector*). Asigna un nombre al conector, pega la **URL MCP** del paso 1 y haz clic en **Agregar**. Abre **Search and Tools** y activa el conector de Databases. **Instalación automática:** En la página de MCP de Databases, copia la URL generada. Haz clic en el botón **Add to Cursor** o copia el deeplink de Cursor proporcionado. Cuando Cursor se abra: * Revisa los detalles del conector y haz clic en **Install** * Permite que Cursor se reinicie si es necesario * Abre un nuevo chat y habilita las herramientas de Databases **Instalación manual:** Agrega este JSON en **Settings → Cursor Settings → MCP → Add new global MCP server**: ```json theme={null} { "datum": { "url": "https://datum.jelou.cloud/TU_ID_UNICO/mcp" } } ``` En la página de MCP de Databases, copia la URL generada. En tu workspace, crea el archivo `.vscode/mcp.json` con la configuración del servidor de Databases: ```json theme={null} { "servers": { "datum": { "type": "http", "url": "https://datum.jelou.cloud/TU_ID_UNICO/mcp" } } } ``` Usa el Command Palette para: * Ejecutar **MCP: Add Server**, elige **HTTP** y proporciona la URL de Databases * Selecciona **Workspace Settings** para crear `.vscode/mcp.json` si no existe * Usar **MCP: Show Installed Servers** para confirmar que el conector está disponible Evita incluir secretos directamente en el archivo de configuración. Usa variables de entorno o archivos de input para credenciales. En la página de MCP de Databases, copia la URL generada. Usa la URL generada con el método de transporte soportado por tu cliente. La mayoría de clientes soportan conexiones MCP basadas en HTTP. ## Qué puede hacer la IA conectada Una vez conectada, la IA puede consultar, crear, actualizar y eliminar registros, además de ayudar a gestionar colecciones e importar datos. La IA conectada puede modificar o eliminar datos. Conéctala solo con herramientas y personas de confianza. # Monitor Source: https://docs.jelou.ai/guides/datum/monitor Visualiza métricas de rendimiento y logs de actividad de tu base de datos La sección de **Monitor** te permite supervisar el estado y rendimiento de tu base de datos en tiempo real. Accede desde el ícono de Monitoreo en la barra lateral izquierda. Tiene dos pestañas, **Metrics** y **Logs**, y se abre en **Logs** por defecto. ## Métricas Pestaña Metrics del Monitor mostrando CPU Usage, Memory Usage, Storage Usage y gráficas de tendencia La pestaña **Metrics** muestra tres indicadores principales: | Métrica | Descripción | | ----------------- | ---------------------------------------------------------------------- | | **CPU Usage** | Porcentaje de uso de CPU de tu instancia (ej. 0.2% de 1 vCPU) | | **Memory Usage** | Memoria RAM utilizada vs. disponible (ej. 75.41 MB de 207.35 MB) | | **Storage Usage** | Espacio de almacenamiento utilizado vs. total (ej. 0.05 GB de 1.00 GB) | Cada métrica incluye una barra visual que muestra el porcentaje de uso y el espacio libre. ### Gráficas de tendencia Debajo de los indicadores encontrarás gráficas de tendencia para **CPU usage trend** y **Memory usage trend**. Cada gráfica incluye una línea roja punteada que indica el umbral de alerta. ### Rangos de tiempo Puedes ajustar el período de las métricas con los botones de rango: | Rango | Descripción | | ------- | ------------------------------ | | **1H** | Última hora | | **6H** | Últimas 6 horas | | **24H** | Últimas 24 horas (por defecto) | | **3D** | Últimos 3 días | | **7D** | Últimos 7 días | Haz clic en **Refresh** para actualizar los datos en tiempo real. ## Logs Pestaña Logs del Monitor mostrando gráfica de timeline, tabla de logs con nivel, mensaje y timestamp La pestaña **Logs** muestra un registro cronológico de toda la actividad de tu base de datos: * **Peticiones a la API** (GET, POST, PUT, DELETE) con código de estado, tiempo de ejecución y dirección IP * **Errores y advertencias** del sistema * **Eventos de autenticación** ### Niveles de log | Nivel | Código | Descripción | | --------- | ------ | -------------------------------------- | | **DEBUG** | -4 | Información detallada para diagnóstico | | **INFO** | 0 | Operaciones normales del sistema | | **WARN** | 4 | Situaciones que requieren atención | | **ERROR** | 8 | Errores que requieren acción | ### Buscar y filtrar logs Usa la barra de búsqueda en la parte superior para filtrar logs. Puedes buscar por texto libre o usar expresiones de filtro como: ``` level > 0 && data.auth = 'guest' ``` La gráfica de timeline encima de la tabla de logs muestra la distribución de eventos a lo largo del tiempo, permitiéndote identificar picos de actividad o errores. ### Detalle de un registro Haz clic en cualquier fila de la tabla de logs para abrir el panel **Log Details**. Muestra siempre un **Resumen** (*Overview*) y, según la información del log, secciones de **Solicitud**, **Autenticación**, **Cliente**, **Información de error** y **Datos adicionales**. Incluye un botón **Download JSON** para exportar la entrada completa. # Registros Source: https://docs.jelou.ai/guides/datum/registros Crea, edita, busca, filtra y exporta registros dentro de tus colecciones Los **registros** son las filas de datos dentro de una colección. Cada registro contiene valores para los campos definidos en la colección. ## Crear un registro Formulario New record con campos brand, model, price y más, mostrando tipos de campo e indicadores de campo obligatorio Selecciona la colección donde quieres agregar un registro desde la barra lateral izquierda. El botón se encuentra en la esquina superior derecha de la vista de colección. El formulario muestra todos los campos de la colección con sus tipos. Los campos marcados con \* son obligatorios. El campo **id** se genera automáticamente al guardar. El registro se crea y aparece en la tabla de la colección. ## Editar un registro Panel Edit record mostrando campos editables con valores, contadores de caracteres y botón Save changes Haz clic en la flecha al final de cualquier fila para abrir el panel de edición. Modifica los valores que necesites y haz clic en **Save changes** para guardar. Cada campo de texto muestra un contador de caracteres (ej. `5/5000 characters`) para que puedas controlar la longitud del contenido. ## Duplicar y eliminar registros Dentro del panel de edición de un registro, haz clic en el menú **⋮** (esquina superior derecha) para acceder a: * **Duplicate** — Crea una copia del registro con un nuevo ID * **Delete** — Elimina el registro permanentemente ## Buscar registros Utiliza la barra de búsqueda en la parte superior de la tabla (**Search across visible columns**) para buscar texto en todas las columnas visibles. También puedes usar los atajos **Cmd/Ctrl+K** para enfocar la búsqueda y **Cmd/Ctrl+N** para abrir el formulario de nuevo registro. ## Filtros avanzados Haz clic en el ícono de filtro (embudo) junto a la barra de búsqueda para abrir el panel de **Advanced filters**. Elige entre **AND** (todas las condiciones deben cumplirse) u **OR** (al menos una condición debe cumplirse). Haz clic en **+ Add** y configura cada condición con: * **Campo** — Selecciona el campo a filtrar * **Operador** — Elige el operador de comparación * **Valor** — Ingresa el valor a comparar Haz clic en **Apply filters** para ver los resultados filtrados. Los operadores disponibles dependen del tipo de campo: | Tipo de campo | Operadores | | ------------- | --------------------------------------------------------------------------- | | **Text** | Equals, Not equal, Contains, Not contains, Is empty, Is not empty | | **Number** | Equals, Not equal, Greater than, Greater or equal, Less than, Less or equal | | Tipo de campo | Operadores | | ------------------- | ----------------------------------------------------------------------------------- | | **Boolean** | Is, Is not | | **Date / Autodate** | Equals, Not equal, After, On or after, Before, On or before, Is empty, Is not empty | | **Select** | Equals, Not equal, Is empty, Is not empty | | **Multi-select** | Contains like, Not contains like, Is empty, Is not empty | | **JSON** | Contains text, Does not contain, Is empty, Is not empty | Los nombres de los operadores se muestran en inglés en la interfaz. En la sección **CURRENT QUERY** puedes ver y copiar la expresión de filtro generada. Esto es útil para reutilizar el filtro en llamadas a la API. Puedes guardar combinaciones de filtros que uses frecuentemente con el botón **Save filters** y limpiar todos los filtros activos con **Clear**. ## Ordenar registros Haz clic en el encabezado de cualquier columna para alternar el orden de los registros: * Primera vez: orden **ascendente** (A→Z, 0→9) * Segunda vez: orden **descendente** (Z→A, 9→0) * Tercera vez: sin orden específico ## Exportar registros Haz clic en **Export** en la esquina superior derecha para abrir el menú **Descargar como** y elegir el formato: **CSV** (.csv) o **Excel** (.xlsx), cuando esté disponible. La exportación respeta los filtros activos, permitiéndote exportar solo un subconjunto de datos. ## Paginación La tabla muestra los registros paginados. En la esquina inferior derecha puedes: * Ver cuántos registros hay en total (ej. "1 - 10 de 10") * Navegar entre páginas con las flechas de anterior y siguiente * Cambiar la cantidad de filas por página con el selector de filas ## Operaciones por lote Cuando activas **Operaciones por lote** en la configuración de la base de datos, al seleccionar registros aparece una barra de acciones en la parte inferior con: * El número de registros seleccionados y un botón **Limpiar** (o la tecla `Esc`). * **Exportar** — Descarga los registros seleccionados como **JSON**, **CSV** o **TSV**. * **Eliminar** — Borra los registros seleccionados (con confirmación). La exportación por lote incluye los registros seleccionados de la página actual. Si seleccionaste registros en otras páginas, la interfaz te pedirá cargarlos primero. ## API Preview Panel API Preview mostrando endpoints List/Search, View, Create, Update, Delete y Batch con ejemplos en cURL Haz clic en **API Preview** en la esquina superior derecha para ver la documentación interactiva de la API para la colección actual. Incluye: * **List/Search** — Listar y buscar registros con filtros, orden y paginación * **View** — Obtener un registro por ID * **Create** — Crear un nuevo registro * **Update** — Actualizar un registro existente * **Delete** — Eliminar un registro * **Batch** — Operaciones en lote Cada operación incluye ejemplos de código en **cURL**, **JavaScript**, **Python**, **PHP** y **Laravel** con los endpoints y parámetros específicos de tu colección. # Triggers Source: https://docs.jelou.ai/guides/datum/triggers Configura webhooks que se disparan automáticamente cuando se crean, actualizan o eliminan registros Los **Triggers** (disparadores) te permiten ejecutar webhooks automáticamente cuando ocurren cambios en tus colecciones. Son ideales para sincronizar datos con sistemas externos, enviar notificaciones o ejecutar flujos de trabajo. Página de Triggers con estado vacío y botón New trigger ## Crear un trigger Formulario New trigger con campos Name, Collection, Event (Create, Update, Delete), Webhook Request, Headers y Status Ve a **Configuración > Triggers** desde la barra lateral izquierda. El botón se encuentra en la esquina superior derecha. Completa los siguientes campos: * **Name** — Un nombre descriptivo (ej. "Sync CRM", "Notificar nuevo pedido") * **Collection** — La colección que quieres monitorear * **Event** — El tipo de evento que dispara el webhook * **Update scope** — Solo para el evento Update: dispara ante cualquier columna o solo columnas específicas * **Webhook Request** — El método HTTP y la URL destino * **Headers** — Headers personalizados para la petición * **Status** — Activa o desactiva el trigger El trigger se activa inmediatamente si el estado está en **Active**. ## Eventos disponibles | Evento | Se dispara cuando... | | ---------- | ---------------------------------------------------------------------- | | **Create** | Se inserta un nuevo registro en la colección | | **Update** | Cambia un registro existente (puedes limitarlo a columnas específicas) | | **Delete** | Se elimina un registro | Puedes crear múltiples triggers para la misma colección con diferentes eventos. Por ejemplo, un trigger para sincronizar datos en cada **Create** y otro para enviar una alerta en cada **Delete**. ### Alcance de actualización Cuando el evento es **Update**, aparece un control adicional de **Update scope** (alcance de actualización) con dos opciones: | Opción | Comportamiento | | :----------------------- | :----------------------------------------------------------------------------------- | | **Cualquier columna** | El webhook se dispara ante cualquier cambio en el registro. | | **Columnas específicas** | Eliges una o más columnas; el webhook solo se dispara cuando cambia alguna de ellas. | Al elegir **Columnas específicas** debes seleccionar al menos una columna. Los eventos **Create** y **Delete** siempre usan "cualquier columna" y no muestran este control. ## Configuración del webhook ### Método HTTP Selecciona el método HTTP apropiado para tu endpoint: | Método | Uso típico | | ---------- | ------------------------------------------------- | | **POST** | Enviar datos del registro al servidor (más común) | | **GET** | Notificar sin enviar datos en el body | | **PUT** | Reemplazar un recurso completo | | **PATCH** | Actualizar parcialmente un recurso | | **DELETE** | Solicitar eliminación de un recurso | ### URL del webhook Ingresa la URL completa del endpoint que recibirá la notificación (ej. `https://tu-api.com/webhooks/databases`). La URL debe ser **pública** y usar **HTTPS**. ### Headers Agrega headers personalizados para autenticación u otros propósitos. Por defecto se incluye un header de ejemplo para **Authorization** con formato Bearer token. Haz clic en **+ Add header** para agregar headers adicionales. Cada header tiene un campo de nombre (key) y valor (value). ## Reintentos automáticos Si la entrega de un webhook falla, Databases la reintenta automáticamente. ## Estado del trigger El toggle de **Status** te permite activar o desactivar un trigger sin eliminarlo. Un trigger desactivado no envía webhooks aunque ocurran eventos en la colección. # Autenticación Source: https://docs.jelou.ai/guides/functions/autenticacion Cómo la plataforma protege tus funciones con API keys, el uso de runtime tokens legacy, y cómo hacer funciones públicas. Toda función desplegada está protegida por defecto. Las peticiones sin una credencial válida reciben `401 Unauthorized`. ## Inicio rápido ```bash theme={null} jelou functions deploy # ✓ Deployed # ▸ URL: https://mi-funcion.fn.jelou.ai ``` El deploy no genera ninguna credencial automáticamente. Para llamar tu función necesitas una API key de plataforma (ver el siguiente paso). Crea una API key en [apps.jelou.ai](https://apps.jelou.ai), en la sección de configuración de la app, y envíala en el header `Authorization`: ```bash theme={null} curl -X POST https://mi-funcion.fn.jelou.ai \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_..." \ -d '{"query": "test"}' ``` Respuesta exitosa: ```json theme={null} { "results": [] } ``` Sin API key o con una inválida: ```json theme={null} { "error": "Unauthorized", "message": "Missing or invalid auth token (use X-Jelou-Token or Authorization: Bearer)" } ``` ## ¿Cómo funciona? 1. Crea una API key de plataforma en [apps.jelou.ai](https://apps.jelou.ai), en la sección de configuración de la app 2. Cada petición debe incluirla en el header `Authorization: Bearer sk_...` 3. El SDK valida la key contra el gateway de Jelou, confirma que pertenece a la misma organización dueña de la función, y cachea el resultado en memoria 4. Si la key es válida, la petición llega a tu handler. Si no, retorna `401` ## API keys (recomendado) La creación de nuevos runtime tokens (`jelou functions tokens create`) está **deprecada** y responde `410 Gone`. Los deploys tampoco generan uno automáticamente — el mecanismo soportado para autenticar funciones, nuevas o existentes, es una API key de plataforma. Los runtime tokens `jfn_rt_...` creados antes de esta migración se siguen validando con `X-Jelou-Token` (compatibilidad hacia atrás) y pueden listarse o revocarse con `jelou functions tokens list` / `jelou functions tokens revoke`, pero no pueden crearse nuevos ni se generan en deploys nuevos. ## Rutas sin autenticación Las rutas `/__health` y `/openapi.json` nunca requieren credencial. Los triggers cron tampoco — la plataforma los autentica automáticamente. ## Ejemplos de uso ```bash theme={null} curl -X POST https://mi-funcion.fn.jelou.ai \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_..." \ -d '{"telefono": "593987654321"}' ``` ```javascript theme={null} const res = await fetch("https://mi-funcion.fn.jelou.ai", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.JELOU_FUNCTION_API_KEY}`, }, body: JSON.stringify({ telefono: "593987654321" }), }); const data = await res.json(); ``` ```python theme={null} import requests import os res = requests.post( "https://mi-funcion.fn.jelou.ai", headers={ "Content-Type": "application/json", "Authorization": f"Bearer {os.environ['JELOU_FUNCTION_API_KEY']}", }, json={"telefono": "593987654321"}, ) data = res.json() ``` Para conectar tu función como servidor MCP externo en Brain Studio: 1. URL: `https://mi-funcion.fn.jelou.ai/mcp` 2. Header: `Authorization` → `Bearer sk_...` Consulta la [guía de Brain Studio](/guides/functions/brain) para instrucciones paso a paso. La plataforma no valida nada — tu código es responsable. # Brain Studio Source: https://docs.jelou.ai/guides/functions/brain Conecta tu Jelou Function como servidor MCP externo en Brain Studio para que tus agentes IA la invoquen como herramienta. ## Conectar tu función en Brain Studio ```bash theme={null} jelou functions deploy # ✓ Deployed to https://mi-funcion.fn.jelou.ai ``` Crea una API key en [apps.jelou.ai](https://apps.jelou.ai) — la necesitarás en el paso 4. ```bash theme={null} curl https://mi-funcion.fn.jelou.ai/__health \ -H "Authorization: Bearer sk_..." ``` Respuesta esperada: ```json theme={null} { "status": "ok", "mode": "define", "tool": { "name": "mi-funcion", "description": "Busca información de clientes" } } ``` Si usas `app()`, verás múltiples tools en el array `tools`. Si tu función tiene `config: { mcp: false }`, no se expondrá como herramienta MCP. Asegúrate de que MCP esté activo (es el default). En el dashboard de Jelou, navega a tu agente IA: **AI Agent → Tools → Servidores MCP externos** Completa los campos: | Campo | Valor | | ------------ | ------------------------------------ | | URL | `https://mi-funcion.fn.jelou.ai/mcp` | | Header name | `Authorization` | | Header value | `Bearer sk_...` | Brain Studio descubrirá automáticamente las herramientas expuestas por tu función. Selecciona las que quieras que el agente pueda usar. Si usas `app()` con múltiples tools, cada uno aparece como una herramienta independiente con su nombre y descripción. Inicia una conversación con el agente y pide algo que requiera usar tu herramienta. Por ejemplo: > "¿Cuál es el saldo del cliente 593987654321?" El agente detectará que necesita invocar tu función, pasará los parámetros correctos, y mostrará la respuesta al usuario. ## Multi-tool en Brain Studio Si tu función usa `app()`, Brain Studio ve cada tool individualmente: ```typescript theme={null} export default app({ tools: { consultarSaldo: define({ description: "Consulta el saldo de un cliente por teléfono", input: z.object({ telefono: z.string().min(10) }), handler: async (input) => ({ saldo: 150.00 }), }), crearTicket: define({ description: "Crea un ticket de soporte", input: z.object({ asunto: z.string(), detalle: z.string() }), handler: async (input) => ({ ticketId: "TKT-001" }), }), }, }); ``` En Brain Studio aparecerán: * **consultarSaldo** — "Consulta el saldo de un cliente por teléfono" * **crearTicket** — "Crea un ticket de soporte" Puedes habilitar o deshabilitar cada herramienta individualmente. Las descripciones de `define()` y las anotaciones `.describe()` de Zod en los campos de input son lo que Brain Studio muestra al agente IA. Descripciones claras y específicas mejoran la precisión del agente al decidir cuándo y cómo usar tu herramienta. ## Troubleshooting Verifica que: 1. La función está desplegada (`jelou functions deploy`) 2. MCP está activo (`config.mcp` no es `false`) 3. La URL termina en `/mcp` 4. El token es correcto Prueba manualmente: ```bash theme={null} curl https://mi-funcion.fn.jelou.ai/mcp \ -H "Authorization: Bearer sk_..." ``` La API key es inválida o falta en el header `Authorization`. El deploy no genera ninguna credencial automáticamente — crea una en [apps.jelou.ai](https://apps.jelou.ai) y configura el header MCP como `Authorization: Bearer `. Si tu función es legacy y todavía usa un runtime token, puedes verificarlo con: ```bash theme={null} jelou functions tokens list mi-funcion ``` `jelou functions tokens create` está **deprecado** (responde `410 Gone`) — ya no puedes generar un runtime token nuevo. Consulta la [guía de autenticación](/guides/functions/autenticacion#api-keys-recomendado). Revisa la `description` de tu `define()`. Si es vaga (como "Maneja datos"), el agente no sabe cuándo usarla. Usa descripciones específicas como "Busca un cliente por número de teléfono y retorna su nombre, email y saldo". # CLI Source: https://docs.jelou.ai/guides/functions/cli Referencia del subcomando jelou functions dentro del CLI unificado de Jelou: desarrollo local, despliegue, secrets, tokens, cron y logs de funciones. El CLI de Functions forma parte del **[CLI unificado de Jelou](/guides/cli)** (`@jelou/cli`). Los comandos de funciones viven bajo el namespace `jelou functions …` (por ejemplo `jelou functions deploy`), mientras que la autenticación y los perfiles se comparten con el resto de la plataforma. ¿Recién empiezas con el CLI? Revisa primero la [introducción al CLI de Jelou](/guides/cli), la [autenticación](/guides/cli/autenticacion) y las [skills para editores de IA](/guides/cli/skills). Esta página documenta en detalle el grupo `jelou functions`. ## Instalación ```bash theme={null} npm install -g @jelou/cli ``` Verifica: ```bash theme={null} jelou --version ``` ## Autenticación La autenticación es global del CLI (no específica de funciones). Resumen rápido; detalles en [Autenticación y perfiles](/guides/cli/autenticacion). ```bash theme={null} jelou login # interactivo jelou login --token $JELOU_TOKEN # no interactivo (CI) jelou whoami # identidad actual jelou logout # olvida el perfil activo ``` En CI, pasa el token por variable de entorno: ```bash theme={null} export JELOU_TOKEN=${{ secrets.JELOU_TOKEN }} ``` ## Proyectos ### `jelou functions init` Inicializa un proyecto de Jelou Functions. ```bash theme={null} jelou functions init # ? What is your function name? mi-funcion # ? Add a description? Consulta de clientes # ✓ Created mi-funcion ``` | Flag | Descripción | | ----------------------- | --------------------------------------------------- | | `--slug ` | Nombre de la función | | `--description ` | Descripción | | `--mode ` | Crear una función nueva o enlazar con una existente | | `--force` | Sobrescribir `jelou.json` existente | Genera: `index.ts`, `jelou.json`, `deno.json`, `.env`, `.gitignore`. ```bash theme={null} # CI: crear una función nueva jelou functions init --slug mi-funcion --mode create --no-input # CI: enlazar con una función existente jelou functions init --slug funcion-existente --mode link --no-input ``` ### `jelou functions dev` Inicia el servidor de desarrollo local con hot reload. ```bash theme={null} jelou functions dev # ▸ Function mi-funcion # ▸ Port 3000 # ▸ Routes http://localhost:3000 # ▸ Health http://localhost:3000/__health # ▸ MCP http://localhost:3000/mcp ``` | Flag | Descripción | | -------------------- | -------------------------------- | | `--port ` | Puerto (default: 3000) | | `--env ` | Ruta al archivo `.env` | | `--no-watch` | Desactivar hot reload | | `--local-sdk ` | Ruta al directorio del SDK local | El servidor detecta automáticamente si tu función usa `define()` o `app()` y muestra las rutas correspondientes. Para probar [ejecuciones diferidas](/guides/functions/diferidas) en local, arranca con `JELOU_FN_DEFER_DEV=1`. ### `jelou functions check` Valida el proyecto antes de desplegar. No necesita autenticación ni red. ```bash theme={null} jelou functions check # ✓ index.ts entrypoint válido # ✓ deno.json JSON plano # ⚠ package.json entrada @jelou/* de npm — usa el import map de deno.json # ✓ Sin problemas bloqueantes ``` Detecta lo que haría fallar el arranque de tu función en producción y avisa sobre configuraciones dudosas. Termina con error si encuentra algo bloqueante. Ejecútalo antes de `deploy` en tus pipelines de CI: detecta los problemas sin gastar un despliegue. ## Despliegue ### `jelou functions deploy` Despliega a producción. ```bash theme={null} jelou functions deploy # ▸ Files: index.ts (1.2 KB), jelou.json (98 B), deno.json (65 B) # ? Deploy mi-funcion? (Y/n) y # ✓ Deployed # ▸ URL: https://mi-funcion.fn.jelou.ai ``` | Flag | Descripción | | ---------------- | -------------------------------------- | | `--no-confirm` | Omitir confirmación (para CI) | | `--follow`, `-f` | Streaming de logs después del deploy | | `--no-wait` | No esperar a que el deploy quede listo | | `--dry-run` | Mostrar qué se subiría sin desplegar | El primer deploy genera automáticamente un runtime token. ### `jelou functions rollback [slug] [id]` Revierte a un despliegue anterior. ```bash theme={null} jelou functions rollback # interactivo jelou functions rollback mi-funcion dep_abc12345 # directo ``` ### `jelou functions deployments list [slug]` Lista el historial de despliegues. ```bash theme={null} jelou functions deployments list mi-funcion # ▸ ID Status Source Files Deployed By Age # ▸ dep_abc123.. active cli 3 alex@jelou.ai 2h ago ``` ```bash theme={null} jelou functions deployments info mi-funcion dep_abc123 jelou functions deployments download mi-funcion dep_abc123 -o artifact.tar.gz ``` ### `jelou functions deployments build [slug] [id]` Muestra el log de compilación de un despliegue — responde "¿por qué falló el deploy que acabo de hacer?". Sin argumentos usa el despliegue más reciente del proyecto actual. ```bash theme={null} jelou functions deployments build # ▸ Status failed # ✗ index.ts:14:3 — Property 'telefono' does not exist on type ... ``` | Flag | Descripción | | ------- | -------------------------------------------------------- | | `--all` | Mostrar todas las líneas, no solo errores y advertencias | Este es el log de **compilación**. Para los logs de ejecución de la función usa `jelou functions logs`. ## Funciones ### `jelou functions list` Lista todas las funciones. ```bash theme={null} jelou functions list # ▸ Slug Status URL Updated # ▸ consultar-cliente active https://consultar-cliente.fn.jelou.ai 4/7/2026 ``` ### `jelou functions info ` Muestra detalles de una función. ```bash theme={null} jelou functions info consultar-cliente ``` ### `jelou functions create` Crea una función remota (sin código local). ```bash theme={null} jelou functions create --slug mi-api --name "Mi API" ``` ### `jelou functions delete ` Elimina una función. Requiere confirmación (`-y` para omitirla en CI). ```bash theme={null} jelou functions delete mi-funcion # ? Delete function "mi-funcion"? This cannot be undone. (y/N) ``` ## Secrets Secrets a nivel de **función**. Para secrets de toda la organización, usa `jelou secret` — ver [Secrets de organización](/guides/cli/secret). ### `jelou functions secrets list ` ```bash theme={null} jelou functions secrets list mi-funcion # ▸ Key Updated # ▸ CRM_API_KEY 2 hours ago ``` ### `jelou functions secrets set ` ```bash theme={null} jelou functions secrets set mi-funcion CRM_API_KEY=sk_test_123 DB_URL=postgres://... jelou functions secrets set mi-funcion --from-env .env.production # Un solo secret leyendo el valor de un archivo jelou functions secrets set mi-funcion PRIVATE_KEY --from-file key.pem ``` Los valores nuevos toman efecto en el **siguiente despliegue**. Ejecuta `jelou functions deploy` para aplicarlos. ### `jelou functions secrets delete ` ```bash theme={null} jelou functions secrets delete mi-funcion CRM_API_KEY ``` ## Tokens Runtime tokens: los bearers (`X-Jelou-Token` / `Authorization: Bearer`) que usan los clientes externos para invocar las rutas de una función. El deploy auto-genera uno la primera vez. ### `jelou functions tokens list [slug]` ```bash theme={null} jelou functions tokens list mi-funcion # ▸ Name Prefix Last Used Created # ▸ default jfn_rt_abc1.. 4/7/2026, 10:30 AM 4/1/2026 ``` ### `jelou functions tokens create [slug]` Genera un nuevo runtime token para una función. El token completo se muestra una sola vez en stdout — guárdalo de inmediato, porque a partir de ahí `tokens list` solo muestra un prefijo censurado. ```bash theme={null} jelou functions tokens create mi-funcion --name ci-deploy # ✓ Token created # ▸ Token jfn_rt_abc123def456... (guárdalo ahora, no vuelve a mostrarse) ``` ```bash theme={null} jelou functions tokens create # interactivo: pide slug y nombre ``` | Flag | Descripción | | --------------- | ---------------- | | `--name ` | Nombre del token | Crea tokens adicionales para separar credenciales por entorno o integración (por ejemplo uno para CI y otro para un partner externo) sin tocar el token que generó el primer `deploy`. ### `jelou functions tokens revoke ` Revoca un runtime token existente. ```bash theme={null} jelou functions tokens revoke mi-funcion token-uuid-123 -y ``` ## Logs ### `jelou functions logs [slug]` ```bash theme={null} jelou functions logs mi-funcion # streaming en vivo (default) jelou functions logs mi-funcion --history # logs históricos jelou functions logs mi-funcion -f # alias de streaming ``` | Flag | Descripción | | ----------------------------- | -------------------------------------------------------------- | | `--history` | Logs históricos en vez de streaming | | `--follow`, `-f` | Streaming en vivo (default) | | `--limit ` | Máximo de entradas a devolver | | `--since ` / `--until ` | Rango temporal en ISO 8601 | | `--order ` | `asc` (default, más antiguos primero) o `desc` (más recientes) | | `--cursor ` | Cursor de paginación de una consulta anterior | ```bash theme={null} # Últimas 100 entradas jelou functions logs mi-funcion --history --limit 100 # Solo lo de la última hora jelou functions logs mi-funcion --history --since 2026-08-03T14:00:00Z ``` No confundir con `jelou logs` (sin `functions`), que accede a los logs de conversaciones de producción de un bot — ver [Workflows y pruebas](/guides/cli/workflows). ## Cron ### `jelou functions cron list [slug]` ```bash theme={null} jelou functions cron list mi-funcion # ▸ Tool Expression Timezone Last Triggered # ▸ default 0 9 * * * America/Guayaquil 2 hours ago ``` ```bash theme={null} jelou functions cron logs mi-funcion jelou functions cron logs mi-funcion --state ERROR ``` | Flag | Descripción | | ------------------- | ----------------------------------------------- | | `--state ` | Filtrar por estado (`DELIVERED`, `ERROR`, etc.) | | `--cursor ` | Cursor de paginación de una consulta anterior | ```bash theme={null} # Página siguiente del historial de cron jelou functions cron logs mi-funcion --cursor eyJpZCI6MTIzfQ== ``` ## Diferidas Consulta las [ejecuciones diferidas](/guides/functions/diferidas) que tu función tiene agendadas. Las reservas se crean desde el código o por cabeceras HTTP, no desde el CLI. ### `jelou functions defer list [slug]` ```bash theme={null} jelou functions defer list mi-funcion # ▸ ID Status Scheduled At Key # ▸ dinv_01ksqbvy.. scheduled 4/8/2026, 9:00:00 AM carrito:42:c-9 # ▸ dinv_01ksqcxz.. firing 4/7/2026, 6:00:00 PM recordatorio:user-7 ``` Por defecto muestra las reservas activas (`scheduled`, `firing`, `cancel_requested`). | Flag | Descripción | | --------------------- | ------------------------------------------------------------------------- | | `--status ` | `active` (default), `scheduled`, `firing`, `fired`, `cancelled`, `failed` | | `--key ` | Filtrar por etiqueta exacta | | `--subject ` | Filtrar por audiencia (normalmente el ID de usuario) | | `--limit ` | Máximo de filas (1-100) | ```bash theme={null} # Las que fallaron jelou functions defer list mi-funcion --status failed # Las de un usuario jelou functions defer list mi-funcion --subject user_42 ``` ### `jelou functions defer get ` Muestra el detalle de una reserva, incluido el último error si falló. ```bash theme={null} jelou functions defer get dinv_01ksqbvyqpe0prt91exc97mh4n ``` | Flag | Descripción | | --------------- | ------------------------------------------- | | `--slug ` | Función. Por defecto la del proyecto actual | | `--payload` | Incluir el cuerpo del payload en la salida | ## Skills para editores de IA El antiguo `jelou skill install` se unificó en `jelou agent install`, que instala las skills de toda la plataforma (incluida `jelou-functions`) en tus editores de IA. ```bash theme={null} jelou agent install # interactivo jelou agent install --global # global (todos los proyectos) jelou agent install --only functions ``` Detalles en [Skills para editores de IA](/guides/cli/skills). ## Flags globales | Flag | Descripción | | ------------------ | ------------------------------------------------------------ | | `--json` | Output JSON a stdout | | `--agent` | Modo agente (implica `--json --no-input --compact NO_COLOR`) | | `--compact` | JSON sin espacios | | `--human` | Forzar salida legible aunque stdout esté redirigido | | `--no-input` | Desactivar prompts interactivos | | `--describe` | Emite el esquema del comando como JSON, sin ejecutarlo | | `--profile ` | Usar un perfil específico | Referencia completa de flags, modos y exit codes en [Referencia del CLI](/guides/cli/referencia). ## Variables de entorno | Variable | Descripción | | ---------------- | ----------------------------------------------------------- | | `JELOU_TOKEN` | Token de autenticación (sin necesidad de `jelou login`) | | `JELOU_NO_INPUT` | `1` para modo no interactivo | | `CI` | `true` para modo no interactivo (detectado automáticamente) | | `JELOU_PROFILE` | Perfil por defecto | En pipelines de CI, usa `JELOU_TOKEN` como variable de entorno y `--json` para output parseable: ```bash theme={null} export JELOU_TOKEN=${{ secrets.JELOU_TOKEN }} jelou functions deploy --no-confirm --json | jq -r '.data.url' ``` # Context Source: https://docs.jelou.ai/guides/functions/context Referencia completa del objeto ctx disponible en cada handler: empresa, canal, usuario, trigger, environment, mensajería, memoria y más. El objeto `ctx` es el segundo parámetro de todo handler. Contiene información de la empresa, canal, usuario, tipo de trigger, y da acceso a secrets, mensajería, memoria y logging. ```typescript theme={null} handler: async (input, ctx, request) => { ctx.log("Petición recibida", { company: ctx.company.name, bot: ctx.bot.channel, user: ctx.user.id, trigger: ctx.trigger.type, }); } ``` ## Referencia | Propiedad | Tipo | Descripción | | ------------------------- | ------------------------- | ------------------------------------------------- | | `ctx.functionSlug` | `string` | Slug de la función | | `ctx.company` | `{ id, name }` | Empresa del request | | `ctx.bot` | `{ id, name, channel }` | Canal que originó el request | | `ctx.user` | `{ id, names?, roomId? }` | Usuario de la conversación | | `ctx.conversation` | `{ id?, ... }` | Datos de la conversación | | `ctx.operator` | `{ id?, ... }` | Operador asignado | | `ctx.trigger` | `TriggerInfo` | Tipo de trigger (http, cron, event) | | `ctx.env` | `EnvAccessor` | Acceso a secrets | | `ctx.params` | `Record` | Parámetros de ruta | | `ctx.query` | `Record` | Query string params | | `ctx.jelou` | `JelouSDK` | Mensajería WhatsApp y ejecuciones diferidas | | `ctx.memory` | `MemorySDK` | Memoria de sesión key-value | | `ctx.templateRegistry` | `TemplateRegistry` | Catálogo de plantillas WhatsApp aprobadas | | `ctx.guard` | `Guard` | Cadena de verificaciones antes de actuar | | `ctx.verifyStripe()` etc. | `(req) => Promise` | Verificación de firma de webhooks | | `ctx.method` | `string` | Método HTTP (GET, POST, etc.) | | `ctx.path` | `string` | Path del request | | `ctx.requestId` | `string` | UUID único del request | | `ctx.isCron` | `boolean` | `true` si es trigger cron | | `ctx.isEvent` | `boolean` | `true` si es trigger event | | `ctx.isHttp` | `boolean` | `true` si es request HTTP | | `ctx.isScheduledFire` | `boolean` | `true` si es el disparo de una ejecución diferida | | `ctx.skillId` | `string \| null` | ID del flujo de Brain Studio | | `ctx.executionId` | `string \| null` | ID de ejecución de Brain Studio | | `ctx.log()` | `(...args) => void` | Logger estructurado | ## Identidad ```typescript theme={null} handler: async (input, ctx) => { // Empresa ctx.company.id; // 42 ctx.company.name; // "Tienda ABC" // Canal ctx.bot.id; // "bot-123" ctx.bot.name; // "Canal de Soporte" ctx.bot.channel; // "whatsapp" // Usuario ctx.user.id; // 99 ctx.user.names; // "María García" (opcional) ctx.user.roomId; // "room-456" (opcional) // Conversación y operador ctx.conversation.id; // "conv-789" (opcional) ctx.operator.id; // "op-012" (opcional) } ``` Los datos se hidratan automáticamente desde la plataforma según los headers del request (`x-bot-id`, `x-user-id`). ## Trigger Tres tipos de trigger determinan cómo se invocó tu función: ```typescript theme={null} handler: async (input, ctx) => { if (ctx.isHttp) { // Petición HTTP normal ctx.trigger; // { type: "http" } } } ``` ```typescript theme={null} handler: async (input, ctx) => { if (ctx.isCron) { ctx.trigger.type; // "cron" ctx.trigger.cron; // "0 9 * * *" ctx.trigger.cronName; // "recordatorio-mañana" (opcional) } } ``` ```typescript theme={null} handler: async (input, ctx) => { if (ctx.isEvent) { ctx.trigger.type; // "event" ctx.trigger.event; // "pago.completado" } } ``` Usa los guards `ctx.isCron`, `ctx.isEvent` y `ctx.isHttp` en lugar de comparar `ctx.trigger.type` manualmente. ## Request ```typescript theme={null} export default define({ description: "API con ruta parametrizada", input: z.object({}), config: { path: "/users/:id" }, handler: async (input, ctx) => { ctx.method; // "GET" ctx.path; // "/users/42" ctx.params.id; // "42" ctx.query.format // "json" (de ?format=json) ctx.requestId; // "a1b2c3d4-..." return { userId: ctx.params.id }; }, }); ``` ## Environment (secrets) ```typescript theme={null} handler: async (input, ctx) => { // Obtener un secret const apiKey = ctx.env.get("CRM_API_KEY"); // string | undefined // Verificar si existe if (ctx.env.has("WEBHOOK_SECRET")) { // ... } // Obtener todos (excepto internos __FN_*) const all = ctx.env.toObject(); // Record } ``` Las variables internas con prefijo `__FN_` están bloqueadas — `ctx.env.get("__FN_COMPANY_ID")` retorna `undefined`. ## Mensajería (`ctx.jelou`) Envía mensajes de WhatsApp directamente desde tu función: ```typescript theme={null} handler: async (input, ctx) => { if (ctx.jelou.available) { await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Tu pedido está listo", }); } } ``` 14 tipos de mensaje, templates HSM, validación de plantillas y manejo de errores. ## Ejecuciones diferidas (`ctx.jelou.schedule`) Agenda una ejecución futura de tu propia función: ```typescript theme={null} handler: async (input, ctx) => { await ctx.jelou.schedule({ in: "24h", path: "/enviar-seguimiento", payload: { userId: input.userId, botId: ctx.bot.id }, subject: `user_${input.userId}`, }); } ``` Agendar, consultar, cancelar y verificar antes de actuar. ## Verificaciones (`ctx.guard`) Encadena condiciones y ejecuta la acción solo si todas se cumplen. Útil cuando la realidad pudo cambiar desde que agendaste algo: ```typescript theme={null} handler: async (input, ctx) => { return ctx.guard .when("sinPagar", async () => !(await consultarPedido(input.pedidoId)).pagado) .run(async () => { await ctx.jelou.send({ type: "text", to: input.telefono, text: "Tu pago está pendiente" }); return { enviado: true }; }); } ``` Si alguna verificación devuelve un valor falso, la cadena se corta y devuelve `{ skipped: "" }` sin ejecutar la acción. ## Webhooks (`ctx.verify*`) Verifica la firma de un webhook antes de procesarlo: ```typescript theme={null} async handler(event, ctx, request) { await ctx.verifyStripe(request); return { received: true }; } ``` Stripe, Shopify, Meta y firmas HMAC genéricas. ## Memoria (`ctx.memory`) Persiste datos por sesión (key-value con TTL): ```typescript theme={null} handler: async (input, ctx) => { if (ctx.memory.available) { const paso = await ctx.memory.get("paso", "inicio"); await ctx.memory.set("paso", "confirmacion", 3600); } } ``` Primitivos, JSON, TTL, límites y patrones comunes. ## Brain Studio (`skillId`, `executionId`) Cuando tu función es invocada desde Brain Studio como herramienta MCP: ```typescript theme={null} handler: async (input, ctx) => { if (ctx.skillId) { ctx.log("Invocado desde Brain Studio", { skillId: ctx.skillId, // "skill-abc-123" executionId: ctx.executionId, // "exec-def-456" }); } } ``` Estos campos son `null` para peticiones HTTP directas y triggers cron. ## Logging `ctx.log()` escribe JSON estructurado con metadatos automáticos: ```typescript theme={null} handler: async (input, ctx) => { ctx.log("Procesando pedido", { telefono: input.telefono }); // Escribe a stdout: // { // "requestId": "a1b2c3d4-...", // "function": "mi-funcion", // "company": 42, // "timestamp": "2026-04-07T15:30:01.234Z", // "args": ["Procesando pedido", { "telefono": "593987654321" }] // } } ``` Visualiza los logs con `jelou functions logs mi-funcion`. ## Ejemplo completo ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "procesar-pedido", description: "Procesa un pedido y notifica al cliente por WhatsApp", input: z.object({ pedidoId: z.string(), telefono: z.string().min(10), }), handler: async (input, ctx) => { ctx.log("Procesando pedido", { pedidoId: input.pedidoId, company: ctx.company.id, bot: ctx.bot.name, }); // Consultar API externa con secret const apiKey = ctx.env.get("ORDERS_API_KEY"); const res = await fetch(`https://api.example.com/orders/${input.pedidoId}`, { headers: { Authorization: `Bearer ${apiKey}` }, }); const pedido = await res.json(); // Notificar al cliente por WhatsApp if (ctx.jelou.available) { await ctx.jelou.send({ type: "text", to: input.telefono, text: `Tu pedido ${pedido.id} está ${pedido.status}.`, }); } // Guardar estado en memoria if (ctx.memory.available) { await ctx.memory.set("ultimo_pedido", input.pedidoId, 86400); } return { pedidoId: pedido.id, status: pedido.status, notificado: ctx.jelou.available, }; }, }); ``` Enviar WhatsApp con ctx.jelou. Persistir datos con ctx.memory. Variables de entorno cifradas. Runtime tokens y funciones públicas. # Cron Source: https://docs.jelou.ai/guides/functions/cron Configura tareas programadas en tus funciones: sintaxis cron, zonas horarias, guard isCron, límites y sincronización en deploy. ## Configuración Define schedules cron directamente en el `config` de tu función. No necesitas configuración externa. ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "recordatorio-citas", description: "Envía recordatorios de citas por WhatsApp", input: z.object({}), config: { cron: [ { expression: "0 8 * * *", timezone: "America/Guayaquil" }, { expression: "0 8 * * *", timezone: "America/Bogota" }, ], }, handler: async (_input, ctx) => { if (!ctx.isCron) return { skipped: true }; ctx.log("Enviando recordatorios", { cron: ctx.trigger.cron }); const apiKey = ctx.env.get("JELOU_API_KEY"); const res = await fetch("https://api.jelou.ai/v1/messages/send", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ botId: ctx.bot.id, phone: "593987654321", message: "Hola, te recordamos que tienes una cita mañana a las 10:00 AM.", }), }); return { enviados: 1, status: res.status }; }, }); ``` ## Prueba local Inicia el servidor con `jelou functions dev`. Puedes enviar una petición HTTP a la función, pero el guard `isCron` la rechazará porque no es un trigger cron real: ```bash curl theme={null} curl -X POST http://localhost:3000 \ -H "Content-Type: application/json" \ -d '{}' ``` ```json Respuesta 200 theme={null} { "skipped": true, "reason": "no es un trigger cron" } ``` No puedes simular un trigger cron real desde curl — la plataforma inyecta `ctx.isCron` y la firma criptográfica automáticamente. Para probar la lógica del cron, usa [`createMockContext({ isCron: true })`](/guides/functions/testing) en tus tests. ## Sintaxis Cada schedule tiene dos campos: | Campo | Tipo | Requerido | Descripción | | ------------ | -------- | --------- | ------------------------------------------------------- | | `expression` | `string` | Sí | Cron estándar de 5 campos | | `timezone` | `string` | No | Zona horaria IANA. Default: `UTC`. | | `name` | `string` | No | Label identificativo del schedule. Max 64 caracteres. | | `botId` | `string` | No | ID del bot para hidratar `ctx.bot`. Max 128 caracteres. | ### Formato de expresión ``` ┌───────────── minuto (0-59) │ ┌───────────── hora (0-23) │ │ ┌───────────── día del mes (1-31) │ │ │ ┌───────────── mes (1-12) │ │ │ │ ┌───────────── día de la semana (0-7, 0 y 7 = domingo) │ │ │ │ │ * * * * * ``` ### Ejemplos comunes | Expresión | Descripción | | -------------- | ----------------------------------- | | `0 9 * * *` | Todos los días a las 9:00 AM | | `0 9 * * 1-5` | Lunes a viernes a las 9:00 AM | | `*/30 * * * *` | Cada 30 minutos | | `0 0 1 * *` | Primer día de cada mes a medianoche | | `0 */2 * * *` | Cada 2 horas | | `30 14 * * 3` | Miércoles a las 2:30 PM | ### Zonas horarias Usa cualquier zona IANA válida: ```typescript theme={null} config: { cron: [ { expression: "0 9 * * *", timezone: "America/Guayaquil" }, // Ecuador { expression: "0 9 * * *", timezone: "America/Bogota" }, // Colombia { expression: "0 9 * * *", timezone: "America/Mexico_City" }, // México { expression: "0 9 * * *", timezone: "America/Argentina/Buenos_Aires" }, { expression: "0 3 * * *" }, // UTC por defecto ], } ``` ### Name y botId Usa `name` para identificar cada schedule y `botId` para hidratar `ctx.bot` con un canal específico: ```typescript theme={null} config: { cron: [ { expression: "0 9 * * *", timezone: "America/Guayaquil", name: "recordatorio-mañana", botId: "bot-whatsapp-123", }, ], } ``` ## Guard `isCron` Tu función puede recibir tanto peticiones HTTP como disparos cron. Usa `ctx.isCron` para distinguirlos: ```typescript theme={null} handler: async (_input, ctx) => { if (!ctx.isCron) { return { skipped: true, reason: "no es un trigger cron" }; } ctx.log("Tarea cron ejecutándose", { cron: ctx.trigger.cron }); return { ejecutado: true }; } ``` Sin el guard `isCron`, cualquier petición HTTP a tu función ejecutará la lógica del cron. Siempre incluye esta verificación. ## Cómo funciona 1. Defines los schedules en `config.cron` 2. Al ejecutar `jelou functions deploy`, la plataforma lee tu configuración y crea los schedules 3. Cuando un schedule se dispara, tu función recibe una petición con `ctx.isCron === true` y `ctx.trigger.cron` con la expresión que lo activó 4. La verificación de firma criptográfica previene invocaciones no autorizadas ## Límites * Máximo **10** schedules cron por función * Exceder este límite lanza un error en tiempo de definición ## Gestión Los schedules son **declarativos** — se definen en el código y se sincronizan en cada despliegue. Para modificar un schedule, cambia `config.cron` en tu código y vuelve a desplegar. Para ver los schedules activos: ```bash theme={null} jelou functions cron list consultar-cliente # ▸ Expression Timezone Enabled Last Triggered # ▸ 0 8 * * * America/Guayaquil yes 2 hours ago # ▸ 0 8 * * * America/Bogota yes 2 hours ago ``` ## Multi-tool cron Cuando usas `app()`, cada tool puede tener sus propios schedules cron independientes. Las peticiones cron se envían a la ruta específica de cada tool. ```typescript theme={null} import { app, define, z } from "@jelou/functions"; export default app({ tools: { limpiezaDiaria: define({ description: "Limpia registros obsoletos", input: z.object({}), config: { cron: [{ expression: "0 3 * * *", timezone: "UTC" }] }, handler: async (_input, ctx) => { if (!ctx.isCron) return { skipped: true }; return { cleaned: true }; }, }), sincronizacionHoraria: define({ description: "Sincroniza datos externos", input: z.object({}), config: { cron: [{ expression: "0 * * * *" }] }, handler: async (_input, ctx) => { if (!ctx.isCron) return { skipped: true }; return { synced: true }; }, }), }, }); ``` El límite de **10** schedules cron es **agregado** entre todos los tools de un `app()`. Si un tool usa 6 schedules, los demás tools solo pueden usar 4 en total. ## Logs de ejecución Consulta el historial de ejecuciones cron: ```bash theme={null} jelou functions cron logs mi-funcion # ▸ Time Tool State HTTP # ▸ 4/7/2026, 9:00:00 AM default DELIVERED 200 # ▸ 4/6/2026, 9:00:00 AM default ERROR 500 ``` Filtra por estado: ```bash theme={null} jelou functions cron logs mi-funcion --state ERROR ``` Estados posibles: `DELIVERED`, `ERROR`, `RETRY`, `RETRY_SCHEDULED`, `FAILED`. ## Problemas comunes Los schedules cron se sincronizan al hacer deploy. Si cambiaste la expresión cron, necesitas redesplegar: ```bash theme={null} jelou functions deploy ``` Verifica que el schedule esté activo: ```bash theme={null} jelou functions cron list mi-funcion # ▸ Expression Timezone Enabled Last Triggered # ▸ 0 8 * * * America/Guayaquil yes 2 hours ago ``` Si la columna `Enabled` muestra `no`, revisa que la expresión cron sea válida. Sin el guard `isCron`, cualquier petición HTTP ejecutará la lógica del cron. Agrega la verificación al inicio del handler: ```typescript theme={null} handler: async (_input, ctx) => { if (!ctx.isCron) return { skipped: true }; // tu lógica cron aquí return { ejecutado: true }; } ``` Esto retorna `{ skipped: true }` para peticiones HTTP normales y solo ejecuta la lógica cuando es un trigger cron real. Consulta la [guía completa de multi-tool](/guides/functions/multi-tool) para más detalles sobre `app()`. # Despliegue Source: https://docs.jelou.ai/guides/functions/despliegue Flujo de despliegue, límites de archivos, rollback interactivo y directo, y configuración de CI/CD con GitHub Actions. ## Flujo de despliegue Cuando ejecutas `jelou functions deploy`, la plataforma: 1. Lee `jelou.json` para encontrar el slug y entrypoint 2. Recolecta todos los archivos desplegables del directorio 3. Muestra un resumen con nombres y tamaños 4. Sube los archivos y ejecuta el despliegue 5. Renombra tu entrypoint a `user-function.ts` 6. Genera un `main.ts` wrapper que importa tu código e inicia el servidor 7. Inyecta tus secrets como variables de entorno ```bash theme={null} jelou functions deploy # ▸ Files: index.ts (1.2 KB), helpers.ts (800 B), jelou.json (98 B), deno.json (65 B) # ? Deploy consultar-cliente? (Y/n) y # ✓ Deployed # ▸ ID: dep_abc12345 # ▸ URL: https://consultar-cliente.fn.jelou.ai ``` El deploy no genera ningún runtime token — la función queda protegida por defecto y solo acepta llamadas autenticadas con una API key de plataforma. Créala en la sección de configuración de apps ([apps.jelou.ai](https://apps.jelou.ai)) y envíala como `Authorization: Bearer `. Consulta la [guía de autenticación](/guides/functions/autenticacion) para más detalles. ## Límites de archivos | Límite | Valor | | ----------------------- | ------------------------------------ | | Archivos por despliegue | 20 | | Tamaño por archivo | 256 KB | | Tamaño total | 1 MB | | Extensiones permitidas | `.ts`, `.js`, `.json`, `.md`, `.txt` | | Entrypoint requerido | `index.ts` | ### Qué se excluye automáticamente * `node_modules/` * `.git/` * `.env` * `dist/` * `.jelou/` * Archivos ocultos (que empiezan con `.`) ## Omitir confirmación Para despliegues automatizados, usa `--no-confirm`: ```bash theme={null} jelou functions deploy --no-confirm ``` ## Rollback Si necesitas revertir a una versión anterior, usa `jelou functions rollback`. Sin argumentos, muestra un menú con despliegues recientes: ```bash theme={null} jelou functions rollback # ? Select deployment to rollback to: # ▸ dep_abc12345.. — 2 hours ago by alex@jelou.ai (current) # dep_def67890.. — 1 day ago by ci@jelou.ai # dep_ghi11223.. — 3 days ago by alex@jelou.ai ``` Especifica el slug y el ID del despliegue: ```bash theme={null} jelou functions rollback consultar-cliente dep_def67890 # ✓ Rolled back to dep_def67890 ``` ## CI/CD con GitHub Actions ```yaml deploy.yml theme={null} name: Deploy Function on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install CLI run: npm install -g @jelou/cli - name: Deploy env: JELOU_TOKEN: ${{ secrets.JELOU_TOKEN }} run: jelou functions deploy --no-confirm --json | jq '.data.url' ``` ```yaml deploy.yml theme={null} name: Deploy with Secrets on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install CLI run: npm install -g @jelou/cli - name: Configure secrets env: JELOU_TOKEN: ${{ secrets.JELOU_TOKEN }} run: | jelou functions secrets set consultar-cliente \ CRM_API_KEY=${{ secrets.CRM_API_KEY }} \ JELOU_API_KEY=${{ secrets.JELOU_API_KEY }} - name: Deploy env: JELOU_TOKEN: ${{ secrets.JELOU_TOKEN }} run: | DEPLOY_URL=$(jelou functions deploy --no-confirm --json | jq -r '.data.url') echo "Deployed to $DEPLOY_URL" ``` Usa `--json` en pipelines para obtener output estructurado que puedes parsear con `jq`. El formato es siempre `{ "ok": true, "data": ... }` en stdout. ## Rutas expuestas | Ruta | Descripción | | ----------- | ----------------------------------------------------------- | | `/__health` | Health check y metadata de la función | | `/mcp` | Endpoint MCP (a menos que `config.mcp: false`) | | Tu ruta | Ruta del handler (default: `*` coincide con cualquier path) | | Ruta | Descripción | | ------------- | -------------------------------------------------------------------------- | | `/__health` | Health check con lista de todos los tools | | `/mcp` | Servidor MCP unificado con todos los tools | | `/` | Ruta de cada tool (kebab-case auto-generado o `config.path` personalizado) | ## Historial de despliegues ```bash theme={null} jelou functions deployments list mi-funcion # ▸ ID Status Source Files Deployed By Age # ▸ dep_abc123.. active cli 3 alex@jelou.ai 2h ago # ▸ dep_def456.. active cli 3 ci@jelou.ai 1d ago ``` Para ver detalles de un despliegue específico: ```bash theme={null} jelou functions deployments info mi-funcion dep_abc123 ``` Para descargar el artifact de un despliegue: ```bash theme={null} jelou functions deployments download mi-funcion dep_abc123 -o artifact.tar.gz ``` # Ejecuciones diferidas Source: https://docs.jelou.ai/guides/functions/diferidas Programa una ejecución futura de tu función: recordatorios, seguimientos y carritos abandonados con ctx.jelou.schedule, cancelación y guards de disparo. Una **ejecución diferida** es una invocación que agendas para que ocurra una sola vez en el futuro: un recordatorio a las 24 horas, un seguimiento de carrito abandonado, una encuesta 2 días después de la compra. **¿Cron o diferida?** * **[Cron](/guides/functions/cron)** — se repite en un horario fijo (todos los días a las 9:00). Se define en el código. * **Diferida** — ocurre una vez, en un momento calculado en tiempo de ejecución (24 horas después de *este* pedido). Se agenda desde el handler o desde la petición HTTP. ## Agendar desde el handler `ctx.jelou.schedule()` agenda una ejecución relativa a "ahora": ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "carrito-abandonado", description: "Recibe el evento de carrito abandonado y agenda el seguimiento", input: z.object({ userId: z.string(), carritoId: z.string(), }), handler: async (input, ctx) => { const booking = await ctx.jelou.schedule({ in: "24h", path: "/enviar-seguimiento", // Incluye el botId: en el disparo no hay canal resuelto payload: { userId: input.userId, carritoId: input.carritoId, botId: ctx.bot.id }, key: `carrito:${input.userId}:${input.carritoId}`, subject: `user_${input.userId}`, }); ctx.log("Seguimiento agendado", { id: booking.id, cuando: booking.scheduledAt }); return { agendado: true, id: booking.id }; }, }); ``` ### Parámetros | Campo | Tipo | Requerido | Descripción | | ---------------- | ------------------ | --------- | -------------------------------------------------------- | | `in` | `string \| number` | Sí | Duración: `"30m"`, `"24h"`, `"1h30m"`, `"3d"` o segundos | | `path` | `string` | Sí | Ruta de tu función que se ejecutará. Empieza con `/` | | `payload` | `object \| array` | No | Datos que recibirá el handler al dispararse | | `key` | `string` | No | Etiqueta para buscar o cancelar después | | `subject` | `string` | No | Audiencia, normalmente el ID del usuario | | `idempotencyKey` | `string` | No | Evita agendar dos veces si reintentas | | `pinDeployment` | `boolean` | No | Fija la ejecución al despliegue actual | La duración acepta unidades compuestas de mayor a menor: `"1h30m"` es válido, `"30m1h"` no. Para seguimientos muy cortos usa **5 segundos o más**. Con duraciones menores la latencia de red puede dejar el momento agendado en el pasado y la plataforma lo rechaza. ### Momento absoluto `ctx.jelou.scheduleAt()` recibe una fecha en vez de una duración: ```typescript theme={null} await ctx.jelou.scheduleAt({ at: new Date("2026-12-24T18:00:00Z"), // o el string ISO 8601 path: "/felicitacion-navidad", payload: { userId: input.userId }, }); ``` Lanza un error si la fecha ya pasó o no se puede interpretar. El resto de campos funciona igual que en `schedule()`. ### Evitar duplicados Si el servicio que llama a tu función reintenta, `idempotencyKey` garantiza una sola reserva: ```typescript theme={null} const booking = await ctx.jelou.schedule({ in: "24h", path: "/enviar-seguimiento", payload: { userId: input.userId, carritoId: input.carritoId }, idempotencyKey: `carrito-abandonado:${input.eventoId}`, }); if (booking.idempotentReplay) { ctx.log("Ya estaba agendado, no se duplicó", { id: booking.id }); } ``` Reusar la misma `idempotencyKey` con un `payload` distinto devuelve un error `409`. ## Agendar desde una petición HTTP Cualquier cliente puede diferir una llamada agregando una cabecera al POST normal. Sin cabeceras de agenda, la función se ejecuta de inmediato como siempre. ```bash theme={null} curl -X POST https://recordatorios.fn.jelou.ai/enviar-recordatorio \ -H "Authorization: Bearer " \ -H "X-Jelou-Schedule-In: 5m" \ -H "X-Jelou-Key: recordatorio:user-42" \ -H "Content-Type: application/json" \ -d '{"userId":"42","mensaje":"Tu cita es mañana"}' ``` | Cabecera | Efecto | | ------------------------ | ------------------------------------------------ | | `X-Jelou-Schedule-In` | Duración (`"5m"`, `"1h30m"`, `"24h"` o segundos) | | `X-Jelou-Schedule-At` | Momento absoluto en ISO 8601 | | `X-Jelou-Key` | Etiqueta para buscar o cancelar después | | `X-Jelou-Subject` | Audiencia, normalmente el ID del usuario | | `X-Jelou-Pin-Deployment` | `"true"` para fijar el despliegue actual | | `Idempotency-Key` | Evita agendar dos veces al reintentar | La respuesta es `202 Accepted` cuando se crea la reserva y `200 OK` cuando un reintento reutiliza una reserva existente. El cuerpo no puede superar **64 KB**. ## Recibir el disparo Cuando llega el momento, tu función recibe el `payload` original en la ruta que indicaste. Usa `ctx.isScheduledFire` para distinguir el disparo de una petición normal: ```typescript theme={null} handler: async (input, ctx) => { if (ctx.isScheduledFire) { ctx.log("Ejecución diferida disparada"); } // ... } ``` ### Verificar antes de actuar Entre el momento en que agendas y el momento en que se dispara, la realidad pudo cambiar: el cliente ya pagó, la plantilla se pausó, el usuario se dio de baja. `ctx.guard` encadena esas verificaciones y evita el envío si alguna falla: ```typescript theme={null} export default define({ name: "enviar-seguimiento", description: "Envía el seguimiento de carrito abandonado si sigue aplicando", input: z.object({ userId: z.string(), carritoId: z.string(), botId: z.string(), }), handler: async (input, ctx) => { // El botId viajó en el payload que agendaste antes const registro = ctx.templateRegistry.for(input.botId); return ctx.guard .when("sinPagar", async () => !(await consultarCarrito(input.carritoId)).pagado) .when("plantillaAprobada", () => registro.has("carrito_abandonado_v3")) .run(async () => { await ctx.jelou.sendTemplate({ template: "carrito_abandonado_v3", to: input.userId, params: ["María", `https://tienda.com/recuperar?c=${input.carritoId}`], }); return { enviado: true }; }); }, }); ``` Si todas las verificaciones pasan, se ejecuta `.run()` y su resultado es la respuesta. Si alguna falla, la cadena se corta y devuelve `{ skipped: "" }` sin ejecutar el envío. La cadena termina con `.run(handler)`, no con `.then()`. `ctx.guard` es una cadena nueva en cada petición. Si el bot no viaja en el `payload`, en un disparo diferido `ctx.bot` no está resuelto y `ctx.templateRegistry` queda sin canal asociado. Incluye el `botId` en el `payload` al agendar y enlázalo con `ctx.templateRegistry.for(botId)`. Ver [plantillas de WhatsApp](/guides/functions/mensajeria#validar-plantillas-antes-de-enviar). ## Consultar lo agendado `ctx.jelou.findDeferred()` lista las ejecuciones pendientes de tu función: ```typescript theme={null} const { data, total } = await ctx.jelou.findDeferred({ subject: `user_${input.userId}`, status: "scheduled", }); for (const fila of data) { ctx.log("pendiente", fila.id, fila.scheduledAt, fila.key); } ``` | Campo | Descripción | | ------------------ | ------------------------------------------------------------------------------------- | | `key` | Filtra por etiqueta exacta | | `subject` | Filtra por audiencia | | `status` | `"active"` (default), `"scheduled"`, `"firing"`, `"fired"`, `"cancelled"`, `"failed"` | | `page` / `perPage` | Paginación. Default `20`, máximo `100` | ## Cancelar `ctx.jelou.cancelDefer()` cancela en bloque por audiencia o etiqueta. Requiere exactamente uno de `subject`, `key` o `keyPrefix`: ```typescript theme={null} // Baja del usuario: cancela todos sus recordatorios pendientes const { cancelled, raced } = await ctx.jelou.cancelDefer({ subject: `user_${input.userId}`, }); ctx.log("baja procesada", { cancelados: cancelled.length, en_curso: raced.length }); ``` `raced` contiene las ejecuciones que ya se estaban disparando cuando llegó la cancelación — para esas, la verificación con `ctx.guard` en el handler es la última defensa. Escribe tus handlers para que sean idempotentes. Antes de una cancelación amplia, previsualiza el alcance con `dryRun`: ```typescript theme={null} const previa = await ctx.jelou.cancelDefer({ keyPrefix: "promo-verano:", dryRun: true, }); if (previa.wouldCancelCount < 5000) { await ctx.jelou.cancelDefer({ keyPrefix: "promo-verano:" }); } ``` No existe "reprogramar": una reserva es inmutable. Para cambiar la hora, cancela y agenda de nuevo. ## Prueba local `schedule`, `scheduleAt`, `findDeferred` y `cancelDefer` funcionan solo en una función **desplegada**. En local lanzan un error porque no hay credenciales de plataforma. Para probar el flujo con `jelou functions dev`, activa la simulación: ```bash theme={null} JELOU_FN_DEFER_DEV=1 jelou functions dev ``` En ese modo se validan los argumentos, se registra la reserva en los logs y se devuelve un resultado sintético — tu handler sigue ejecutándose, pero no se agenda nada real. Para tests unitarios usa [`createMockContext`](/guides/functions/testing), cuyo `ctx.jelou` registra las llamadas sin red ni configuración. ## Inspeccionar desde el CLI ```bash theme={null} # Reservas activas de la función del proyecto actual jelou functions defer list # Las que fallaron jelou functions defer list mi-funcion --status failed # Las de un usuario jelou functions defer list mi-funcion --subject user_42 # Detalle de una reserva (incluye el último error) jelou functions defer get dinv_01ksqbvyqpe0prt91exc97mh4n ``` Ver la [referencia del CLI](/guides/functions/cli#diferidas). ## Límites | Límite | Valor | | --------------------------------- | -------------------- | | Reservas activas por empresa | 1,000 | | Anticipación máxima | 30 días | | Tamaño del `payload` | 64 KB | | Cancelación en bloque por llamada | 10,000 coincidencias | Las reservas ya disparadas, canceladas o fallidas no cuentan contra el límite de activas. ## Problemas comunes Consulta el estado y el último error de la reserva: ```bash theme={null} jelou functions defer get dinv_01ksqbvy... --payload ``` Si el estado es `failed`, el campo de error indica por qué falló la entrega. Si es `cancelled`, algo la canceló antes — revisa tus llamadas a `cancelDefer`. Una cancelación que llega cuando la ejecución ya empezó no la detiene. Aparece en `raced` y tu handler se ejecuta. Por eso la verificación va en el handler, no solo en la cancelación: ```typescript theme={null} return ctx.guard .when("sigueActivo", async () => await usuarioActivo(input.userId)) .run(async () => { /* envío */ }); ``` ``` scheduleAt unavailable: platform credentials missing ``` Estás llamando a la familia `schedule` en local. Arranca el servidor con `JELOU_FN_DEFER_DEV=1 jelou functions dev` para simular las reservas. El servicio que llama a tu función reintentó. Agrega `idempotencyKey` (o la cabecera `Idempotency-Key`) con un valor derivado del evento: ```typescript theme={null} idempotencyKey: `carrito-abandonado:${input.eventoId}` ``` Tareas recurrentes en horario fijo. Enviar WhatsApp y validar plantillas. Verificar firmas de servicios externos. Comandos `defer list` y `defer get`. # Jelou Functions Source: https://docs.jelou.ai/guides/functions/index Funciones serverless en TypeScript: endpoints HTTP, validación Zod, herramientas MCP y multi-tool con app(), todo automático. **Vista previa** — Jelou Functions está en fase de vista previa. La API y el comportamiento pueden cambiar sin previo aviso. No lo uses en flujos de producción críticos sin contactar al equipo de soporte. **¿Cómo obtener tu token?** * **Clientes Enterprise:** el token debe ser solicitado al equipo de soporte técnico de Jelou. * **Clientes Self-service:** puedes generar tu propio token desde [Autenticación](/guides/functions/autenticacion). ## ¿Qué es Jelou Functions? Jelou Functions es una plataforma serverless de TypeScript donde con `define()` obtienes automáticamente: * **Endpoint HTTP** listo para recibir peticiones * **Validación** de entrada y salida con Zod * **Herramienta MCP** para que tus agentes IA la invoquen directamente * **Cron jobs** declarativos sin infraestructura adicional * **Ejecuciones diferidas** para agendar seguimientos y recordatorios * **Verificación de webhooks** de Stripe, Shopify y Meta en una línea * **Multi-tool** con `app()` para agrupar varias herramientas en un solo despliegue ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "consultar-cliente", description: "Busca información de un cliente por teléfono", input: z.object({ telefono: z.string().min(10), }), output: z.object({ nombre: z.string(), plan: z.string(), }), handler: async (input, ctx) => { ctx.log("Buscando cliente", { telefono: input.telefono }); return { nombre: "María García", plan: "Premium" }; }, }); ``` Despliega con un solo comando y obtén una URL de producción: ```bash theme={null} jelou functions deploy # → https://consultar-cliente.fn.jelou.ai ``` ## ¿Para qué sirve? Crea herramientas que tus agentes de WhatsApp pueden invocar: consultar datos, procesar pagos, verificar estados. Recibe callbacks de pasarelas de pago, CRMs o cualquier servicio externo con validación automática. Ejecuta tareas recurrentes como enviar recordatorios, sincronizar datos o limpiar sesiones inactivas. Agenda una ejecución futura: recordatorios a 24 horas, seguimientos de carrito abandonado, encuestas post-compra. Recibe eventos de Stripe, Shopify o Meta con verificación de firma en una línea. Del código local a producción en segundos, con rollback incluido y soporte para CI/CD. Agrupa múltiples herramientas en un solo despliegue con `app()`: rutas auto-generadas, MCP unificado y cron independiente. ## Prerrequisitos Antes de empezar, asegúrate de tener: * **Node.js 18+** instalado * Tu cuenta de Jelou con acceso a Functions * Tu token de acceso personal (prefijo `jfn_pat_`) ## Siguiente paso Crea, prueba y despliega tu primera función en menos de 5 minutos. # Límites Source: https://docs.jelou.ai/guides/functions/limites Límites de la plataforma Jelou Functions: archivos, tamaños, timeouts, cron, tokens, memoria y herramientas. ## Despliegue | Límite | Valor | | --------------------------- | ------------------------------------ | | Archivos por despliegue | 20 | | Tamaño por archivo | 256 KB | | Tamaño total del despliegue | 1 MB | | Extensiones permitidas | `.ts`, `.js`, `.json`, `.md`, `.txt` | | Entrypoint requerido | `index.ts` | ## Ejecución | Límite | Valor | | ------------------- | ------------ | | Timeout máximo | 120 segundos | | Timeout por defecto | 30 segundos | El timeout se configura con `config.timeout` en milisegundos: ```typescript theme={null} export default define({ description: "Operación lenta", input: z.object({}), config: { timeout: 60_000 }, // 60 segundos handler: async (_input, ctx) => { // operación que toma hasta 60s return { ok: true }; }, }); ``` ## Herramientas (tools) | Límite | Valor | | ------------------- | ----------------------------------------- | | Nombre de tool | Max 64 caracteres, patrón `[a-zA-Z0-9_-]` | | Descripción de tool | Max 1024 caracteres | ```typescript theme={null} // ✓ Nombres válidos define({ name: "consultar-saldo", ... }) define({ name: "search_products", ... }) define({ name: "myTool123", ... }) // ✗ Nombres inválidos define({ name: "mi tool", ... }) // espacios no permitidos define({ name: "tool.name", ... }) // punto no permitido define({ name: "", ... }) // vacío ``` ## Cron | Límite | Valor | | --------------------- | ---------------------------------------------- | | Schedules por función | 10 (agregado entre todos los tools en `app()`) | | Nombre de schedule | Max 64 caracteres | | botId de schedule | Max 128 caracteres | ```typescript theme={null} config: { cron: [ { expression: "0 9 * * *", timezone: "America/Guayaquil", name: "recordatorio-mañana" }, { expression: "0 15 * * *", timezone: "America/Guayaquil", name: "recordatorio-tarde" }, ], } ``` En modo `app()`, el límite de 10 schedules es **agregado** entre todos los tools. Si un tool usa 6 schedules, los demás tools solo pueden usar 4 en total. ## Ejecuciones diferidas | Límite | Valor | | --------------------------------- | -------------------- | | Reservas activas por empresa | 1,000 | | Anticipación máxima | 30 días | | Tamaño del `payload` | 64 KB | | Cancelación en bloque por llamada | 10,000 coincidencias | | Longitud de la ruta destino | Max 255 caracteres | Las reservas ya disparadas, canceladas o fallidas no cuentan contra el límite de activas. Ver [ejecuciones diferidas](/guides/functions/diferidas). ## Tokens | Límite | Valor | | ---------------------- | -------------------- | | Tamaño máximo de token | 4 KB | | Tokens por función | Sin límite explícito | ## Memoria (`ctx.memory`) | Límite | Valor | | ---------------- | ------------------------------------------------------------ | | Valor de `set()` | Max 255 caracteres (usar `setJson()` para datos más grandes) | | TTL máximo | 86,400 segundos (24 horas) | | Alcance | Por usuario | ```typescript theme={null} // ✓ Correcto await ctx.memory.set("paso", "confirmacion", 3600); // ✗ Error: excede 255 caracteres await ctx.memory.set("datos", jsonMuyLargo, 3600); // ✓ Usar setJson para datos grandes await ctx.memory.setJson("datos", { items: [...] }, 3600); ``` ## Secrets | Límite | Valor | | ----------------- | --------------------------------------------- | | Formato de clave | `UPPER_SNAKE_CASE` (`^[A-Z][A-Z0-9_]*$`) | | Prefijo bloqueado | `__FN_` (variables internas de la plataforma) | ## Archivos excluidos del despliegue Los siguientes se excluyen automáticamente: * `node_modules/` * `.git/` * `.env` * `dist/` * `.jelou/` * Archivos ocultos (que empiezan con `.`) # Logs Source: https://docs.jelou.ai/guides/functions/logs Monitorea tus funciones con jelou functions logs, ctx.log() para logging estructurado, y jelou functions cron logs para ejecuciones cron. ## Streaming en vivo Por defecto, `jelou functions logs` hace streaming de logs en tiempo real: ```bash theme={null} jelou functions logs mi-funcion # ▸ Streaming logs for mi-funcion # ▸ Press Ctrl+C to stop # # 10:30:01 INFO Buscando cliente { telefono: "593987654321" } # 10:30:02 WARN API externa lenta { latency: 2300 } # 10:31:15 INFO Buscando cliente { telefono: "593912345678" } ``` La flag `--follow` / `-f` es equivalente (es el comportamiento por defecto): ```bash theme={null} jelou functions logs mi-funcion --follow ``` ## Logs históricos Para obtener logs pasados en lugar de streaming: ```bash theme={null} jelou functions logs mi-funcion --history # ◇ Fetched 47 logs # # 10:25:01 INFO Deployed successfully # 10:30:01 INFO Buscando cliente { telefono: "593987654321" } # 10:30:02 WARN API externa lenta { latency: 2300 } ``` ## Escribir logs desde tu función Usa `ctx.log()` dentro del handler para escribir logs estructurados: ```typescript theme={null} handler: async (input, ctx) => { ctx.log("Procesando pedido", { telefono: input.telefono, company: ctx.company.id, requestId: ctx.requestId, }); const resultado = await procesarPedido(input); ctx.log("Pedido completado", { resultado }); return resultado; } ``` `ctx.log()` escribe JSON estructurado a stdout: ```json theme={null} { "requestId": "a1b2c3d4-...", "function": "mi-funcion", "company": 42, "timestamp": "2026-04-07T15:30:01.234Z", "args": ["Procesando pedido", { "telefono": "593987654321", "company": 42 }] } ``` `console.log()`, `console.warn()` y `console.error()` también funcionan y son capturados por la plataforma, pero `ctx.log()` agrega metadatos útiles (requestId, function, company, timestamp) automáticamente. ## Logs de cron Para ver el historial de ejecuciones cron: ```bash theme={null} jelou functions cron logs mi-funcion # ◇ Found 12 log entries # # ▸ Time Tool State HTTP # ▸ 4/7/2026, 9:00:00 AM default DELIVERED 200 # ▸ 4/7/2026, 3:00:00 AM default DELIVERED 200 # ▸ 4/6/2026, 9:00:00 AM default ERROR 500 ``` ### Filtrar por estado ```bash theme={null} jelou functions cron logs mi-funcion --state ERROR ``` Estados posibles: `DELIVERED`, `ERROR`, `RETRY`, `RETRY_SCHEDULED`, `ACTIVE`, `FAILED`. ### Paginación ```bash theme={null} jelou functions cron logs mi-funcion --cursor ``` El cursor se muestra al final de la salida cuando hay más resultados disponibles. ## JSON mode Para integrar con herramientas externas, usa `--json`: ```bash theme={null} # Logs como JSON lines (streaming) jelou functions logs mi-funcion --json # Logs históricos como JSON jelou functions logs mi-funcion --history --json # Cron logs como JSON jelou functions cron logs mi-funcion --json ``` En CI/CD, combina `--json` con herramientas como `jq` para filtrar logs: ```bash theme={null} jelou functions logs mi-funcion --history --json | jq 'select(.level == "ERROR")' ``` # MCP Source: https://docs.jelou.ai/guides/functions/mcp Cómo Jelou Functions genera automáticamente herramientas MCP para que tus agentes IA invoquen funciones como tools. ## Auto-generación de herramientas MCP Cuando creas una función con `define()`, se expone automáticamente como una herramienta [MCP](https://modelcontextprotocol.io/) (Model Context Protocol). Tus agentes IA pueden descubrir e invocar tu función como un tool sin configuración adicional. ## Ejemplo Para esta función: ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "consultar-saldo", description: "Consulta el saldo de un cliente por número de teléfono", input: z.object({ telefono: z.string().min(10).describe("Número de teléfono con código de país"), }), output: z.object({ nombre: z.string(), saldo: z.number(), moneda: z.string(), }), handler: async (input, ctx) => { ctx.log("Consultando saldo", { telefono: input.telefono }); return { nombre: "María García", saldo: 150.00, moneda: "USD" }; }, }); ``` El endpoint `/mcp` expone este esquema: ```json theme={null} { "name": "consultar-saldo", "description": "Consulta el saldo de un cliente por número de teléfono", "inputSchema": { "type": "object", "properties": { "telefono": { "type": "string", "minLength": 10, "description": "Número de teléfono con código de país" } }, "required": ["telefono"] } } ``` ## El campo `description`: lo más importante para MCP El campo `description` de tu `define()` es lo que el agente IA lee para decidir **cuándo** invocar tu herramienta. Si la descripción es vaga, el agente no sabrá cuándo usarla — o peor, la usará en el momento incorrecto. | ❌ Vaga | ✅ Específica | Por qué importa | | ------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------- | | `"Maneja usuarios"` | `"Busca un usuario por email o teléfono y retorna su perfil con saldo"` | El agente sabe exactamente qué datos puede obtener | | `"Envía mensaje"` | `"Envía un mensaje de WhatsApp a un número con código de país (ej: 593…)"` | El agente sabe el canal y el formato esperado | | `"Consulta API"` | `"Consulta el estado de un envío por tracking number en la API de Servientrega"` | El agente sabe para qué proveedor y qué dato necesita | ### Anotaciones `.describe()` en campos Las anotaciones `.describe()` de Zod se convierten en descripciones de parámetros del tool MCP. Esto es lo que ven los agentes IA cuando descubren tu función: ```typescript theme={null} input: z.object({ telefono: z.string().min(10).describe("Número con código de país, ej: 593987654321"), tipo: z.enum(["prepago", "pospago"]).describe("Tipo de plan del cliente"), incluirHistorial: z.boolean().default(false).describe("Si incluir las últimas 5 transacciones"), }) ``` Escribe descripciones claras y específicas en `.describe()`. Los agentes IA usan estas descripciones para decidir qué valores pasar a tu función. ## Probar el endpoint MCP Inicia el servidor local con `jelou functions dev` y consulta el esquema MCP: ```bash curl theme={null} curl http://localhost:3000/mcp ``` ```json Respuesta theme={null} { "tools": [ { "name": "consultar-saldo", "description": "Consulta el saldo de un cliente por número de teléfono", "inputSchema": { "type": "object", "properties": { "telefono": { "type": "string", "minLength": 10, "description": "Número de teléfono con código de país" } }, "required": ["telefono"] } } ] } ``` Invoca la función directamente: ```bash curl theme={null} curl -X POST http://localhost:3000 \ -H "Content-Type: application/json" \ -d '{"telefono": "593987654321"}' ``` ```json Respuesta 200 theme={null} { "nombre": "María García", "saldo": 150.00, "moneda": "USD" } ``` En producción: ```bash theme={null} curl https://consultar-saldo.fn.jelou.ai/mcp \ -H "X-Jelou-Token: jfn_rt_abc123..." ``` En producción, el endpoint `/mcp` requiere el header `X-Jelou-Token`. Sin token, recibirás `401 Unauthorized`. Solo `/__health` y `/openapi.json` son públicos sin token. ## Desactivar MCP Si tu función no debe ser descubierta como herramienta (por ejemplo, un webhook que solo recibe callbacks), desactiva MCP: ```typescript theme={null} config: { mcp: false } ``` Cuando MCP está desactivado, el endpoint `/mcp` retorna 404. ## ¿Cómo lo usan los agentes? Cuando configuras un agente IA en Jelou Brain Studio y le asignas funciones como tools, el agente: 1. Descubre las herramientas disponibles vía el endpoint `/mcp` 2. Lee el nombre, descripción y esquema de entrada 3. Decide cuándo invocar la herramienta basándose en la conversación del usuario 4. Envía los parámetros validados a tu función 5. Recibe la respuesta y la incorpora a la conversación Todo esto sucede automáticamente — solo necesitas escribir la función con `define()` y asignarla al agente. ## Multi-tool MCP Cuando usas `app()`, un solo servidor MCP en `/mcp` registra automáticamente todos los tools. Cada tool aparece como una herramienta independiente con su nombre, descripción y esquema. Para excluir un tool específico del registro MCP, usa `mcp: false` en su config: ```typescript theme={null} import { app, define, z } from "@jelou/functions"; export default app({ tools: { consultarSaldo: define({ description: "Consulta el saldo de un cliente", input: z.object({ telefono: z.string().min(10) }), handler: async (input) => ({ saldo: 150.00 }), }), webhookPagos: define({ description: "Recibe callbacks de pagos", input: z.object({ transactionId: z.string() }), config: { mcp: false }, handler: async (input) => ({ received: true }), }), }, }); ``` En este ejemplo, los agentes IA descubren `consultarSaldo` vía MCP pero `webhookPagos` solo es accesible por HTTP directo en `/webhook-pagos`. Consulta la [guía completa de multi-tool](/guides/functions/multi-tool) para más detalles sobre rutas auto-generadas y combinación de config. Guía paso a paso para configurar tu función como servidor MCP externo en Brain Studio. # Memoria de sesión Source: https://docs.jelou.ai/guides/functions/memoria Guarda y lee estado por conversación con ctx.memory: flujos multi-paso, carritos, contadores y datos temporales sin base de datos externa. ## ¿Qué es `ctx.memory`? `ctx.memory` es un cliente HTTP al **Memory API** de Jelou. Habla con el mismo almacenamiento key-value del usuario (`$memory` en el builder). Lo que escribas desde una Function es visible en placeholders `{{$memory.key}}` del workflow y viceversa — es una sola memoria por usuario, no una copia paralela. Disponible automáticamente cuando la petición viene de una conversación activa y la empresa tiene una API key de workflow configurada. Casos de uso típicos: * **Flujos multi-paso** — recordar en qué paso está el usuario * **Carritos de compras** — acumular productos durante la conversación * **Contadores** — limitar intentos de login, tracking de reintentos * **Preferencias del usuario** — idioma, formato, filtros que persisten entre conversaciones ## Inicio rápido ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "flujo-registro", description: "Registro multi-paso con memoria de sesión", input: z.object({ respuesta: z.string().optional(), }), handler: async (input, ctx) => { if (!ctx.memory.available) { return { error: "memory_unavailable" }; } const paso = await ctx.memory.get("paso", "inicio"); if (paso === "inicio") { await ctx.memory.set("paso", "nombre", 3600); return { pregunta: "¿Cuál es tu nombre?" }; } if (paso === "nombre") { await ctx.memory.set("nombre", input.respuesta || "", 3600); await ctx.memory.set("paso", "email", 3600); return { pregunta: "¿Cuál es tu email?" }; } const nombre = await ctx.memory.get("nombre", ""); await ctx.memory.delete("paso"); await ctx.memory.delete("nombre"); return { completado: true, nombre, email: input.respuesta }; }, }); ``` ## Verificar disponibilidad ```typescript theme={null} if (!ctx.memory.available) { ctx.log("Memory no disponible — fuera de una conversación o sin API key de workflow"); return { error: "memory_unavailable" }; } ``` `ctx.memory.available` es `false` cuando la petición no viene de una conversación activa o la API key del workflow no está configurada. Llamar a métodos en un cliente no disponible lanza un `Error`. ## Primitivos vs JSON Usa `set()`/`get()` para valores simples y `setJson()`/`getJson()` para objetos: ```typescript theme={null} await ctx.memory.set("paso", "confirmacion", 3600); const paso = await ctx.memory.get("paso", "inicio"); await ctx.memory.setJson("carrito", { items: [], total: 0 }, 86400); const carrito = await ctx.memory.getJson("carrito", { items: [], total: 0 }); ``` El tipo de retorno de `get()` coincide con el tipo del valor por defecto: ```typescript theme={null} const nombre = await ctx.memory.get("nombre", "anónimo"); // string const intentos = await ctx.memory.get("intentos", 0); // number const verificado = await ctx.memory.get("verificado", false); // boolean ``` ## TTL (tiempo de vida) Hay **dos capas de expiración** en Memory: 1. **TTL por variable** (el que pasas al `set`) — controla cuándo caduca esa variable individual. 2. **hashTTL de 30 días** (gestionado por la plataforma) — controla cuándo caduca la memoria completa del usuario. Cuando expira, se borra todo su Memory. **Renovación del hashTTL desde `ctx.memory`**: las escrituras vía `ctx.memory` solo **inicializan** el hashTTL cuando la memoria del usuario está vacía; escrituras posteriores **no lo extienden**. En cambio, las escrituras vía `$memory` en el builder sí extienden el hashTTL en cada write. Si tu flujo persiste solo desde Functions y necesitas actividad continua, combina con al menos una escritura desde el builder. El TTL por variable se especifica en segundos: | Método | TTL por variable | Máximo por variable | | ----------- | ---------------- | ------------------- | | `set()` | Opcional | 86,400 (24h) | | `setJson()` | **Requerido** | 86,400 (24h) | ```typescript theme={null} await ctx.memory.set("paso", "pago"); // sin TTL explícito await ctx.memory.set("paso", "pago", 1800); // expira en 30 min await ctx.memory.setJson("carrito", carrito, 86400); // expira en 24h (máximo) ``` ## Límites | Restricción | Valor | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Longitud máxima de `set()` | 255 caracteres | | TTL máximo por variable | 86,400 segundos (24h) | | Duración total de la memoria del usuario | 30 días desde la primera escritura; solo las escrituras vía `$memory` del builder renuevan la ventana (ver Warning arriba) | | Alcance | Por usuario — misma memoria que ve `$memory` del builder | Valores de `set()` que excedan 255 caracteres lanzan un `Error`. Para datos más grandes, usa `setJson()`. ## Patrones comunes ```typescript theme={null} handler: async (input, ctx) => { const paso = await ctx.memory.get("paso", "inicio"); if (paso === "inicio") { await ctx.memory.set("paso", "datos", 3600); return { siguiente: "datos" }; } if (paso === "datos") { await ctx.memory.set("nombre", input.nombre, 3600); await ctx.memory.set("paso", "confirmar", 3600); return { siguiente: "confirmar", nombre: input.nombre }; } const nombre = await ctx.memory.get("nombre", ""); await ctx.memory.delete("paso"); await ctx.memory.delete("nombre"); return { completado: true, nombre }; } ``` ```typescript theme={null} interface Carrito { items: Array<{ id: string; nombre: string; precio: number; cantidad: number }>; total: number; } handler: async (input, ctx) => { const vacio: Carrito = { items: [], total: 0 }; const carrito = await ctx.memory.getJson("carrito", vacio); carrito.items.push({ id: input.productoId, nombre: input.nombre, precio: input.precio, cantidad: input.cantidad, }); carrito.total = carrito.items.reduce( (sum, i) => sum + i.precio * i.cantidad, 0 ); await ctx.memory.setJson("carrito", carrito, 86400); return { carrito }; } ``` ```typescript theme={null} handler: async (input, ctx) => { const intentos = await ctx.memory.get("intentos_pin", 0); if (intentos >= 3) { return { bloqueado: true, mensaje: "Demasiados intentos" }; } const valido = input.pin === "1234"; if (!valido) { await ctx.memory.set("intentos_pin", intentos + 1, 1800); return { bloqueado: false, error: "PIN incorrecto" }; } await ctx.memory.delete("intentos_pin"); return { bloqueado: false, verificado: true }; } ``` ## Manejo de errores ```typescript theme={null} import { MemoryApiError } from "@jelou/functions"; try { await ctx.memory.set("paso", "pago", 3600); } catch (err) { if (err instanceof MemoryApiError) { ctx.log("Memory API falló", { status: err.status, code: err.code }); if (err.isRateLimit()) { return { error: "rate_limit", retryAfter: 2 }; } } throw err; } ``` ## ¿Cuándo usar `ctx.memory` vs una base de datos? | | `ctx.memory` | Base de datos externa | | ----------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------- | | **Alcance** | Por usuario | Global | | **Persistencia** | Hasta 30 días de inactividad · TTL opcional en `set()`, requerido en `setJson()` (máx 24h) | Permanente | | **Configuración** | Cero — viene incluido | Requiere connection string en secrets | | **Ideal para** | Estado de conversación, preferencias del usuario | Datos históricos, catálogos, config compartida | ## Acceder a memoria de otro usuario Desde triggers **cron** o **event**, puedes acceder a la memoria de un usuario específico con `ctx.memory.for(userId)`: ```typescript theme={null} import { define } from "@jelou/functions"; export default define({ name: "recordatorio-usuario", handler: async (_input, ctx) => { if (!ctx.isCron && !ctx.isEvent) return { skipped: true }; // Acceder a la memoria del usuario 12345 const userMemory = ctx.memory.for("12345"); const paso = await userMemory.get("paso", "desconocido"); if (paso === "pendiente") { await userMemory.set("paso", "recordado", 3600); } return { checked: true, paso }; }, }); ``` `ctx.memory.for()` solo funciona desde triggers cron o event. En requests HTTP normales, `ctx.memory` ya está vinculado a la sesión del usuario actual — llamar `.for()` lanza un error. # Mensajería Source: https://docs.jelou.ai/guides/functions/mensajeria Envía mensajes de WhatsApp desde tu función con ctx.jelou: texto, imágenes, botones, listas, carruseles, templates HSM y más. ## ¿Qué es `ctx.jelou`? `ctx.jelou` es el cliente de mensajería integrado. Permite enviar mensajes de WhatsApp directamente desde tu función sin configurar APIs externas. Está disponible automáticamente cuando la empresa tiene credenciales de la API de Jelou configuradas. ## Verificar disponibilidad ```typescript theme={null} handler: async (input, ctx) => { if (!ctx.jelou.available) { return { error: "Mensajería no configurada para esta empresa" }; } await ctx.jelou.send({ type: "text", to: input.telefono, text: "Hola" }); return { enviado: true }; } ``` `ctx.jelou.available` es `false` cuando la empresa no tiene `clientId`/`clientSecret` configurados. Llamar a `send()` o `sendTemplate()` en un cliente no disponible lanza un `Error`. ## Enviar mensajes ### `ctx.jelou.send(options)` Envía un mensaje individual. Retorna `{ messageId: string }`. ```typescript theme={null} await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Tu pedido #1234 está en camino", }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "image", to: "+593987654321", mediaUrl: "https://cdn.example.com/producto.jpg", caption: "Producto: Laptop Dell XPS 15", }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "video", to: "+593987654321", mediaUrl: "https://cdn.example.com/tutorial.mp4", caption: "Tutorial de configuración", }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "file", to: "+593987654321", mediaUrl: "https://cdn.example.com/factura.pdf", filename: "factura-1234.pdf", caption: "Tu factura del mes de abril", }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "audio", to: "+593987654321", mediaUrl: "https://cdn.example.com/mensaje.ogg", }); ``` ### Mensajes interactivos ```typescript theme={null} await ctx.jelou.send({ type: "buttons", to: "+593987654321", text: "¿Cómo deseas recibir tu pedido?", buttons: [ { id: "delivery", title: "Envío a domicilio" }, { id: "pickup", title: "Retiro en tienda" }, { id: "express", title: "Envío express" }, ], }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "list", to: "+593987654321", text: "Selecciona una categoría de productos:", buttonText: "Ver categorías", sections: [ { title: "Electrónica", rows: [ { id: "laptops", title: "Laptops", description: "Portátiles y notebooks" }, { id: "phones", title: "Teléfonos", description: "Smartphones y accesorios" }, ], }, { title: "Hogar", rows: [ { id: "furniture", title: "Muebles" }, { id: "kitchen", title: "Cocina" }, ], }, ], }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "quick_reply", to: "+593987654321", text: "¿Fue útil esta información?", replies: [ { title: "Sí, gracias", payload: "helpful_yes" }, { title: "No, necesito más ayuda", payload: "helpful_no" }, ], }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "cta_url", to: "+593987654321", text: "Completa tu compra en nuestra tienda:", displayText: "Ir a la tienda", url: "https://tienda.example.com/checkout/1234", }); ``` ### Mensajes avanzados ```typescript theme={null} await ctx.jelou.send({ type: "location", to: "+593987654321", lat: -2.1894, lng: -79.8891, name: "Tienda Guayaquil", address: "Av. 9 de Octubre 123, Guayaquil", }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "contacts", to: "+593987654321", contacts: [ { name: { formatted_name: "Soporte Técnico", first_name: "Soporte" }, phones: [{ phone: "+593912345678", type: "WORK" }], }, ], }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "carousel", to: "+593987654321", cards: [ { mediaUrl: "https://cdn.example.com/laptop.jpg", body: "Laptop Dell XPS 15 — $1,299", buttons: [ { id: "buy_laptop", title: "Comprar" }, { id: "info_laptop", title: "Más info" }, ], }, { mediaUrl: "https://cdn.example.com/tablet.jpg", body: "iPad Pro 12.9\" — $999", buttons: [ { id: "buy_tablet", title: "Comprar" }, { id: "info_tablet", title: "Más info" }, ], }, ], }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "flow", to: "+593987654321", text: "Completa tu registro:", flowId: "flow_abc123", flowCta: "Iniciar registro", flowAction: { screen: "REGISTER", data: { userId: "user-42" }, }, }); ``` ```typescript theme={null} await ctx.jelou.send({ type: "sticker", to: "+593987654321", mediaUrl: "https://cdn.example.com/sticker.webp", }); ``` ## Enviar templates HSM ### `ctx.jelou.sendTemplate(options)` Envía un template HSM aprobado por WhatsApp. Retorna `Array<{ id, destination }>`. ```typescript theme={null} // Template básico await ctx.jelou.sendTemplate({ template: "confirmacion_pedido", to: "+593987654321", language: "es", params: ["María", "PED-1234", "$59.99"], }); ``` ### Múltiples destinatarios ```typescript theme={null} await ctx.jelou.sendTemplate({ template: "promocion_mensual", to: ["+593987654321", "+593912345678", "+593998765432"], language: "es", params: ["20%", "30 de abril"], }); ``` ### Template con media ```typescript theme={null} await ctx.jelou.sendTemplate({ template: "recibo_pago", to: "+593987654321", language: "es", params: ["$150.00"], mediaUrl: "https://cdn.example.com/recibo-1234.pdf", filename: "recibo.pdf", }); ``` ## Validar plantillas antes de enviar Un `sendTemplate` falla en silencio por tres motivos: el nombre está mal escrito, el idioma no está aprobado, o mandas más o menos parámetros de los que espera la plantilla. `ctx.templateRegistry` te da acceso al catálogo de plantillas aprobadas del canal para detectarlo antes de llamar a Meta. ```typescript theme={null} handler: async (input, ctx) => { if (!ctx.templateRegistry.available) { return await ctx.jelou.send({ type: "text", to: input.telefono, text: "Tu pedido está listo" }); } if (await ctx.templateRegistry.has("confirmacion_pedido", "es")) { await ctx.templateRegistry.validate({ name: "confirmacion_pedido", language: "es", params: [input.nombre, input.pedidoId], }); await ctx.jelou.sendTemplate({ template: "confirmacion_pedido", to: input.telefono, language: "es", params: [input.nombre, input.pedidoId], }); } return { enviado: true }; } ``` ### Métodos | Método | Descripción | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `templateRegistry.available` | `false` si la empresa no tiene credenciales configuradas | | `templateRegistry.has(nombre, idioma?)` | `true` si el canal actual tiene la plantilla. Nunca lanza error | | `templateRegistry.hasAnywhere(nombre)` | `true` si cualquier canal de la empresa la tiene. Para auditorías, no para decidir un envío | | `templateRegistry.get(nombre, idioma)` | Devuelve la plantilla o `null`. Lanza error si no hay canal asociado | | `templateRegistry.list()` | Todas las plantillas aprobadas del canal actual | | `templateRegistry.validate({ name, language, params })` | Lanza error si el nombre, el idioma, el estado o la cantidad de parámetros no cuadran | | `templateRegistry.for(botId)` | Devuelve un catálogo enlazado a otro canal | `has()` y `hasAnywhere()` son verificaciones booleanas: si no hay canal asociado o el catálogo no responde, registran una advertencia y devuelven `false` en lugar de lanzar error. Cuando necesitas una respuesta definitiva usa `get()`, `list()` o `validate()`, que lanzan `TemplateRegistryError` con un `code`: `unknown_template`, `language_not_approved`, `param_count_mismatch`, `template_paused`, `registry_unavailable` o `no_bot_context`. `sendTemplate` **no** valida por su cuenta — el catálogo es de solo lectura. Llama a `validate()` explícitamente cuando quieras la verificación. ### En cron y ejecuciones diferidas En un disparo de cron o de una [ejecución diferida](/guides/functions/diferidas) no hay canal resuelto, así que `has()` siempre devolvería `false`. Lleva el `botId` en tu propio payload y enlaza el catálogo explícitamente: ```typescript theme={null} handler: async (input, ctx) => { // El botId viajó en el payload que agendaste antes const registro = ctx.templateRegistry.for(input.botId); if (await registro.has("recordatorio_cita", "es")) { await ctx.jelou.sendTemplate({ template: "recordatorio_cita", to: input.telefono, language: "es", params: [input.fecha], botId: input.botId, }); } } ``` `for(botId)` es seguro en cualquier ruta: en una petición normal simplemente reenlaza al canal indicado. Los resultados se guardan en caché **90 segundos**. Una plantilla aprobada hace un momento puede no aparecer hasta que la caché expire. ## Override de canal Todos los mensajes usan `ctx.bot.id` por defecto. Para enviar desde otro canal: ```typescript theme={null} await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Mensaje desde otro canal", botId: "bot-notificaciones-456", }); ``` ## Manejo de errores ```typescript theme={null} import { JelouApiError } from "@jelou/functions"; try { await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Hola" }); } catch (err) { if (err instanceof JelouApiError) { if (err.isRateLimit()) { ctx.log("Rate limit", { retryAfter: 2 }); return { error: "rate_limit" }; } if (err.isAuth()) { ctx.log("Credenciales inválidas", { status: err.status }); return { error: "auth_error" }; } if (err.isValidation()) { ctx.log("Datos inválidos", { status: err.status, code: err.code }); return { error: "validation_error" }; } } throw err; } ``` ## Testing Usa `createMockJelouClient()` para testing sin enviar mensajes reales: ```typescript theme={null} import { createMockContext, createMockJelouClient } from "@jelou/functions/testing"; const mockJelou = createMockJelouClient(); const ctx = createMockContext({ jelou: mockJelou }); await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Test" }); mockJelou.calls.length; // 1 mockJelou.calls[0].method; // "send" mockJelou.calls[0].args.text; // "Test" mockJelou.reset(); // limpia las llamadas ``` Referencia de createMockJelouClient, createMockMemoryClient y más. # Funciones multi-tool Source: https://docs.jelou.ai/guides/functions/multi-tool Agrupa múltiples herramientas en un solo despliegue con app(): rutas auto-generadas, MCP unificado, cron independiente y config compartida. **Preview** — Jelou Functions está en fase de vista previa. La API y el comportamiento pueden cambiar sin previo aviso. ## ¿Cuándo usar `app()` vs `define()`? | Patrón | Cuándo usarlo | | ---------- | ------------------------------------------------------------- | | `define()` | Una sola operación con un endpoint HTTP | | `app()` | Varias herramientas relacionadas que quieres desplegar juntas | Si tu proyecto tiene un solo handler, usa `define()`. Si necesitas agrupar múltiples operaciones — por ejemplo, enviar y leer emails desde el mismo servicio — usa `app()`. ## Ejemplo básico ```typescript index.ts theme={null} import { app, define, z } from "@jelou/functions"; export default app({ config: { cors: { origin: "*" }, timeout: 30_000 }, tools: { enviarEmail: define({ description: "Envía un correo electrónico", input: z.object({ to: z.string(), subject: z.string(), body: z.string() }), config: { timeout: 5_000 }, handler: async (input) => ({ sent: true }), }), leerBandeja: define({ description: "Lee los mensajes de la bandeja de entrada", input: z.object({ limit: z.coerce.number().default(10) }), handler: async (input) => ({ messages: [] }), }), }, }); ``` ## Prueba local Inicia el servidor con `jelou functions dev` y prueba cada tool en su ruta: ```bash curl /enviar-email theme={null} curl -X POST http://localhost:3000/enviar-email \ -H "Content-Type: application/json" \ -d '{"to": "maria@example.com", "subject": "Hola", "body": "Bienvenida"}' ``` ```json Respuesta 200 theme={null} { "sent": true } ``` ```bash curl /leer-bandeja theme={null} curl http://localhost:3000/leer-bandeja?limit=5 ``` ```json Respuesta 200 theme={null} { "messages": [] } ``` Si envías un campo inválido, recibes un `400` con detalles: ```bash curl con input inválido theme={null} curl -X POST http://localhost:3000/enviar-email \ -H "Content-Type: application/json" \ -d '{"to": ""}' ``` ```json Respuesta 400 theme={null} { "error": "Validation failed", "details": [ { "path": ["subject"], "message": "Required", "code": "invalid_type" }, { "path": ["body"], "message": "Required", "code": "invalid_type" } ] } ``` ## API ```typescript theme={null} import { app } from "@jelou/functions"; const edgeApp = app({ config: { cors: { origin: "*" }, timeout: 30_000 }, tools: { /* ... tus define() aquí ... */ }, }); export default edgeApp; ``` Firma de tipo: ```typescript theme={null} function app(options: { config?: AppConfig; tools: Record; }): EdgeApp; ``` ### `AppConfig` ```typescript theme={null} interface AppConfig { cors?: { origin?: string | string[]; methods?: string[]; headers?: string[]; credentials?: boolean; maxAge?: number; }; timeout?: number; methods?: string[]; mcp?: boolean; public?: boolean; } ``` | Campo | Tipo | Predeterminado | Descripción | | --------- | ---------- | --------------------------------------- | ----------------------------------------------------------- | | `cors` | `object` | `{ origin: "*" }` | Configuración CORS global | | `timeout` | `number` | `30000` | Timeout en ms para todos los tools | | `methods` | `string[]` | `["GET","POST","PUT","PATCH","DELETE"]` | Métodos HTTP permitidos | | `mcp` | `boolean` | `true` | Registrar tools en el servidor MCP | | `public` | `boolean` | `false` | Desactivar autenticación de plataforma para todos los tools | ## Rutas auto-generadas Tus keys del objeto `tools` se convierten automáticamente a kebab-case para generar las rutas HTTP: | Key | Ruta | | ------------- | --------------- | | `enviarEmail` | `/enviar-email` | | `leerBandeja` | `/leer-bandeja` | | `MiTool` | `/mi-tool` | Para personalizar una ruta, usa `config.path` en el `define()` individual: ```typescript theme={null} tools: { enviarEmail: define({ config: { path: "/emails/enviar" }, handler: async (input) => ({ sent: true }), }), } ``` ## Combinación de config La configuración global de `app()` se aplica a todos los tools. Cada `define()` puede sobreescribir valores específicos: | Campo | Comportamiento | | --------- | ------------------------------ | | `timeout` | El tool sobreescribe el global | | `methods` | El tool sobreescribe el global | | `cors` | El tool sobreescribe el global | | `mcp` | El tool sobreescribe el global | | `public` | El tool sobreescribe el global | | `path` | Siempre por tool | | `cron` | Siempre por tool | ```typescript theme={null} export default app({ config: { timeout: 30_000, cors: { origin: "*" } }, tools: { rapido: define({ config: { timeout: 5_000 }, handler: async () => ({ ok: true }), }), lento: define({ handler: async () => ({ ok: true }), }), }, }); ``` En este ejemplo, `rapido` tiene un timeout de 5 segundos y `lento` hereda los 30 segundos globales. Ambos comparten la configuración CORS. ## Endpoint de salud En modo app, `/__health` retorna información de todos los tools: ```json theme={null} { "mode": "app", "tools": [ { "key": "enviarEmail", "name": "Enviar Email", "description": "Envía un correo electrónico", "path": "/enviar-email", "cron": [] }, { "key": "leerBandeja", "name": "Leer Bandeja", "description": "Lee los mensajes de la bandeja de entrada", "path": "/leer-bandeja", "cron": [] } ] } ``` ## Guardas de tipo Usa `isEdgeApp()` e `isEdgeFunction()` para identificar el tipo de export en runtime: ```typescript theme={null} import { isEdgeApp, isEdgeFunction } from "@jelou/functions"; isEdgeApp(module); // true si fue creado con app() isEdgeFunction(module); // true si fue creado con define() ``` ## Multi-tool cron Cada tool dentro de `app()` puede tener sus propios schedules cron independientes. Las peticiones cron se envían a la ruta específica de cada tool. ```typescript theme={null} export default app({ tools: { limpiezaDiaria: define({ description: "Limpia registros obsoletos", input: z.object({}), config: { cron: [{ expression: "0 3 * * *", timezone: "UTC" }] }, handler: async (_input, ctx) => { if (!ctx.isCron) return { skipped: true }; return { cleaned: true }; }, }), sincronizacionHoraria: define({ description: "Sincroniza datos externos", input: z.object({}), config: { cron: [{ expression: "0 * * * *" }] }, handler: async (_input, ctx) => { if (!ctx.isCron) return { skipped: true }; return { synced: true }; }, }), }, }); ``` El límite de **10** schedules cron es **agregado** entre todos los tools de un `app()`. Si `limpiezaDiaria` tiene 3 y `sincronizacionHoraria` tiene 4, quedan 3 disponibles. ## Multi-tool MCP Un solo servidor MCP en `/mcp` registra automáticamente todos los tools del `app()`. Para excluir un tool del registro MCP, usa `mcp: false` en su config: ```typescript theme={null} export default app({ tools: { toolPublico: define({ description: "Visible para MCP", input: z.object({}), handler: async () => ({}), }), toolInterno: define({ description: "Solo HTTP", input: z.object({}), config: { mcp: false }, handler: async () => ({}), }), }, }); ``` En este ejemplo, los agentes IA solo descubren `toolPublico` vía MCP. `toolInterno` solo es accesible por HTTP directo en su ruta `/tool-interno`. ## Rutas expuestas (modo app) | Ruta | Descripción | | ------------- | --------------------------------------------------------------- | | `/__health` | Health check con lista de tools | | `/mcp` | Servidor MCP unificado (a menos que `config.mcp: false` global) | | `/` | Ruta de cada tool (kebab-case o `config.path` personalizado) | ## Problemas comunes Las keys de `tools` se convierten a **kebab-case**. Si tu key es `enviarEmail`, la ruta es `/enviar-email`, no `/enviarEmail`. Verifica las rutas reales con el health check: ```bash curl theme={null} curl http://localhost:3000/__health ``` ```json Respuesta theme={null} { "mode": "app", "tools": [ { "key": "enviarEmail", "path": "/enviar-email" }, { "key": "leerBandeja", "path": "/leer-bandeja" } ] } ``` La validación del schema se ejecuta antes que el handler. Si recibes un `400`, revisa el array `details`: ```bash curl theme={null} curl -X POST http://localhost:3000/enviar-email \ -H "Content-Type: application/json" \ -d '{"to": 12345}' ``` ```json Respuesta 400 theme={null} { "error": "Validation failed", "details": [ { "path": ["to"], "message": "Expected string, received number", "code": "invalid_type" }, { "path": ["subject"], "message": "Required", "code": "invalid_type" }, { "path": ["body"], "message": "Required", "code": "invalid_type" } ] } ``` Cada entrada en `details` indica el campo (`path`), qué esperaba (`message`) y el código Zod (`code`). Si un tool tiene `config: { mcp: false }`, no se registra en el servidor MCP. Verifica qué tools están expuestos: ```bash curl theme={null} curl http://localhost:3000/mcp ``` ```json Respuesta theme={null} { "tools": [ { "name": "enviar-email", "description": "Envía un correo electrónico", "inputSchema": { "..." } } ] } ``` Si falta un tool, revisa que no tenga `mcp: false` en su `config`. ## Public per-tool Puedes mezclar tools públicos y protegidos en el mismo `app()`: ```typescript theme={null} export default app({ tools: { webhookPagos: define({ description: "Recibe callbacks de Stripe", input: z.object({ event: z.string() }), config: { public: true, mcp: false }, handler: async (input, ctx) => { // Cualquier cliente puede llamar — valida la firma return { acknowledged: true }; }, }), consultarSaldo: define({ description: "Consulta saldo de un cliente", input: z.object({ telefono: z.string() }), handler: async (input) => { // Requiere X-Jelou-Token return { saldo: 150.00 }; }, }), }, }); ``` En `/__health`, cada tool reporta su estado `public`: ```json theme={null} { "tools": [ { "key": "webhookPagos", "path": "/webhook-pagos", "public": true }, { "key": "consultarSaldo", "path": "/consultar-saldo", "public": false } ] } ``` Consulta la [guía de funciones públicas](/guides/functions/public) para más detalles sobre `public` y cómo validar webhooks. ## Siguientes pasos Aprende a probar funciones multi-tool con `createMockApp()`. Configura el servidor MCP unificado para tus agentes IA. Schedules independientes por tool dentro de `app()`. # OpenAPI Source: https://docs.jelou.ai/guides/functions/openapi Cada función genera automáticamente una especificación OpenAPI 3.1 accesible en /openapi.json. ## Endpoint automático Toda función desplegada expone su especificación OpenAPI en: ``` https://.fn.jelou.ai/openapi.json ``` Este endpoint es **siempre público** — no requiere `X-Jelou-Token`, sin importar la configuración de autenticación. ## Ejemplo Para una función con esta definición: ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "consultar-saldo", description: "Consulta el saldo de un cliente por teléfono", input: z.object({ telefono: z.string().min(10).describe("Número con código de país"), }), output: z.object({ nombre: z.string(), saldo: z.number(), }), handler: async (input) => ({ nombre: "María García", saldo: 150.00, }), }); ``` La spec generada en `/openapi.json`: ```json theme={null} { "openapi": "3.1.0", "info": { "title": "consultar-saldo", "version": "1.0.0" }, "paths": { "/": { "post": { "operationId": "consultar-saldo", "summary": "Consulta el saldo de un cliente por teléfono", "security": [{ "jelouToken": [] }], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "telefono": { "type": "string", "minLength": 10, "description": "Número con código de país" } }, "required": ["telefono"] } } } }, "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "nombre": { "type": "string" }, "saldo": { "type": "number" } } } } } } } } } }, "components": { "securitySchemes": { "jelouToken": { "type": "apiKey", "in": "header", "name": "X-Jelou-Token" } } } } ``` ## Funciones públicas vs protegidas | Config | `security` en la spec | | -------------------------- | ------------------------ | | Sin `public` (default) | `[{ "jelouToken": [] }]` | | `config: { public: true }` | `[]` (sin seguridad) | En modo `app()`, cada tool puede tener su propia configuración de `public`, y la spec refleja esto por ruta. ## Multi-tool En modo `app()`, la spec incluye una ruta por cada tool: ```json theme={null} { "paths": { "/crear-contacto": { "post": { "operationId": "crearContacto", ... } }, "/buscar-contactos": { "get": { "operationId": "buscarContactos", ... } }, "/eliminar-contacto": { "post": { "operationId": "eliminarContacto", ... } } } } ``` ## Probar en local ```bash theme={null} jelou functions dev curl http://localhost:3000/openapi.json | jq . ``` ## Uso con Swagger UI Puedes visualizar la spec con cualquier herramienta compatible con OpenAPI: ```bash theme={null} # Descargar la spec curl https://mi-funcion.fn.jelou.ai/openapi.json -o openapi.json # Abrirla en Swagger Editor online # Pega el contenido en https://editor.swagger.io ``` Las anotaciones `.describe()` de Zod se convierten en el campo `description` de cada propiedad en la spec. Esto mejora la documentación automática de tu API. # Funciones públicas Source: https://docs.jelou.ai/guides/functions/public Expón funciones sin autenticación con config: { public: true } para webhooks, callbacks y APIs públicas. ## ¿Cuándo usar funciones públicas? Usa `config: { public: true }` cuando el llamador no puede enviar un `X-Jelou-Token`: * **Webhooks** de servicios externos (Stripe, GitHub, Twilio) * **Callbacks** de pasarelas de pago * **APIs públicas** accesibles desde el navegador ## En modo `define()` ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ description: "Webhook de Stripe — recibe eventos de pago", input: z.object({ type: z.string(), data: z.object({ id: z.string(), amount: z.number() }), }), config: { public: true, methods: ["POST"], mcp: false, }, handler: async (input, ctx) => { ctx.log("Stripe webhook", { type: input.type, id: input.data.id }); return { received: true }; }, }); ``` Con `public: true` en `define()`, la autenticación de plataforma se omite **completamente** — ninguna ruta requiere token. ## En modo `app()` — global ```typescript theme={null} export default app({ config: { public: true }, tools: { webhookPagos: define({ description: "Recibe pagos", input: z.object({ event: z.string() }), config: { mcp: false }, handler: async (input) => ({ ok: true }), }), webhookEnvios: define({ description: "Recibe actualizaciones de envío", input: z.object({ trackingId: z.string() }), config: { mcp: false }, handler: async (input) => ({ ok: true }), }), }, }); ``` Todos los tools son públicos. ## En modo `app()` — per-tool ```typescript theme={null} export default app({ tools: { webhookPagos: define({ description: "Webhook público de pagos", input: z.object({ event: z.string() }), config: { public: true, mcp: false }, handler: async (input) => ({ acknowledged: true }), }), consultarSaldo: define({ description: "Consulta saldo (requiere token)", input: z.object({ telefono: z.string() }), handler: async (input) => ({ saldo: 150.00 }), }), }, }); ``` Solo `/webhook-pagos` es público. `/consultar-saldo` requiere `X-Jelou-Token`. ## Override: app público, tool protegido ```typescript theme={null} export default app({ config: { public: true }, tools: { apiPublica: define({ description: "Endpoint público", input: z.object({}), handler: async () => ({ status: "ok" }), }), adminProtegido: define({ description: "Panel de admin (requiere token)", input: z.object({ action: z.string() }), config: { public: false }, handler: async (input) => ({ done: true }), }), }, }); ``` `public: false` en el tool **sobrescribe** el `public: true` global. ## Health check `/__health` reporta el estado `public` de cada tool: ```json theme={null} { "mode": "app", "tools": [ { "key": "webhookPagos", "path": "/webhook-pagos", "public": true }, { "key": "consultarSaldo", "path": "/consultar-saldo", "public": false } ] } ``` ## OpenAPI La spec generada en `/openapi.json` refleja la configuración de seguridad: * Tools protegidos: `"security": [{ "jelouToken": [] }]` * Tools públicos: `"security": []` ## Seguridad: validar webhooks Con funciones públicas, la plataforma no valida nada. **Tu código es responsable** de verificar la autenticidad: ```typescript theme={null} handler: async (input, ctx, request) => { const signature = request.headers.get("x-webhook-signature"); const secret = ctx.env.get("WEBHOOK_SECRET"); if (!signature || !secret) { return { error: "missing_signature" }; } // Verificar HMAC-SHA256 const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"], ); const body = await request.clone().text(); const sigBytes = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)); const valid = await crypto.subtle.verify("HMAC", key, sigBytes, encoder.encode(body)); if (!valid) { ctx.log("Firma inválida", { signature }); return { error: "invalid_signature" }; } // Firma válida — procesar el evento ctx.log("Webhook verificado", { event: input.type }); return { acknowledged: true }; } ``` ```bash theme={null} jelou functions secrets set mi-webhook WEBHOOK_SECRET=whsec_... ``` Nunca confíes en una función pública sin validar la firma. Cualquier persona puede enviar peticiones a la URL. ## Rutas siempre públicas Estas rutas nunca requieren token, independientemente de la configuración `public`: | Ruta | Descripción | | --------------- | ----------------------- | | `/__health` | Health check y metadata | | `/openapi.json` | Especificación OpenAPI | # Inicio rápido Source: https://docs.jelou.ai/guides/functions/quickstart Crea, prueba y despliega tu primera Jelou Function en menos de 5 minutos. ```bash theme={null} npm install -g @jelou/cli ``` Verifica la instalación: ```bash theme={null} jelou --version ``` Necesitas un token de acceso personal (créalo en el dashboard de Jelou). ```bash theme={null} jelou login # Paste your access token: **** # ✓ Logged in ``` En CI puedes pasar el token directamente: `jelou login --token jfn_pat_tu_token` Crea un directorio y ejecuta `jelou functions init`: ```bash theme={null} mkdir consultar-cliente && cd consultar-cliente jelou functions init # ? Function slug: consultar-cliente # ? Description: Busca información de clientes para el canal de WhatsApp # ? Create new or link existing? Create new # ✓ Created consultar-cliente ``` Esto te genera los archivos base: | Archivo | Propósito | | ------------ | ---------------------------- | | `index.ts` | Tu función principal | | `jelou.json` | Configuración del proyecto | | `deno.json` | Import map del SDK | | `.env` | Variables de entorno locales | Reemplaza el contenido de `index.ts` con una función que busca clientes por teléfono: ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "consultar-cliente", description: "Busca información de un cliente por número de teléfono para el canal de soporte", input: z.object({ telefono: z.string().min(10).describe("Número de teléfono del cliente"), }), output: z.object({ nombre: z.string(), email: z.string(), plan: z.string(), saldo: z.number(), }), handler: async (input, ctx) => { ctx.log("Buscando cliente", { telefono: input.telefono, company: ctx.company.id, }); const apiKey = ctx.env.get("CRM_API_KEY"); const res = await fetch( `https://crm.example.com/api/clientes?tel=${input.telefono}`, { headers: { Authorization: `Bearer ${apiKey}` } }, ); const cliente = await res.json(); return { nombre: cliente.nombre, email: cliente.email, plan: cliente.plan, saldo: cliente.saldo, }; }, }); ``` Inicia el servidor de desarrollo: ```bash theme={null} jelou functions dev # ▸ Listening on http://localhost:3000 # ▸ Watching for changes... ``` Tu servidor se recarga automáticamente cuando editas archivos. En otra terminal, envía una petición: ```bash theme={null} curl -X POST http://localhost:3000 \ -H "Content-Type: application/json" \ -d '{"telefono": "593987654321"}' ``` Respuesta: ```json theme={null} { "nombre": "María García", "email": "maria@example.com", "plan": "Premium", "saldo": 150.00 } ``` También puedes probar el endpoint MCP en `http://localhost:3000/mcp` y el health check en `http://localhost:3000/__health`. Antes de desplegar, agrega las variables de entorno que necesita tu función: ```bash theme={null} jelou functions secrets set consultar-cliente CRM_API_KEY=YOUR_CRM_API_KEY # ✓ Set 1 secret ``` ```bash theme={null} jelou functions deploy # ▸ Files: index.ts (1.2 KB), jelou.json (98 B), deno.json (65 B) # ? Deploy consultar-cliente? (Y/n) y # ✓ Deployed to https://consultar-cliente.fn.jelou.ai ``` Tu función queda protegida por defecto. Para llamarla necesitas una API key de plataforma — créala en [apps.jelou.ai](https://apps.jelou.ai), en la sección de configuración de la app, y envíala como `Authorization: Bearer sk_...`. Consulta la [guía de autenticación](/guides/functions/autenticacion) para más detalles. Tu función ya está disponible en producción. Los agentes IA de Jelou pueden invocarla como herramienta MCP automáticamente. ```bash theme={null} curl -X POST https://consultar-cliente.fn.jelou.ai \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_..." \ -d '{"telefono": "593987654321"}' ``` Monitorea los logs en tiempo real: ```bash theme={null} jelou functions logs consultar-cliente ``` ## Siguientes pasos * [Validación de entrada/salida](/guides/functions/validacion) — esquemas Zod, coerción de tipos y formato de errores * [Funciones multi-tool](/guides/functions/multi-tool) — agrupa varias herramientas en un solo despliegue con `app()` # Respuestas HTTP Source: https://docs.jelou.ai/guides/functions/respuestas Controla el status code y headers de tus respuestas con el response builder: status personalizados, headers custom y comportamiento en MCP. ## Comportamiento por defecto Cuando tu handler retorna un objeto plano, la plataforma responde con `200 OK` y `Content-Type: application/json`: ```typescript theme={null} handler: async (input) => { return { nombre: "María", saldo: 150.00 }; } // → 200 OK // → { "nombre": "María", "saldo": 150.00 } ``` Esto es suficiente para la mayoría de los casos. Pero cuando necesitas un status code diferente o headers personalizados, usa el **response builder**. ## Response builder Importa `response` desde `@jelou/functions`: ```typescript theme={null} import { define, response, z } from "@jelou/functions"; ``` ### Status code personalizado ```typescript theme={null} export default define({ description: "Crea un usuario", input: z.object({ nombre: z.string(), email: z.string().email() }), output: z.object({ id: z.string(), nombre: z.string() }), handler: async (input) => { const usuario = await crearUsuario(input); return response .status(201) .json({ id: usuario.id, nombre: usuario.nombre, }); }, }); ``` ### Headers personalizados ```typescript theme={null} handler: async (input) => { return response .header("X-Request-Id", crypto.randomUUID()) .header("Cache-Control", "max-age=300") .json({ datos: [] }); } ``` ### Múltiples headers de una vez ```typescript theme={null} handler: async (input) => { return response .headers({ "X-Request-Id": crypto.randomUUID(), "X-Powered-By": "Jelou Functions", "Cache-Control": "no-store", }) .json({ ok: true }); } ``` ### Encadenamiento completo El builder es **inmutable** — cada método retorna una nueva instancia sin modificar la anterior: ```typescript theme={null} handler: async (input) => { return response .status(201) .header("Location", `/usuarios/${input.id}`) .header("X-Created-By", "api") .json({ id: input.id, nombre: input.nombre, }); } // → 201 Created // → Location: /usuarios/abc123 // → X-Created-By: api // → { "id": "abc123", "nombre": "María" } ``` ## API ```typescript theme={null} interface JsonResponseBuilder { status(code: number): JsonResponseBuilder; header(name: string, value: string): JsonResponseBuilder; headers(init: HeadersInit | Record): JsonResponseBuilder; json>(body: TBody): JsonResponse; } ``` | Método | Descripción | | ------------------------------ | --------------------------------------- | | `response.status(code)` | Establece el status HTTP (default: 200) | | `response.header(name, value)` | Agrega un header | | `response.headers(init)` | Agrega múltiples headers | | `response.json(body)` | Finaliza con un body JSON | | `response.noContent()` | Finaliza con `204 No Content` sin body | `.json()` y `.noContent()` son los métodos terminales — después de llamarlos obtienes un response final, no un builder. Los métodos `.status()`, `.header()` y `.headers()` retornan un nuevo builder encadenable. ## Ejemplos prácticos ```typescript theme={null} handler: async (input) => { const ticket = await crearTicket(input); return response .status(201) .header("Location", `/tickets/${ticket.id}`) .json({ ticketId: ticket.id, estado: "abierto" }); } ``` ```typescript theme={null} handler: async (input) => { const cliente = await buscarCliente(input.telefono); if (!cliente) { return response .status(404) .json({ error: "not_found", message: "Cliente no encontrado" }); } return { nombre: cliente.nombre, saldo: cliente.saldo }; } ``` ```typescript theme={null} handler: async (input) => { const productos = await listarProductos(input.categoria); return response .header("Cache-Control", "public, max-age=300") .header("ETag", `"${hashProductos(productos)}"`) .json({ productos, total: productos.length }); } ``` ```typescript theme={null} handler: async (input, ctx) => { ctx.log("Webhook recibido", { event: input.event }); procesarEvento(input).catch((err) => ctx.log("Error procesando evento", { error: err.message }) ); return response .status(202) .json({ acknowledged: true }); } ``` ```typescript theme={null} handler: async (input, ctx) => { await eliminarRecurso(input.id); return response.noContent(); } // → 204 No Content (sin body) ``` ## Validación de output Cuando usas `response.json(body)` con un schema `output` definido, la validación se aplica al **body** del response — exactamente igual que con objetos planos: ```typescript theme={null} export default define({ description: "Crear usuario", input: z.object({ nombre: z.string() }), output: z.object({ id: z.string(), nombre: z.string() }), handler: async (input) => { return response .status(201) .json({ id: "usr_123", nombre: input.nombre, // extra: "este campo no está en output pero no causa error" }); }, }); ``` La validación de output nunca bloquea la respuesta. Si el body no coincide con el schema, se registra un warning en los logs pero el cliente recibe la respuesta con el status que configuraste. ## Funciona en `app()` también ```typescript theme={null} import { app, define, response, z } from "@jelou/functions"; export default app({ tools: { crearContacto: define({ description: "Crea un contacto en el CRM", input: z.object({ nombre: z.string(), email: z.string().email() }), handler: async (input) => { return response .status(201) .header("X-Contact-Created", "true") .json({ id: "ct_abc", nombre: input.nombre }); }, }), buscarContacto: define({ description: "Busca un contacto por email", input: z.object({ email: z.string() }), handler: async (input) => { const contacto = await buscar(input.email); if (!contacto) { return response.status(404).json({ error: "not_found" }); } return contacto; // 200 por defecto }, }), }, }); ``` ## Comportamiento en MCP Cuando tu función es invocada vía MCP (por un agente IA en Brain Studio), el response builder funciona diferente: * El **body** se emite como `structuredContent` del tool result * El **status code** y los **headers** se **ignoran** — MCP no tiene concepto de HTTP status * También se envía el body como texto JSON para compatibilidad con clientes MCP que esperan `content[].text` ```typescript theme={null} // HTTP: → 201 Created + X-User-Created: true + { id: "usr_123", nombre: "María" } // MCP: → structuredContent: { id: "usr_123", nombre: "María" } (status y headers ignorados) handler: async (input) => { return response .status(201) .header("X-User-Created", "true") .json({ id: "usr_123", nombre: input.nombre }); } ``` No necesitas condicionar tu código para HTTP vs MCP — usa el response builder normalmente. La plataforma extrae el body y descarta los metadatos HTTP cuando la invocación es vía MCP. ## Content-Type El response builder **siempre** retorna `application/json`. Si intentas sobreescribir `Content-Type`, la plataforma lo fuerza de vuelta a `application/json`: ```typescript theme={null} // El Content-Type siempre será application/json response.header("Content-Type", "text/plain").json({ ok: true }); // → Content-Type: application/json ``` Para respuestas no-JSON (binarios, HTML, etc.), usa **raw mode** en lugar del response builder. ## Limitaciones | Limitación | Detalle | | ----------------------- | ---------------------------------------------------------------------------------- | | JSON o vacío | Solo `.json()` y `.noContent()` — no hay `.text()`, `.html()`, `.blob()` | | Sin Web Response nativo | Retornar un `new Response()` desde `define()` / `app()` no funciona (produce `{}`) | | Content-Type fijo | Siempre `application/json` en `.json()`, no se puede sobreescribir | | OpenAPI | La spec en `/openapi.json` solo documenta respuesta `200` por ahora | Para necesidades más avanzadas (streaming, binarios, status dinámico basado en content-type), usa raw mode. ## Mezclar con objetos planos Puedes retornar objetos planos o `response.json()` desde el mismo handler — la plataforma detecta automáticamente cuál usas: ```typescript theme={null} handler: async (input) => { if (input.action === "crear") { return response.status(201).json({ id: "new-123", creado: true }); } // Objeto plano → 200 por defecto return { id: "existing-456", creado: false }; } ``` # Secretos Source: https://docs.jelou.ai/guides/functions/secrets Gestiona variables de entorno cifradas: tres modos para configurarlas, acceso con ctx.env, restricciones de prefijo y uso en CI. ## ¿Qué son los secrets? Los secrets son variables de entorno cifradas que accedes en runtime a través de `ctx.env`. Úsalos para API keys, URLs de base de datos, tokens de servicios externos y cualquier valor sensible. ## Configurar secrets Tienes tres formas de configurar secrets con el CLI: Pasa pares `KEY=VALUE` directamente: ```bash theme={null} jelou functions secrets set consultar-cliente CRM_API_KEY=sk_test_EXAMPLE JELOU_API_KEY=jfn_pat_EXAMPLE # ✓ Set 2 secrets ``` Importa desde un archivo `.env`: ```bash theme={null} jelou functions secrets set consultar-cliente --from-env .env.production ``` El archivo sigue el formato estándar: ```bash .env.production theme={null} CRM_API_KEY=sk_test_EXAMPLE JELOU_API_KEY=jfn_pat_EXAMPLE DATABASE_URL=postgres://user:pass@host:5432/db ``` Se ignoran líneas vacías y comentarios (`#`). Sin argumentos, el CLI te guía paso a paso: ```bash theme={null} jelou functions secrets set consultar-cliente # ? Secret key: CRM_API_KEY # ? Secret value: **** # ? Add another secret? (y/N) y # ? Secret key: JELOU_API_KEY # ? Secret value: **** # ? Add another secret? (y/N) n # ✓ Set 2 secrets ``` Los valores se enmascaran durante la entrada. Las claves deben ser `UPPER_SNAKE_CASE` (patrón: `^[A-Z][A-Z0-9_]*$`). ## Acceder a secrets en tu función Usa `ctx.env` dentro del handler: ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "enviar-whatsapp", description: "Envía un mensaje de WhatsApp usando la API de Jelou", input: z.object({ telefono: z.string().min(10), mensaje: z.string().min(1), }), handler: async (input, ctx) => { const apiKey = ctx.env.get("JELOU_API_KEY"); const botId = ctx.env.get("BOT_ID"); if (!apiKey) { ctx.log("Error: JELOU_API_KEY no configurada"); return { enviado: false, error: "API key faltante" }; } const res = await fetch("https://api.jelou.ai/v1/messages/send", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ botId, phone: input.telefono, message: input.mensaje, }), }); return { enviado: res.ok, status: res.status }; }, }); ``` ### Métodos de `ctx.env` | Método | Retorna | Descripción | | -------------------- | ------------------------ | ------------------------------------- | | `ctx.env.get("KEY")` | `string \| undefined` | Obtiene el valor de un secret | | `ctx.env.has("KEY")` | `boolean` | Verifica si un secret existe | | `ctx.env.toObject()` | `Record` | Obtiene todos los secrets como objeto | ## Variables bloqueadas Las variables internas de la plataforma con prefijo `__FN_` están bloqueadas. Llamar a `ctx.env.get("__FN_COMPANY_ID")` retorna `undefined`. ## Desarrollo local En desarrollo local, el servidor de `jelou functions dev` carga secrets desde tu archivo `.env`: ```bash .env theme={null} CRM_API_KEY=sk-test-local123 JELOU_API_KEY=jfn_pat_test_local DATABASE_URL=postgres://localhost:5432/mydb ``` Puedes especificar un archivo diferente: ```bash theme={null} jelou functions dev --env .env.local ``` Nunca subas tu archivo `.env` al repositorio. `jelou functions init` lo agrega automáticamente al `.gitignore`. ## Listar y eliminar secrets ```bash theme={null} jelou functions secrets list consultar-cliente # ▸ Key Updated # ▸ CRM_API_KEY 2 hours ago # ▸ JELOU_API_KEY 3 days ago jelou functions secrets delete consultar-cliente CRM_API_KEY # ✓ Deleted CRM_API_KEY ``` ## Secrets en CI/CD En pipelines de CI, usa variables de entorno del sistema para inyectar secrets: ```yaml deploy.yml theme={null} - name: Configure secrets and deploy env: JELOU_TOKEN: ${{ secrets.JELOU_TOKEN }} run: | jelou functions secrets set consultar-cliente \ CRM_API_KEY=${{ secrets.CRM_API_KEY }} \ JELOU_API_KEY=${{ secrets.JELOU_API_KEY }} jelou functions deploy --no-confirm ``` # Codear con IA Source: https://docs.jelou.ai/guides/functions/skill-ia Instala el flujo de Jelou Functions en tu herramienta de IA para que entienda el API define(), los comandos del CLI y las convenciones de la plataforma. Con `jelou agent install` instalas un archivo `SKILL.md` en tu herramienta de codificación con IA. Este archivo le enseña todo sobre Jelou Functions: el API `define()`, el objeto `ctx`, validación con Zod, cron, testing y los comandos del CLI. Con esto, tu herramienta puede escribir código correcto para la plataforma desde el primer prompt. ## Instalación Desde la raíz de tu proyecto de Jelou Functions: ```bash theme={null} jelou agent install ``` El CLI detecta automáticamente qué herramientas tienes instaladas: ```bash theme={null} ✓ Installed jelou-functions skill for 4 agents ▸ Claude Code ▸ Cursor ▸ Windsurf ▸ Cline Installed locally — commit .agents/ to share with your team. ``` El flujo se activa automáticamente cuando la herramienta detecta código relacionado con Jelou Functions (`define()`, `@jelou/functions`, `ctx.env`, etc.). ## Herramientas soportadas El instalador detecta 12 herramientas de codificación con IA. Cada una usa su propio directorio de workflows, pero todas reciben el mismo contenido. **Directorio:** `.claude/skills/jelou-functions/SKILL.md` Se detecta automáticamente. El flujo aparece como contexto disponible en tus conversaciones. **Directorio:** `.cursor/skills/jelou-functions/SKILL.md` Se detecta automáticamente. Cursor carga el flujo cuando trabajas en archivos de Jelou Functions. **Directorio:** `.windsurf/skills/jelou-functions/SKILL.md` Se detecta automáticamente al abrir el proyecto. **Directorio compartido:** `.agents/skills/jelou-functions/SKILL.md` Las siguientes herramientas leen del directorio universal `.agents/skills/`: | Herramienta | Detección | | -------------- | ---------------------------------------- | | Amp | `~/.config/amp` | | Codex | `~/.codex` | | Gemini CLI | `~/.gemini` | | GitHub Copilot | `.github/` en el proyecto o `~/.copilot` | | OpenCode | `~/.config/opencode` | | Kimi Code CLI | `~/.kimi` | Las siguientes herramientas tienen su propio directorio de workflows: | Herramienta | Directorio | | ----------- | ------------------- | | Cline | `.cline/skills/` | | Continue | `.continue/skills/` | | Roo Code | `.roo/skills/` | El instalador crea un symlink desde cada directorio hacia la copia canónica en `.agents/skills/`. ## Cómo usarlo Una vez instalado, tu herramienta de IA entiende la plataforma completa. Prueba con prompts como: ``` Crea una función que consulte el saldo de un cliente por teléfono ``` ``` Agrega validación Zod al input con .describe() para que el MCP tenga buenas descripciones ``` ``` Configura un cron que se ejecute cada hora para limpiar registros viejos ``` ``` Escribe tests con createMockContext para la función de consulta de clientes ``` El flujo se activa automáticamente cuando la herramienta detecta patrones como `define()`, `@jelou/functions`, `ctx.env`, `ctx.isCron`, `createMockContext`, o comandos `jelou`. ## Local vs global | Modo | Comando | Ubicación | Uso | | ------ | ------------------------------ | ------------------------------ | -------------------------------------------- | | Local | `jelou agent install` | `.agents/skills/` del proyecto | Por proyecto, comparte con el equipo via git | | Global | `jelou agent install --global` | Directorio home (`~`) | Activo en todos los proyectos | **Local** es ideal para equipos: al hacer commit de `.agents/skills/`, cualquier miembro que clone el repo tendrá el flujo disponible sin ejecutar nada. **Global** es útil si trabajas en múltiples proyectos de Jelou Functions y quieres el flujo siempre activo. Haz commit de `.agents/skills/` en tu repositorio para que todo tu equipo tenga el flujo automáticamente al clonar el proyecto. # Pruebas Source: https://docs.jelou.ai/guides/functions/testing Utilidades de pruebas para Jelou Functions: mock de context, cron, eventos, requests y clientes de mensajería/memoria. ## Configuración Importa las utilidades de testing desde `@jelou/functions/testing`: ```typescript theme={null} import { createMockContext, createMockCronContext, createMockEventContext, createMockRequest, createMockPlatformRequest, createMockJelouClient, createMockMemoryClient, } from "@jelou/functions/testing"; ``` ## `createMockContext(overrides?)` Crea un context con valores por defecto sensatos para testing. ```typescript theme={null} const ctx = createMockContext(); const ctx2 = createMockContext({ company: { id: 42, name: "Tienda ABC" }, user: { id: 99, names: "María García" }, bot: { id: "bot-123", name: "Canal de Soporte", channel: "whatsapp" }, env: { get: (key) => key === "CRM_API_KEY" ? "sk-test-123" : undefined, has: (key) => key === "CRM_API_KEY", toObject: () => ({ CRM_API_KEY: "sk-test-123" }), }, }); ``` ## `createMockCronContext(expression, overrides?, cronName?)` Crea un context con `isCron: true` y `trigger.type: "cron"`. ```typescript theme={null} const ctx = createMockCronContext("0 9 * * *"); // ctx.isCron === true // ctx.trigger === { type: "cron", cron: "0 9 * * *" } // ctx.isHttp === false const ctx2 = createMockCronContext("0 9 * * *", {}, "recordatorio-diario"); // ctx2.trigger === { type: "cron", cron: "0 9 * * *", cronName: "recordatorio-diario" } ``` ## `createMockEventContext(eventName, overrides?)` Crea un context con `isEvent: true` y `trigger.type: "event"`. ```typescript theme={null} const ctx = createMockEventContext("pago.completado"); // ctx.isEvent === true // ctx.trigger === { type: "event", event: "pago.completado" } ``` ## `createMockRequest(body?, options?)` Crea un objeto Web `Request` estándar. ```typescript theme={null} const req = createMockRequest(); // GET http://localhost:8000/ const req2 = createMockRequest({ telefono: "593987654321" }); // POST con body JSON, Content-Type: application/json const req3 = createMockRequest(null, { method: "PUT", url: "https://consultar-cliente.fn.jelou.ai/clientes/42", headers: { "x-api-key": "sk-test-123" }, }); ``` ## `createMockPlatformRequest(token, body?, options?)` Crea un `Request` con header `X-Jelou-Token`. Útil para testing de funciones que reciben peticiones autenticadas con runtime tokens. ```typescript theme={null} const req = createMockPlatformRequest("jfn_rt_test123"); // GET con X-Jelou-Token: jfn_rt_test123 const req2 = createMockPlatformRequest("jfn_rt_test123", { query: "test" }); // POST con body JSON + X-Jelou-Token: jfn_rt_test123 const req3 = createMockPlatformRequest("jfn_rt_test123", null, { method: "PUT", url: "https://mi-funcion.fn.jelou.ai/admin", headers: { "x-custom": "value" }, }); // PUT con X-Jelou-Token + headers adicionales ``` `createMockPlatformRequest` es un wrapper sobre `createMockRequest` que agrega el header `x-jelou-token` automáticamente. Es equivalente a: ```typescript theme={null} createMockRequest(body, { ...options, headers: { ...options?.headers, "x-jelou-token": token } }) ``` ## `createMockJelouClient(options?)` Crea un mock del cliente de mensajería con grabación de llamadas. ```typescript theme={null} const mockJelou = createMockJelouClient(); const ctx = createMockContext({ jelou: mockJelou }); await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Hola" }); // Inspeccionar llamadas mockJelou.calls.length; // 1 mockJelou.calls[0].method; // "send" mockJelou.calls[0].args.type; // "text" mockJelou.calls[0].timestamp; // Date.now() ``` ### Resultados personalizados ```typescript theme={null} const mockJelou = createMockJelouClient({ sendResult: { messageId: "msg-custom-123" }, templateResult: [{ id: "tmpl-1", destination: "+593987654321" }], }); ``` ### Reset entre tests ```typescript theme={null} mockJelou.reset(); // limpia las llamadas grabadas ``` ## `createMockMemoryClient(options?)` Crea un mock del cliente de memoria con un store in-memory. ```typescript theme={null} const mockMemory = createMockMemoryClient({ store: { paso: "inicio", intentos: 0 }, }); const ctx = createMockContext({ memory: mockMemory }); const paso = await ctx.memory.get("paso", "desconocido"); // "inicio" await ctx.memory.set("paso", "confirmacion", 3600); ``` ### Inspeccionar llamadas ```typescript theme={null} mockMemory.calls.length; // 2 (get + set) mockMemory.calls[1].method; // "set" mockMemory.calls[1].args; // { key: "paso", value: "confirmacion", ttl: 3600 } ``` ### Reset entre tests ```typescript theme={null} mockMemory.reset(); // limpia las llamadas y restaura el store inicial ``` ## Ejemplo completo ```typescript index.test.ts theme={null} import { assertEquals } from "jsr:@std/assert"; import { define, z } from "@jelou/functions"; import { createMockContext, createMockCronContext, createMockRequest, createMockPlatformRequest, createMockJelouClient, createMockMemoryClient, } from "@jelou/functions/testing"; const fn = define({ name: "consultar-cliente", description: "Busca información de un cliente por teléfono", input: z.object({ telefono: z.string().min(10), }), output: z.object({ nombre: z.string(), plan: z.string(), companyId: z.number(), }), handler: async (input, ctx) => { return { nombre: "María García", plan: "Premium", companyId: ctx.company.id, }; }, }); Deno.test("retorna info del cliente con el company ID correcto", async () => { const ctx = createMockContext({ company: { id: 42, name: "Tienda ABC" }, bot: { id: "bot-123", name: "Canal Soporte", channel: "whatsapp" }, }); const req = createMockRequest({ telefono: "593987654321" }); const result = await fn.handler( { telefono: "593987654321" }, ctx, req, ); assertEquals(result.nombre, "María García"); assertEquals(result.plan, "Premium"); assertEquals(result.companyId, 42); }); Deno.test("cron skip cuando no es trigger cron", async () => { const cronFn = define({ name: "limpieza-diaria", input: z.object({}), config: { cron: [{ expression: "0 3 * * *" }], }, handler: async (_input, ctx) => { if (!ctx.isCron) return { skipped: true }; return { cleaned: true }; }, }); const httpCtx = createMockContext(); const req = createMockRequest(); const result = await cronFn.handler({}, httpCtx, req); assertEquals(result, { skipped: true }); const cronCtx = createMockCronContext("0 3 * * *"); const result2 = await cronFn.handler({}, cronCtx, req); assertEquals(result2, { cleaned: true }); }); Deno.test("mock de mensajería graba llamadas", async () => { const mockJelou = createMockJelouClient(); const ctx = createMockContext({ jelou: mockJelou }); await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Tu pedido está listo", }); assertEquals(mockJelou.calls.length, 1); assertEquals(mockJelou.calls[0].method, "send"); assertEquals(mockJelou.calls[0].args.type, "text"); }); Deno.test("mock de memoria persiste valores", async () => { const mockMemory = createMockMemoryClient({ store: { paso: "inicio" }, }); const ctx = createMockContext({ memory: mockMemory }); const paso = await ctx.memory.get("paso", "desconocido"); assertEquals(paso, "inicio"); await ctx.memory.set("paso", "confirmacion", 3600); const paso2 = await ctx.memory.get("paso", "desconocido"); assertEquals(paso2, "confirmacion"); }); Deno.test("platform request incluye header de token", () => { const req = createMockPlatformRequest("jfn_rt_test123", { action: "test" }); assertEquals(req.headers.get("x-jelou-token"), "jfn_rt_test123"); assertEquals(req.method, "POST"); }); ``` ## `createMockApp(tools, config?)` Crea un `EdgeApp` mock para testing de funciones multi-tool. ```typescript theme={null} import { assertEquals } from "jsr:@std/assert"; import { createMockApp, createMockContext, createMockRequest } from "@jelou/functions/testing"; import { define, z } from "@jelou/functions"; const myApp = createMockApp({ enviarEmail: define({ description: "Envía un correo electrónico", input: z.object({ to: z.string() }), handler: async (input) => ({ sent: true }), }), leerBandeja: define({ description: "Lee los mensajes de la bandeja de entrada", input: z.object({}), handler: async () => ({ messages: [] }), }), }); Deno.test("app tiene los tools correctos", () => { assertEquals(Object.keys(myApp.tools), ["enviarEmail", "leerBandeja"]); assertEquals(myApp.__jelou_edge_app, true); }); Deno.test("cada tool funciona independientemente", async () => { const ctx = createMockContext(); const req = createMockRequest({ to: "user@example.com" }); const result = await myApp.tools.enviarEmail.handler( { to: "user@example.com" }, ctx, req, ); assertEquals(result, { sent: true }); }); ``` Ejecuta los tests con Deno: ```bash theme={null} deno test ``` # Tokens de Autenticación Source: https://docs.jelou.ai/guides/functions/tokens Runtime tokens legacy: uso con X-Jelou-Token, gestión con CLI, y migración a API keys de plataforma. ## ¿Qué son los runtime tokens? Los runtime tokens (prefijo `jfn_rt_`) son credenciales **legacy** que autentican peticiones a funciones desplegadas antes de la migración a API keys de plataforma. Las funciones desplegadas hoy no reciben ningún runtime token — usan una [API key de plataforma](#alternativa-recomendada-api-keys) desde el primer deploy. La creación de nuevos runtime tokens (`jelou functions tokens create`) está **deprecada** y responde `410 Gone`. Los deploys tampoco generan uno automáticamente. Si tu función es nueva o perdiste tu runtime token, usa una [API key de plataforma](#alternativa-recomendada-api-keys). ## Cómo usarlos Si tu función ya tenía un runtime token activo antes de la migración, sigue funcionando: envíalo en el header `X-Jelou-Token`: ```bash theme={null} curl -X POST https://mi-funcion.fn.jelou.ai \ -H "Content-Type: application/json" \ -H "X-Jelou-Token: jfn_rt_abc123..." \ -d '{"telefono": "593987654321"}' ``` ```javascript theme={null} const res = await fetch("https://mi-funcion.fn.jelou.ai", { method: "POST", headers: { "Content-Type": "application/json", "X-Jelou-Token": process.env.JELOU_FUNCTION_TOKEN, }, body: JSON.stringify({ telefono: "593987654321" }), }); ``` ```python theme={null} import requests, os res = requests.post( "https://mi-funcion.fn.jelou.ai", headers={ "Content-Type": "application/json", "X-Jelou-Token": os.environ["JELOU_FUNCTION_TOKEN"], }, json={"telefono": "593987654321"}, ) ``` | Regla | Detalle | | ------------- | ------------------------------------------------------------------------------------- | | Header | `X-Jelou-Token` (los runtime tokens legacy no se validan vía `Authorization: Bearer`) | | Formato | No query param, no body — solo header | | Tamaño máximo | 4 KB | | Sin token | `401 { "error": "Unauthorized" }` | ## Gestión con CLI ### Listar tokens ```bash theme={null} jelou functions tokens list mi-funcion # ▸ Name Prefix Last Used Created # ▸ default jfn_rt_abc1.. 4/7/2026, 10:30:00 AM 4/1/2026, 2:00:00 PM # ▸ ci-deploy jfn_rt_def4.. never 4/5/2026, 9:00:00 AM ``` ### Crear token adicional (deprecado) `jelou functions tokens create` responde `410 Gone`: *"Runtime tokens are deprecated. Create an API key in the apps configuration section ([https://apps.jelou.ai](https://apps.jelou.ai)) and send it as `Authorization: Bearer `..."*. Ya no es posible crear runtime tokens nuevos, ni siquiera adicionales para un token existente. ```bash theme={null} jelou functions tokens create mi-funcion --name ci-deploy # ✗ 410 Runtime tokens are deprecated. Create an API key in the apps # configuration section (https://apps.jelou.ai) and send it as # `Authorization: Bearer ` when invoking the function. ``` Para credenciales nuevas, usa una API key de plataforma — ver [alternativa recomendada](#alternativa-recomendada-api-keys) más abajo. ### Revocar token Revocar tokens existentes (creados antes de la deprecación) sigue funcionando: ```bash theme={null} jelou functions tokens revoke mi-funcion # ? Revoke token jfn_rt_def4... for mi-funcion? (y/N) y # ✓ Token revoked ``` Revocar un token también genera un **redeploy automático**. Los clientes que usen ese token empezarán a recibir `401` una vez que el redeploy termine. ## Múltiples tokens Si tu función ya tiene varios tokens activos de antes de la deprecación (uno por entorno, servicio o equipo), puedes seguir listándolos y revocándolos individualmente. Revocar uno no afecta a los demás. ## ¿Perdiste el token? Ya no puedes generar un reemplazo con el CLI. En su lugar: 1. Crea una API key en [apps.jelou.ai](https://apps.jelou.ai) 2. Envíala como `Authorization: Bearer ` — ver [alternativa recomendada](#alternativa-recomendada-api-keys) 3. Si el runtime token anterior fue comprometido, revócalo: `jelou functions tokens revoke mi-funcion ` ## Alternativa recomendada: API keys El mecanismo soportado para autenticar funciones nuevas (todo deploy nuevo usa esto desde el inicio) o para reemplazar un runtime token perdido es una API key de plataforma, no un runtime token: ```bash theme={null} curl -X POST https://mi-funcion.fn.jelou.ai \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_..." \ -d '{"telefono": "593987654321"}' ``` Consulta la [guía de autenticación](/guides/functions/autenticacion#api-keys-recomendado) para el flujo completo. ## CI/CD En pipelines, usa una API key de plataforma en vez de crear runtime tokens: ```bash theme={null} # La API key se crea una sola vez en apps.jelou.ai y se guarda como secret de CI curl -X POST https://mi-funcion.fn.jelou.ai \ -H "Authorization: Bearer ${{ secrets.FUNCTION_API_KEY }}" \ -H "Content-Type: application/json" \ -d '{"telefono": "593987654321"}' ``` ## Brain Studio Configura tu función como servidor MCP externo usando tu API key: | Campo | Valor | | ------------ | ------------------------------------ | | URL | `https://mi-funcion.fn.jelou.ai/mcp` | | Header name | `Authorization` | | Header value | `Bearer sk_...` | Si tu función es legacy y todavía usa un runtime token, usa `X-Jelou-Token` / `jfn_rt_abc123...` en su lugar. Consulta la [guía de Brain Studio](/guides/functions/brain) para el flujo completo. # Validación Source: https://docs.jelou.ai/guides/functions/validacion Esquemas Zod para validar entradas y salidas de tus funciones: tipos, coerción en GET, formato de errores y comportamiento de output. ## Validación de entrada Cuando defines un esquema `input`, cada petición se valida antes de ejecutar el handler. Si la validación falla, el handler **no se ejecuta** y se retorna un `400`: ```json theme={null} { "error": "Validation failed", "details": [ { "path": ["email"], "message": "Invalid email", "code": "invalid_string" } ] } ``` ### Ejemplo con esquema completo ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "crear-ticket", description: "Crea un ticket de soporte desde WhatsApp", input: z.object({ nombre: z.string().min(1).describe("Nombre del cliente"), email: z.string().email().describe("Email de contacto"), asunto: z.string().max(200).describe("Asunto del ticket"), prioridad: z.enum(["baja", "media", "alta"]).default("media"), }), output: z.object({ ticketId: z.string(), estado: z.string(), }), handler: async (input, ctx) => { ctx.log("Creando ticket", { cliente: input.nombre, prioridad: input.prioridad }); return { ticketId: "TKT-2024-0042", estado: "abierto" }; }, }); ``` ### Tipos soportados Puedes usar cualquier tipo de Zod dentro de `z.object()`: ```typescript theme={null} input: z.object({ nombre: z.string().min(1), edad: z.number().int().positive(), email: z.string().email(), activo: z.boolean().default(true), rol: z.enum(["admin", "usuario", "invitado"]), tags: z.array(z.string()).optional(), metadata: z.record(z.string(), z.unknown()).optional(), }) ``` ### Coerción en peticiones GET Para peticiones GET, los query parameters son strings. Usa `z.coerce` para convertir tipos automáticamente: ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "buscar-pedidos", description: "Busca pedidos por estado", input: z.object({ q: z.string(), limit: z.coerce.number().default(10), pagina: z.coerce.number().default(1), activo: z.coerce.boolean().default(true), }), handler: async (input, ctx) => { ctx.log("Buscando", { q: input.q, limit: input.limit }); return { resultados: [], total: 0 }; }, }); ``` ```bash theme={null} curl "https://buscar-pedidos.fn.jelou.ai/?q=pendiente&limit=5&pagina=2" ``` ### Anotaciones `.describe()` Usa `.describe()` en cada campo para documentar los parámetros. Estas descripciones aparecen automáticamente en el esquema MCP, lo que ayuda a los agentes IA a entender cómo usar tu función: ```typescript theme={null} input: z.object({ telefono: z.string().min(10).describe("Número de teléfono con código de país, ej: 593987654321"), incluirHistorial: z.boolean().default(false).describe("Si incluir el historial de conversaciones"), }) ``` ## Validación de salida Cuando defines un esquema `output`, el valor retornado por el handler se valida **después** de la ejecución. Si no coincide: * Se registra una advertencia en los logs * La respuesta se envía normalmente con status `200` La validación de output nunca bloquea la respuesta. Es una herramienta de desarrollo para detectar inconsistencias. ```typescript theme={null} output: z.object({ nombre: z.string(), saldo: z.number(), }) ``` Si el handler retorna `{ nombre: "María", saldo: "150" }` (saldo como string), verás una advertencia en los logs pero el cliente recibe la respuesta sin cambios. ## Formato de errores de validación Cada error en el array `details` contiene: | Campo | Tipo | Descripción | | --------- | ---------- | -------------------------------------------------------------------------------- | | `path` | `string[]` | Ruta al campo con error, ej: `["email"]` o `["direccion", "ciudad"]` | | `message` | `string` | Mensaje legible del error | | `code` | `string` | Código de error de Zod (ej: `invalid_string`, `too_small`, `invalid_enum_value`) | ```json theme={null} { "error": "Validation failed", "details": [ { "path": ["nombre"], "message": "String must contain at least 1 character(s)", "code": "too_small" }, { "path": ["prioridad"], "message": "Invalid enum value. Expected 'baja' | 'media' | 'alta', received 'urgente'", "code": "invalid_enum_value" } ] } ``` # Webhooks Source: https://docs.jelou.ai/guides/functions/webhooks Verifica la firma de webhooks de Stripe, Shopify y Meta con una línea: ctx.verifyStripe, verifyShopify, verifyMeta y verifyHmac. Cuando tu función recibe webhooks de un servicio externo, cualquiera que conozca la URL puede enviarle datos falsos. La verificación de firma confirma que el evento viene realmente del servicio. `ctx.verify*` hace esa verificación en una línea: lee la cabecera de firma, resuelve el secret desde tus [secrets](/guides/functions/secrets) y lanza un error si no coincide. ## Stripe ```typescript index.ts theme={null} import { define, z } from "@jelou/functions"; export default define({ name: "stripe-webhook", description: "Recibe eventos de Stripe", input: z.object({}).passthrough(), config: { public: true, path: "/webhooks/stripe", methods: ["POST"], mcp: false, }, async handler(event, ctx, request) { await ctx.verifyStripe(request); ctx.log("Evento verificado", { tipo: event.type }); if (event.type === "payment_intent.succeeded") { await ctx.jelou.send({ type: "text", to: event.data.object.metadata.telefono, text: "¡Recibimos tu pago!", }); } return { received: true }; }, }); ``` Configura el secret una sola vez: ```bash theme={null} jelou functions secrets set stripe-webhook STRIPE_WEBHOOK_SECRET=whsec_... ``` El `event` que recibe tu handler ya es el cuerpo validado con Zod. La plataforma consumió el cuerpo del request para validarlo, así que llamar a `request.json()` dentro del handler lanza un error — usa el primer parámetro. ## Proveedores soportados | Verificador | Cabecera | Secret | | ------------------------------- | ----------------------- | ----------------------------------------- | | `ctx.verifyStripe(request)` | `stripe-signature` | `STRIPE_WEBHOOK_SECRET` | | `ctx.verifyShopify(request)` | `x-shopify-hmac-sha256` | `SHOPIFY_WEBHOOK_SECRET` | | `ctx.verifyMeta(request)` | `x-hub-signature-256` | `META_WEBHOOK_SECRET` o `META_APP_SECRET` | | `ctx.verifyHmac(request, opts)` | La que indiques | `opts.secretEnv` o `opts.secret` | Stripe además rechaza eventos con más de 5 minutos de antigüedad, lo que impide que alguien reenvíe un evento antiguo capturado. ## Otros servicios Para Twilio, GitHub, Slack o cualquier servicio con firma HMAC-SHA256, usa `ctx.verifyHmac`. La cabecera es obligatoria porque no hay un valor por defecto: ```typescript theme={null} await ctx.verifyHmac(request, { secretEnv: "WEBHOOK_SECRET", header: "x-signature", }); ``` ## Rotar secrets Durante una rotación, acepta el secret viejo y el nuevo a la vez separándolos por coma: ```bash theme={null} jelou functions secrets set mi-webhook STRIPE_WEBHOOK_SECRET=whsec_nuevo,whsec_viejo ``` Cuando confirmes que el proveedor ya usa el nuevo, vuelve a dejar uno solo. ## Manejar el fallo Los verificadores lanzan `WebhookVerificationError` con un código que indica qué pasó: ```typescript theme={null} import { define, WebhookVerificationError, z } from "@jelou/functions"; async handler(event, ctx, request) { try { await ctx.verifyStripe(request); } catch (err) { if (err instanceof WebhookVerificationError) { ctx.log("Firma rechazada", { code: err.code, provider: err.provider }); return new Response(null, { status: 401 }); } throw err; } return { received: true }; } ``` | Código | Significado | | ------------------- | ------------------------------------------ | | `missing_signature` | El request llegó sin cabecera de firma | | `invalid_signature` | La firma no coincide con el cuerpo | | `expired_timestamp` | El evento es demasiado antiguo (Stripe) | | `missing_secret` | No configuraste el secret de ese proveedor | Si no capturas el error, la función responde con error y el proveedor reintentará el webhook. Para Stripe y Shopify eso suele ser lo correcto solo cuando el fallo es temporal; ante una firma inválida es mejor responder `401` y no reintentar. ## Funciones públicas Los webhooks necesitan `config.public: true` para que el servicio externo pueda llamarlos sin credenciales de Jelou. Esa es justamente la razón por la que la verificación de firma es obligatoria: es el único control de acceso que queda. ```typescript theme={null} config: { public: true, // el proveedor no tiene API key de Jelou methods: ["POST"], // los webhooks siempre son POST mcp: false, // no tiene sentido como herramienta de IA } ``` Ver [funciones públicas](/guides/functions/public). ## Testing En los tests, `createMockContext()` deja cada verificador lanzando `missing_secret`. Para probar el resto del handler, desactiva la verificación: ```typescript theme={null} import { createMockContext, createMockWebhookVerifiers } from "@jelou/functions/testing"; const verifiers = createMockWebhookVerifiers({ stripe: true }); const ctx = createMockContext({ verifyStripe: verifiers.stripe, }); // Después de ejecutar el handler puedes revisar las llamadas registradas verifiers.calls; // [{ method: "stripe", ... }] ``` Ver la [guía de testing](/guides/functions/testing). Recibir peticiones sin credenciales de Jelou. Guardar los secrets de cada proveedor. Agendar un seguimiento al recibir el webhook. Ejemplo completo listo para copiar. # AI Routing Source: https://docs.jelou.ai/guides/getting-started/ai-routing Configura el enrutamiento inteligente de conversaciones para que cada mensaje llegue al workflow correcto automáticamente.