Skip to content

Schedule triggers: cron with a time zone

Run a workflow on a cron schedule bound to an IANA zone. What happens on the DST switch days, after downtime, and how the firings log explains a quiet night.

Last updated:

Wall-clock time, meant literally

A schedule is a cron expression plus an IANA zone — Europe/Berlin, never a UTC offset, because an offset is wrong for half the year. Wall-clock semantics are taken literally on the days that test them. On the night the clocks go back, a daily job fires once, not twice: on a portal permitting one session per login, a second sign-in would evict the first. On the night they go forward, a job scheduled into the missing hour fires the moment the gap ends rather than not at all.

Downtime follows the same discipline. However long the platform was down, there is exactly one catch-up decision — catchUp either skips the missed occurrences or runs one catch-up firing. Never a burst of back-to-back runs.

Create a nightly trigger

Requires: api-key

BASE="https://browserberg.com"
AUTH="Authorization: Bearer $BROWSERBERG_API_KEY"
JSON="Content-Type: application/json"

WFID=$(curl -s -X POST "$BASE/v1/workflows" -H "$AUTH" -H "$JSON" -d '{
  "title": "Nightly example check",
  "blocks": [ { "blockType": "navigation", "label": "Open_page", "url": "https://example.com" } ]
}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["workflow"]["workflowId"])')

# A schedule is a cron expression plus an IANA zone -- never a UTC offset.
# Wall-clock time is meant literally, including on the DST switch days.
TRIGGER=$(curl -s -X POST "$BASE/v1/workflows/$WFID/triggers" -H "$AUTH" -H "$JSON" -d '{
  "kind": "schedule",
  "name": "Nightly at 02:30 Berlin time",
  "cron": "30 2 * * *",
  "timezone": "Europe/Berlin",
  "catchUp": "run_once",
  "session": { "ttlSeconds": 900 }
}')
echo "$TRIGGER" | python3 -c 'import json,sys; t = json.load(sys.stdin)["trigger"]; print(json.dumps({
  "id": t["id"], "status": t["status"], "nextFireAt": t["nextFireAt"],
  "catchUp": t["catchUp"], "overlap": t["overlap"]}, indent=2))'

TID=$(echo "$TRIGGER" | python3 -c 'import json,sys; print(json.load(sys.stdin)["trigger"]["id"])')
curl -s -X DELETE "$BASE/v1/triggers/$TID" -H "$AUTH" > /dev/null

Schedule options

The minimum interval between occurrences is 5 minutes.

Name Type Description
cron required string Standard five-field cron expression.
timezone required string An IANA zone name such as Europe/Berlin. Offsets are rejected.
catchUp skip | run_once skip drops occurrences missed during downtime; run_once fires a single catch-up.
misfireGraceSeconds integer How late an occurrence may still fire and count as on time (60 to 21600). Default: 300
overlap skip | allow skip refuses a new firing while the previous run is still going; allow starts it anyway.
session object The session each firing gets: poolId, profileId, locale, timezone, ttlSeconds.
version integer Pins the workflow version to run; without it, each firing runs the latest.

The firings log answers the morning question

GET /v1/triggers/:id/firings records every occasion the trigger came due — including the ones where nothing ran, with the outcome started, skipped or refused and the reason. When you arrive at your desk asking why nothing ran last night, the log is the answer, not a shrug.

A failed run does not pause a trigger; failure is what retries and the next occurrence are for. Only a fault that can never fix itself on its own — a deleted workflow, a revoked credential — pauses one.

Also worth reading

  • Webhook triggers Fire on an event instead of a clock
  • Triggers reference Every trigger field and endpoint
  • Building workflows What the trigger actually runs