Which stage is actually failing?

You expect a morning automation, but the output never arrives. Begin by checking whether the Actions page contains a run for that period. No run points toward trigger creation, workflow eligibility or scheduling. A queued run points toward capacity and runner availability. A failed run means the event already reached the workflow, so inspect the step logs. A successful run without the expected artifact calls for checking conditions and application logic. These are different failure locations with different evidence. Repeatedly editing the cron expression cannot fix a script that starts correctly and exits before producing the report. Establish where progress stopped before changing the schedule.

The GitHub-specific rules here were checked against the official documentation on 15 October 2026. One common outdated assumption deserves attention immediately: UTC remains the default, but schedules can now specify an IANA timezone. The Python conversions below were executed locally. The sample workflow was not submitted to a repository or triggered by GitHub during this work. Local output therefore establishes neither scheduler delivery nor repository eligibility. It is a reproducible way to inspect timezone arithmetic, followed by a separate procedure for checking the hosted workflow in your own repository. Keep those two kinds of evidence distinct when reporting a fix.

A schedule is a trigger, then a run — diagnostic sequence
Each box is a separate verification step.

UTC by default, with an explicit timezone option

Without a timezone, 17 6 * * 1-5 targets 06:17 UTC on weekdays. For the Istanbul dates in this article, that corresponds to 09:17 local time. To express the business requirement directly as 09:17 in Istanbul, keep hour 9 and add timezone: Europe/Istanbul to the schedule entry. Do not combine both adjustments: converting the hour to UTC and then attaching the Istanbul timezone shifts the intended instant a second time. Place timezone beside cron within the same list item. Setting TZ inside the job changes the environment seen by running processes; it does not redefine the schedule that caused the job to start.

Diagnostic workflow with timezone; not executed on GitHub.yaml
01name: Schedule diagnostic02on:03  workflow_dispatch:04  schedule:05    - cron: '17 9 * * 1-5'06      timezone: 'Europe/Istanbul'07permissions:08  contents: read09jobs:10  inspect:11    runs-on: ubuntu-latest12    timeout-minutes: 513    steps:14      - name: Record trigger and runner clock15        env:16          EVENT_NAME: ${{ github.event_name }}17          REF_NAME: ${{ github.ref }}18          SCHEDULE: ${{ github.event.schedule }}19        run: |20          printf 'event=%s\nref=%s\nschedule=%s\n' "$EVENT_NAME" "$REF_NAME" "$SCHEDULE"21          date -u '+UTC=%Y-%m-%dT%H:%M:%SZ'22          TZ=Europe/Istanbul date '+Istanbul=%Y-%m-%dT%H:%M:%S%z'23 

Minute 17 is a deliberate choice to avoid concentrating work at the top of an hour; it is not a promise of punctual delivery. workflow_dispatch provides a manual diagnostic trigger. Printing only the trigger and clock information makes it easier to inspect execution before adding network calls or database dependencies. When you introduce your real task, configure only the permissions and secrets it needs. A successful manual run shows that those steps can execute in that context. To establish that the schedule itself delivered an event, inspect a separate run whose event type is schedule. Both checks are useful, but they answer different questions.

Read the five fields correctly

GitHub Actions uses five fields: minute, hour, day of month, month and day of week. A system crontab username or shell command does not belong in that expression. A six-field expression copied from a scheduler with seconds is also a different format. Quote the cron string in YAML, especially when it starts with an asterisk. For an every-five-minute pattern, use */5 * * * *. The expression */5 9 * * * means five-minute steps within hour 9, not every five minutes throughout the day. Keep unnecessary day constraints as wildcards so the schedule is easy to explain and review.

Example expressions in the selected timezone
ExpressionIntent
17 9 * * 1-509:17 on weekdays
*/15 * * * *Minutes 00, 15, 30 and 45 each hour
17 9 1 * *09:17 on the first day of each month

These describe target schedule times; actual starts depend on delivery and queues.

Is the workflow on the eligible branch?

Check the repository settings to identify the default branch instead of assuming it is named main. The scheduled workflow file must exist in .github/workflows on that branch. GitHub creates scheduled runs from its latest commit. A new workflow committed only to a feature branch, or waiting in an open pull request, is not sufficient to activate that schedule. Check the filename, extension, YAML indentation and whether Actions recognizes the workflow. A later checkout step can fetch different source files, but it does not change the branch eligibility of the schedule trigger itself. Inspect the actual default-branch file when investigating a missing run.

If the manual Run workflow control is missing, check workflow_dispatch and the version of the file on the default branch. A small diagnostic job on that branch gives you a clearer first observation than a full production task with many dependencies. Also inspect whether the workflow is disabled and whether repository or organization Actions policies prevent execution. Once a run actually exists, stop treating the problem as an invisible trigger: examine its event type, commit and step statuses. Those fields let you connect the configuration you reviewed with the code GitHub actually used. They also prevent a manual feature-branch test from being mistaken for evidence about the scheduled default-branch job.

Inactivity, delivery delays and interval limits

The current schedule documentation says that scheduled workflows in public repositories are automatically disabled after 60 days without repository activity. Inspect the workflow state and follow the documented re-enabling procedure if that applies. The documented minimum scheduling interval is five minutes. A denser expression does not turn this service into a precision timer. Delivery can be delayed during high load, and the documentation also warns that some queued jobs may be dropped under sufficient load. This makes the trigger an unsuitable sole source of assurance for operations requiring an exact delivery instant. Decide what lateness or a missed run means for your particular automation.

For a reproducible daily report, store the last successfully processed business period and let the next run catch up on missing periods. Processing the same period twice should not create duplicate external records; use a period key or another appropriate deduplication rule. Editing a cron expression cannot remove all queue delays. Logging the start time, completion time and processed data window helps distinguish trigger lateness from slow application work. These are engineering choices you make around the service, rather than guarantees supplied by GitHub. Choose the amount of state and retry handling according to the cost of a skipped or repeated task.

Check timezone arithmetic locally

Comparing Istanbul and New York on two dates demonstrates why a fixed UTC offset and a named regional timezone are different concepts. The sample uses 09:17, outside transition hours; it is neither a general cron engine nor a daylight-saving transition simulator. Python zoneinfo reads the installed IANA timezone data. Istanbul has the same offset in these examples, while New York changes between summer and winter. If the business requirement is nine in the local morning, translating one date to UTC and reusing that fixed hour all year can violate the requirement in regions with seasonal clock changes. Test representative dates rather than only the next occurrence.

timecheck.py — executed locally.python
01from datetime import datetime, timezone02from zoneinfo import ZoneInfo03 04istanbul = ZoneInfo("Europe/Istanbul")05for day in ("2026-10-15", "2026-12-15"):06    local = datetime.fromisoformat(day + "T09:17:00").replace(tzinfo=istanbul)07    print(local.isoformat(), "=>", local.astimezone(timezone.utc).isoformat())08 09new_york = ZoneInfo("America/New_York")10for day in ("2026-07-15", "2026-12-15"):11    local = datetime.fromisoformat(day + "T09:17:00").replace(tzinfo=new_york)12    print(local.isoformat(), "=>", local.astimezone(timezone.utc).isoformat())
Recorded timezone conversionstext
012026-10-15T09:17:00+03:00 => 2026-10-15T06:17:00+00:00022026-12-15T09:17:00+03:00 => 2026-12-15T06:17:00+00:00032026-07-15T09:17:00-04:00 => 2026-07-15T13:17:00+00:00042026-12-15T09:17:00-05:00 => 2026-12-15T14:17:00+00:00

Handle missing or repeated wall-clock times at daylight-saving transitions according to the target platform documentation. Attaching tzinfo to a Python datetime does not reproduce GitHub scheduler policy. Record an ISO 8601 instant with UTC or an explicit offset instead of a bare 09:17 in logs. The Unix timestamp converter can help compare a log instant with an Istanbul representation. Check whether an epoch value is expressed in seconds or milliseconds before converting it. A unit mismatch can produce a wildly incorrect date even when the timezone setting is right. Preserve the original log value alongside its human-readable interpretation.

How to verify the hosted schedule in your repository

Place the sample workflow on your own repository’s default branch and run it manually first. Confirm workflow_dispatch in the event output and inspect the printed clocks. After the chosen schedule time, look for a separate run with event name schedule and the expected cron value. Review the interval between run creation and runner startup too. A correct local preview describes candidate instants; it cannot replace these hosted observations. If no run appears, return to branch eligibility, disabled state and policy checks. If a run fails, read the first failing step rather than continuing to adjust the trigger. Record the event, commit, start time and result together when documenting the fix.

Use the cron expression generator to review the five fields together with the selected timezone. Compare its preview with several dates from your business calendar, including month boundaries. Add a short comment to the workflow stating the requirement, such as weekdays at 09:17 in Istanbul. That statement gives a future maintainer more context than the numbers alone. Finally, decide how the application determines its business day: the trigger instant, the Istanbul calendar date, or the last completed reporting period. A correctly triggered workflow processing the wrong day can look like a broken schedule from the outside. Verify the application’s time window as well as the trigger.

Official sources

GitHub and Python documentation checked on 15 October 2026.

  1. GitHub: Events that trigger workflows — schedule
  2. GitHub: Manually running a workflow
  3. GitHub: Disabling and enabling a workflow
  4. GitHub: Workflow syntax
  5. Python: zoneinfo — IANA time zone support

Local Python conversions were executed. The workflow was not submitted to or scheduled on GitHub; check official documentation for current service behavior.

✳

Review the expression with its timezone, then verify an actual scheduled run.

Open the cron expression generator ↗