Docs / Guides
Cron schedules and time zones
How to create, inspect, and change cron schedules in Tend, including time zone handling, daylight saving behavior, and the 30-second minimum interval.
Last updated March 12, 2026
#How schedules work
A schedule is a named, recurring rule that creates a job run each time its cron expression matches. Schedules belong to a project and an environment, so a schedule created with a tnd_dev_ key is invisible to tnd_live_ keys and vice versa. Schedule names are unique per environment; creating a second schedule with the same name returns a 409 schedule_name_taken error.
Each time a schedule fires, Tend creates a run with its own ID (for example run_01J9K3M7Q2XV), delivers the schedule's payload to your target, and applies the retry policy attached to the schedule. The schedule itself is never retried; only the individual runs it produces are.
#Creating a schedule
Send a POST to /v2/schedules with a name, a cron expression, an IANA time zone, and the target that should receive each run. If timezone is omitted, it defaults to UTC. The example below creates a nightly report job that fires at 02:30 every day in Denver.
curl https://api.tendcomputer.com/v2/schedules \
-H "Authorization: Bearer tnd_live_8f2c1a9d4e7b" \
-H "Content-Type: application/json" \
-d '{
"name": "nightly-usage-report",
"cron": "30 2 * * *",
"timezone": "America/Denver",
"target": {"url": "https://api.example-shop.dev/internal/reports"},
"payload": {"report": "usage", "window": "24h"}
}'from tend import Tend
client = Tend(api_key="tnd_live_8f2c1a9d4e7b")
schedule = client.schedules.create(
name="nightly-usage-report",
cron="30 2 * * *",
timezone="America/Denver",
target={"url": "https://api.example-shop.dev/internal/reports"},
payload={"report": "usage", "window": "24h"},
)
print(schedule.id, schedule.next_run_at)import { Tend } from "@tend/sdk";
const client = new Tend({ apiKey: "tnd_live_8f2c1a9d4e7b" });
const schedule = await client.schedules.create({
name: "nightly-usage-report",
cron: "30 2 * * *",
timezone: "America/Denver",
target: { url: "https://api.example-shop.dev/internal/reports" },
payload: { report: "usage", window: "24h" },
});
console.log(schedule.id, schedule.next_run_at);client := tend.NewClient("tnd_live_8f2c1a9d4e7b")
sch, err := client.Schedules.Create(ctx, &tend.ScheduleParams{
Name: "nightly-usage-report",
Cron: "30 2 * * *",
Timezone: "America/Denver",
Target: tend.Target{URL: "https://api.example-shop.dev/internal/reports"},
Payload: map[string]any{"report": "usage", "window": "24h"},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(sch.ID, sch.NextRunAt)A successful call returns 201 Created with the schedule object, including a next_run_at timestamp in UTC so you can confirm that Tend interpreted your expression the way you meant it.
{
"id": "sch_01J9K2WZ4T8R",
"name": "nightly-usage-report",
"cron": "30 2 * * *",
"timezone": "America/Denver",
"status": "active",
"region": "us-east",
"next_run_at": "2026-03-13T08:30:00Z",
"created_at": "2026-03-12T16:04:11Z"
}#Cron expression syntax
Tend accepts standard five-field cron expressions (minute, hour, day of month, month, day of week). For sub-minute intervals, a sixth leading field for seconds is accepted; see the section on the minimum interval below. Ranges (1-5), lists (1,15), steps (*/10), and three-letter month and weekday names (MON, JAN) are supported.
| Expression | Meaning |
|---|---|
*/5 * * * * | Every 5 minutes |
0 9 * * MON-FRI | 09:00 on weekdays |
15 3 1 * * | 03:15 on the first day of each month |
0 0 * * SUN | Midnight at the start of every Sunday |
*/30 * * * * * | Every 30 seconds (six-field form) |
A malformed expression, or a time zone that is not a valid IANA name, returns 400 invalid_cron_expression. Abbreviations such as EST or MST are not IANA zone names; use America/New_York or America/Denver instead.
#Time zones and daylight saving time
Cron expressions are evaluated in the schedule's time zone, and the result is converted to UTC for execution. This matters twice a year. When clocks move forward, wall-clock times that do not exist (such as 02:30 on the spring transition day in Denver) are skipped for that day, and the schedule resumes normally on the next matching day. When clocks move back, a wall-clock time occurs twice, and Tend fires on the first occurrence only, so you never receive a duplicate run.
Schedules that fire more often than hourly, such as */15 * * * *, are unaffected by daylight saving transitions in whole-hour zones, because the interval boundaries do not move. Zones with 30- or 45-minute offsets still align correctly, since evaluation always happens in the local zone.
#The 30-second minimum interval
No schedule may fire more often than once every 30 seconds. An expression that would violate this returns 422 interval_too_short. The six-field expression */30 * * * * * is the fastest permitted cadence; */10 * * * * * is rejected.
If your work needs to happen more frequently than that, batch it: schedule one run every 30 seconds and have the job process everything that accumulated in the interval. Alternatively, create delayed one-off jobs with run_at from the code that produces the work.
Runs that are still executing when the next tick arrives do not block the schedule. Whether the two overlap is governed by the schedule's overlap setting, which accepts allow (the default) or skip. With skip, a tick that arrives while a previous run is still in flight is recorded as skipped and no request is sent. Concurrency limits on your plan (5 on Hobby, 50 on Pro, 500 on Scale) apply on top of this setting.
#Pausing, updating, and listing schedules
Use PATCH /v2/schedules/{id} to change a schedule's cron expression, time zone, payload, or status. Changing the expression recomputes next_run_at immediately; runs already queued are not affected. Set status to paused to stop future runs without deleting history, and back to active to resume.
curl -X PATCH https://api.tendcomputer.com/v2/schedules/sch_01J9K2WZ4T8R \
-H "Authorization: Bearer tnd_live_8f2c1a9d4e7b" \
-H "Content-Type: application/json" \
-d '{"status": "paused"}'client.schedules.update("sch_01J9K2WZ4T8R", status="paused")await client.schedules.update("sch_01J9K2WZ4T8R", { status: "paused" });Listing uses cursor-based pagination. Pass limit (default 50, maximum 200) and the cursor value returned in the previous response's next_cursor field. When next_cursor is null, you have reached the end of the list.
curl "https://api.tendcomputer.com/v2/schedules?limit=100&cursor=c_8Jq2mXw" \
-H "Authorization: Bearer tnd_live_8f2c1a9d4e7b"