> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jelou.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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:

<CodeGroup>
  ```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"
  }
  ```
</CodeGroup>

<Note>
  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.
</Note>

## 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 bot 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 };
}
```

<Warning>
  Sin el guard `isCron`, cualquier petición HTTP a tu función ejecutará la lógica del cron. Siempre incluye esta verificación.
</Warning>

## 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 };
      },
    }),
  },
});
```

<Warning>
  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.
</Warning>

## 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

<Tabs>
  <Tab title="Cron no se dispara">
    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.
  </Tab>

  <Tab title="Lógica cron se ejecuta por HTTP">
    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.
  </Tab>
</Tabs>

<Tip>
  Consulta la [guía completa de multi-tool](/guides/functions/multi-tool) para más detalles sobre `app()`.
</Tip>
