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

> Configure scheduled tasks in your functions: cron syntax, timezones, isCron guard, limits, and sync on deploy.

## Configuration

Define cron schedules directly in your function's `config`. No external configuration needed.

```typescript index.ts theme={null}
import { define, z } from "@jelou/functions";

export default define({
  name: "appointment-reminder",
  description: "Sends appointment reminders via 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("Sending reminders", { 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: "Hi, we remind you that you have an appointment tomorrow at 10:00 AM.",
      }),
    });

    return { sent: 1, status: res.status };
  },
});
```

## Local testing

Start the server with `jelou functions dev`. You can send an HTTP request to the function, but the `isCron` guard will reject it because it is not a real cron trigger:

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST http://localhost:3000 \
    -H "Content-Type: application/json" \
    -d '{}'
  ```

  ```json Response 200 theme={null}
  {
    "skipped": true,
    "reason": "not a cron trigger"
  }
  ```
</CodeGroup>

<Note>
  You cannot simulate a real cron trigger from curl — the platform injects `ctx.isCron` and the cryptographic signature automatically. To test cron logic, use [`createMockContext({ isCron: true })`](/guides/functions/testing) in your tests.
</Note>

## Syntax

Each schedule has two fields:

| Field        | Type     | Required | Description                    |
| ------------ | -------- | -------- | ------------------------------ |
| `expression` | `string` | Yes      | Standard 5-field cron          |
| `timezone`   | `string` | No       | IANA timezone. Default: `UTC`. |

### Expression format

```
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *
```

### Common examples

| Expression     | Description                         |
| -------------- | ----------------------------------- |
| `0 9 * * *`    | Every day at 9:00 AM                |
| `0 9 * * 1-5`  | Monday to Friday at 9:00 AM         |
| `*/30 * * * *` | Every 30 minutes                    |
| `0 0 1 * *`    | First day of each month at midnight |
| `0 */2 * * *`  | Every 2 hours                       |
| `30 14 * * 3`  | Wednesday at 2:30 PM                |

### Timezones

Use any valid IANA zone:

```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" }, // Mexico
    { expression: "0 9 * * *", timezone: "America/Argentina/Buenos_Aires" },
    { expression: "0 3 * * *" },                                   // UTC by default
  ],
}
```

## `isCron` guard

Your function can receive both HTTP requests and cron triggers. Use `ctx.isCron` to distinguish them:

```typescript theme={null}
handler: async (_input, ctx) => {
  if (!ctx.isCron) {
    return { skipped: true, reason: "not a cron trigger" };
  }

  ctx.log("Cron task running", { cron: ctx.trigger.cron });
  return { executed: true };
}
```

<Warning>
  Without the `isCron` guard, any HTTP request to your function will execute the cron logic. Always include this check.
</Warning>

## How it works

1. You define the schedules in `config.cron`
2. When you run `jelou functions deploy`, the platform reads your configuration and creates the schedules
3. When a schedule fires, your function receives a request with `ctx.isCron === true` and `ctx.trigger.cron` with the expression that triggered it
4. Cryptographic signature verification prevents unauthorized invocations

## Limits

* Maximum **10** cron schedules per function
* Exceeding this limit throws an error at definition time

## Management

Schedules are **declarative** — they are defined in code and synchronized on each deployment. To modify a schedule, change `config.cron` in your code and redeploy.

To view active schedules:

```bash theme={null}
jelou functions cron list query-customer
# ▸ Expression     Timezone             Enabled  Last Triggered
# ▸ 0 8 * * *     America/Guayaquil    yes      2 hours ago
# ▸ 0 8 * * *     America/Bogota       yes      2 hours ago
```

## Multi-tool cron

When you use `app()`, each tool can have its own independent cron schedules. Cron requests are sent to the specific route of each tool.

```typescript theme={null}
import { app, define, z } from "@jelou/functions";

export default app({
  tools: {
    dailyCleanup: define({
      description: "Cleans up stale records",
      input: z.object({}),
      config: { cron: [{ expression: "0 3 * * *", timezone: "UTC" }] },
      handler: async (_input, ctx) => {
        if (!ctx.isCron) return { skipped: true };
        return { cleaned: true };
      },
    }),
    hourlySync: define({
      description: "Syncs external data",
      input: z.object({}),
      config: { cron: [{ expression: "0 * * * *" }] },
      handler: async (_input, ctx) => {
        if (!ctx.isCron) return { skipped: true };
        return { synced: true };
      },
    }),
  },
});
```

<Warning>
  The **10** cron schedule limit is **aggregated** across all tools in an `app()`. If one tool uses 6 schedules, the other tools can only use 4 in total.
</Warning>

## Common issues

<Tabs>
  <Tab title="Cron does not fire">
    Cron schedules are synchronized on deploy. If you changed the cron expression, you need to redeploy:

    ```bash theme={null}
    jelou functions deploy
    ```

    Verify that the schedule is active:

    ```bash theme={null}
    jelou functions cron list my-function
    # ▸ Expression     Timezone             Enabled  Last Triggered
    # ▸ 0 8 * * *     America/Guayaquil    yes      2 hours ago
    ```

    If the `Enabled` column shows `no`, check that the cron expression is valid.
  </Tab>

  <Tab title="Cron logic executes via HTTP">
    Without the `isCron` guard, any HTTP request will execute the cron logic. Add the check at the start of the handler:

    ```typescript theme={null}
    handler: async (_input, ctx) => {
      if (!ctx.isCron) return { skipped: true };

      // your cron logic here
      return { executed: true };
    }
    ```

    This returns `{ skipped: true }` for normal HTTP requests and only executes the logic when it is a real cron trigger.
  </Tab>
</Tabs>

<Tip>
  See the [full multi-tool guide](/guides/functions/multi-tool) for more details about `app()`.
</Tip>
