SwitchTenderSwitchTender Docs

Schedule a job

A schedule uses a cron expression or an RFC 5545 recurrence rule, and it can fire a stored template, a single run, a split, or a whole pipeline.

Schedule a template

  1. Save the work first as a template, so the schedule stays a one-line reference instead of a copy of every field.
  2. Open Schedules and add a schedule.
  3. Give it a cron expression for the cadence, for example 0 2 * * * for 02:00 every day, and point it at the template.
  4. Save. It fires on the schedule and each firing shows up in Runs like any other.

From the API

curl -s -X POST localhost:8080/v1/schedules \
  -H "Authorization: Bearer $ST_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"cron":"0 2 * * *","template_id":"tpl_abc123"}'

Use a real template id: the API stores the schedule either way, and one naming a template that does not exist fires nothing. The Doctor page, at /ui/doctor, and GET /v1/doctor report a schedule in that state by id. A name is optional and worth setting, since it is what that report and the schedules list call it.

To schedule without a template, send playbook and inventory inline, add shards for a split, or steps for a pipeline. A worker or the server must be running for a schedule to fire. On a Team license, add capacity with switchtender worker.

A schedule over a smart or constructed inventory can match no hosts at a fire. That fire is skipped, not failed, as when a fire matches no hosts explains.

By default the cron expression reads in the server's local time, and the server writes its zone's name onto the schedule when it is created or imported, such as America/Chicago, so every server of a highly available group reads it the same way. The published container image runs in UTC, so a schedule created there without a zone is pinned to UTC. Add a timezone, an IANA name such as America/New_York, to pin it to a zone of your choosing and let it follow that zone's daylight-saving shifts, so a nightly window stays put across the year.

A schedule an earlier release stored without a zone is read in UTC, and the first server of this release to open the database writes UTC onto it, so no server reads it in a zone of its own. That is the zone the published image already ran those schedules in. On a host install whose clock is not on UTC, those schedules now fire by UTC, so set each one's timezone to the zone it should fire in.

curl -s -X POST localhost:8080/v1/schedules \
  -H "Authorization: Bearer $ST_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"cron":"0 2 * * *","timezone":"America/New_York","template_id":"tpl_abc123"}'

Schedule a template with a survey

Nobody is there to answer a survey when a schedule fires, so every question takes its default: a plain question's default becomes an extra var, and a secret question's sealed default rides onto the run still sealed, opened only inside the execution, the same as when a person leaves the field blank. A required question takes its default too, held to the question's own rules.

A required question with no default has no answer, so the schedule refuses to fire rather than run without it. No run is created. The schedules list shows "did not run" with the reason, which names the question, the audit trail records the refusal as its own entry naming the question, and the Doctor page reports the schedule as broken before its first fire comes due. Give the question a default in the template editor and the next fire runs. A webhook trigger and a pull request plan fire templates the same way and refuse the same way.

When a fire matches no hosts

A template whose inventory is smart or constructed is resolved each time a fire comes due. When the inventory matches no host at that moment, the fire is skipped, not failed: no run starts, and the schedule fires again on its cadence. A skip is recorded where it is hard to miss:

A schedule's last_skip holds the reason the last fire was skipped and skipped_fires how many fires in a row were. A fire that starts a run, or fails, clears both.

Schedule what cron cannot say

A cron expression has no way to say the last Friday of each quarter, every other Tuesday, every third day, run six times and stop, or run nightly except on a holiday. A recurrence rule says all of them. It is the same RFC 5545 form AWX stores its schedules in, so a rule copied out of AWX works as written.

In the schedule dialog, set Cadence to Recurrence rule and write the rule. Send it as rrule instead of cron from the API. A schedule carries one of the two, never both.

curl -s -X POST localhost:8080/v1/schedules \
  -H "Authorization: Bearer $ST_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"rrule":"DTSTART;TZID=America/New_York:20260102T170000\nRRULE:FREQ=MONTHLY;BYMONTH=3,6,9,12;BYDAY=-1FR","template_id":"tpl_abc123"}'

A rule is a DTSTART line and one or more RRULE lines, separated by line breaks or spaces:

Some rules worth having:

Cadence Rule
Last Friday of each quarter at 17:00 RRULE:FREQ=MONTHLY;BYMONTH=3,6,9,12;BYDAY=-1FR
Last weekday of each quarter RRULE:FREQ=MONTHLY;BYMONTH=3,6,9,12;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1
Every other Tuesday RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TU
Every third day RRULE:FREQ=DAILY;INTERVAL=3
Weekdays, except the first Monday of the month RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR and EXRULE:FREQ=MONTHLY;BYDAY=1MO
Nightly, but not on 25 December RRULE:FREQ=DAILY and EXDATE;TZID=America/New_York:20261225T020000
Six runs, then stop RRULE:FREQ=DAILY;COUNT=6

The time of day comes from the DTSTART unless BYHOUR and BYMINUTE say otherwise. The DTSTART itself fires only when it matches the rule, which is how AWX evaluates its schedules, so a rule for the last Friday of the quarter with a DTSTART on a Monday starts at the first last Friday after it.

The dialog previews the next five fires as the rule is typed, on the rule's own clock, and the preview endpoint answers the same question from the API:

curl -s -G localhost:8080/v1/schedules/preview \
  -H "Authorization: Bearer $ST_TOKEN" \
  --data-urlencode 'rrule=DTSTART;TZID=America/New_York:20260102T170000 RRULE:FREQ=MONTHLY;BYMONTH=3,6,9,12;BYDAY=-1FR'

Daylight saving

A rule is expanded on the wall clock of its zone, which keeps a 09:00 rule at 09:00 local through every change of the clocks. The two nights a year the clocks move are handled the way RFC 5545 defines them:

When a rule ends

A rule with a COUNT or an UNTIL fires its last occurrence and then stops: the schedule stays, with no next run, and the Doctor page lists it as finished. Saving a rule that has already fired its last time is refused, since the schedule would never run.

Running more than one server

Every server in a highly available group runs the scheduler, and a due recurrence is claimed the same way a due cron schedule is: by a compare-and-set on its next run time in the shared database, so exactly one server fires it. The last occurrence of a bounded rule is claimed by clearing that time under the same compare-and-set, so it fires once too.

A server that stops while it is firing an occurrence, before the run exists, hands the occurrence back: its next run time is set back to that occurrence, and the schedule's last error says the fire was interrupted. The next scheduler to check fires it, this server after a restart or another server of the group. Each scheduled run carries a key naming its schedule and occurrence, so a fire that takes up a handed back occurrence finds the run the interrupted fire made, if it made one, rather than starting a second.

A server that stops too abruptly to hand anything back, because it crashed or lost its host, does not lose the occurrence either. The claim marks the occurrence in flight on the schedule in the same database write, and the mark stays until the fire is recorded on the schedule. Every server's scheduler looks for marks older than two minutes. When it finds one, it checks for the run under the occurrence's key. If the run exists, the fire is recorded with that run. If it does not, the occurrence is fired again under the same key, so a run that lands in the meantime is found rather than started twice. A schedule disabled since the claim does not fire, and its last error says why. A deleted schedule takes its mark with it. While an occurrence is marked, the schedule's next occurrence is not claimed, so it fires late rather than leaving the earlier one unaccounted for.

A tick that runs late on the night the clocks go back, after a restart, a database failover, or with a --schedule-interval longer than a minute, still fires a time that happens twice only once.

Imported schedules

An AWX import carries each schedule's rule across. A rule a cron expression says exactly, checked against the rule's own next fires, becomes cron, which is easier to read and edit. Every other rule comes across as the recurrence it is, where it used to be skipped. A Rundeck job on a Quartz form cron cannot read, such as the third Friday (6#3) or the last day of the month (L), comes across as a recurrence too.

An AWX schedule that answers its template's survey fires a copy of the template whose questions default to those answers, so it fires with the same answers it did in AWX. A secret answer cannot come across, because an export never carries one readably, so a schedule that gave one arrives switched off and the import report names the question. Give that question a default on the copy, then switch the schedule on.

Next: give the job its secrets with set a secret.