Configuration
Define cron schedules directly in your function’s config. No external configuration needed.
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 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:
curl -X POST http://localhost:3000 \
-H "Content-Type: application/json" \
-d '{}'
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 }) in your tests.
Syntax
Each schedule has two fields:
| Field | Type | Required | Description |
|---|
expression | string | Yes | Standard 5-field cron |
timezone | string | No | IANA timezone. Default: UTC. |
┌───────────── 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:
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:
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 };
}
Without the isCron guard, any HTTP request to your function will execute the cron logic. Always include this check.
How it works
- You define the schedules in
config.cron
- When you run
jelou deploy, the platform reads your configuration and creates the schedules
- When a schedule fires, your function receives a request with
ctx.isCron === true and ctx.trigger.cron with the expression that triggered it
- 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:
jelou 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
When you use app(), each tool can have its own independent cron schedules. Cron requests are sent to the specific route of each tool.
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 };
},
}),
},
});
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.
Common issues
Cron schedules are synchronized on deploy. If you changed the cron expression, you need to redeploy:Verify that the schedule is active:jelou 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. Without the isCron guard, any HTTP request will execute the cron logic. Add the check at the start of the handler: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.