Skip to content
Latchkey LogoLatchkey home

Scheduled workflow disabled after 60 days of inactivity

A scheduled workflow disabled after 60 days leaves no red run behind, because the cron simply stopped being scheduled. GitHub applies this to public repositories that have had no activity, and the workflow stays off until somebody turns it back on.

The five states a workflow can hold, and which three of them stop a cron from firing
Five values, three of them off. Quoted from GitHub's REST description, the workflow schema enum: active, deleted, disabled_fork, disabled_inactivity, disabled_manually.

What this error means

Whatever the cron produced stops updating and nothing in the Actions tab is red. The run history ends on an ordinary green run and then goes quiet, which is why this is usually noticed downstream: a nightly report that is weeks stale, a mirror that has stopped syncing, a dependency scan whose last result predates a release. In the Actions tab the workflow itself carries a banner saying it is disabled. The REST API is more precise and worth preferring, because it distinguishes this from the three other ways a workflow can be off, and it does so in a single field.

Actions tab banner, quoted from camptocamp/docker-postgres#28
This scheduled workflow is disabled because there hasn't been activity in this repository for at least 60 days. Enable this workflow to resume scheduled runs.

One field tells you which kind of off it is

GitHub publishes the workflow schema in its REST API description, and the state property carries an enum of exactly five values. Three of them mean the workflow will not run, and they have different causes and different fixes, so this is the first thing to read rather than the banner.

stateWhat put it thereHow it clears
activeNormalNothing to do; if the cron is still not firing, the cause is elsewhere
disabled_inactivityA public repository with no activity for 60 daysEnable the workflow
disabled_forkThe repository is a fork of a public repositoryEnable it, if you actually want the fork running it
disabled_manuallySomebody turned it offEnable it, and find out why it was turned off
deletedThe workflow file is goneRestore the file

Common causes

A public repository went 60 days without activity

The documented policy, and the one this page is named for. It is most likely on a repository whose only job is the cron itself, since a repository that exists to publish a nightly artifact may receive no commits for months at a time. The state reads disabled_inactivity.

The repository is a fork, so the schedule never started

Different state, same silence. Scheduled workflows are disabled by default when a public repository is forked, so a fork has never run the upstream's cron rather than having stopped. The state reads disabled_fork, and enabling it is a decision about whether you want the fork doing that work.

Somebody disabled it

The state reads disabled_manually, and it can be weeks old with nobody remembering. In our experience this happens during an incident, when a noisy workflow is switched off to stop it making things worse, and then nobody switches it back on.

It is active, and the schedule was never going to run

Worth ruling out before you go looking for a policy. A scheduled workflow only runs on the default branch, so a cron added on a feature branch and never merged has no effect. A workflow whose file is on the default branch but whose state is active and whose history is empty is this case, not the policy.

How to fix it

Read the state before you touch the file

  1. List the repository workflows through the API and read the state field for the one that stopped.
  2. Match it against the table above; the three disabled values have three different stories.
  3. If it reads active, stop looking at the policy and check that the file is on the default branch.

Enable it, then run it once by hand

Re-enabling resumes the schedule from the next tick, which can be a day away and leaves the gap unfilled. A dispatch run immediately afterwards backfills whatever the cron would have produced, so the output is fresh before anyone downstream notices.

Give the workflow a dispatch trigger alongside the schedule

This costs one line and is what makes the previous fix possible. It also gives you a way to test a cron change without waiting for it to fire.

.github/workflows/nightly.yml (illustrative)
on:
  schedule:
    - cron: '15 4 * * *'
  workflow_dispatch:

Alert on the output, not on the run

The failure mode here is the absence of runs, which run-based alerting cannot see. Check the age of whatever the cron produces, from something that is not the cron, and the next sixty-day disable becomes an alert rather than an archaeology exercise.

The rule, as GitHub states it

The policy is three sentences in GitHub's documentation, and each sentence is doing work: "To prevent unnecessary workflow runs, scheduled workflows may be disabled automatically. When a public repository is forked, scheduled workflows are disabled by default. In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days."

The qualifier in the last sentence is the one people miss. It says "In a public repository", so a private repository is not subject to this at all, and a workflow that stopped firing there is disabled_manually, on a non-default branch, or not the problem you think it is. The middle sentence is the separate disabled_fork case, which is why a freshly forked repository never runs the upstream's nightly jobs.

Confirm it before you change anything

One call settles it, and it is worth running rather than working from the banner, because the state field names which of the three it is in a single value. List the workflows with their states, then act on the one that is not active.

Terminal
# which workflows are off, and why
gh api repos/OWNER/REPO/actions/workflows \
  --jq '.workflows[] | select(.state != "active") | "\(.state)  \(.path)"'

# turn it back on
gh workflow enable .github/workflows/nightly.yml

# and run it once now rather than waiting for the next tick
gh workflow run .github/workflows/nightly.yml

Keeping it awake

Re-enabling fixes today and not the next sixty days. The mechanisms people reach for are all variations on giving the repository activity, and they trade differently: a periodic commit is reliable and noisy, a bot that re-enables the workflow on a schedule is quiet but needs its own credentials and its own schedule, and moving the job off the public repository sidesteps the policy entirely because it does not apply to private ones.

Whichever you pick, add a check that the thing the cron produces is fresh, because that is the signal that actually failed here. A workflow that is disabled produces no run, so any alert built on a failed run will never fire. An alert on the age of the output would have caught this on day sixty-one.

Why there is no recorded run on this page

Reproducing this means owning a public repository, leaving it untouched for sixty days and photographing the result, which is not a run and not something a runner does. There is also nothing for a runner to repair: the workflow was never dispatched, so no job existed to retry. What is checkable instead is the state value, which is why this page quotes the enum from GitHub's own API description and the banner text from a public repository that hit it.

How to prevent it

  • Watch the freshness of the artifact the schedule produces, since a disabled workflow emits no failed run to alert on.
  • Keep workflow_dispatch on every scheduled workflow so a gap can be backfilled without editing the cron.
  • Check the state field of critical workflows periodically, rather than trusting the Actions tab banner.
  • Move a cron that matters onto a repository that gets commits, or accept that it needs re-enabling.

Frequently asked questions

Does the 60 day disable apply to private repositories?
No. GitHub scopes the rule to public repositories: "In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days." A private repository whose cron stopped is off for some other reason, and the workflow state field will say which.
How do I tell an auto-disabled workflow from one someone turned off?
By the state field on the workflow in the REST API. GitHub documents five values, and disabled_inactivity is the 60 day policy, disabled_manually is a person, and disabled_fork is the default for a fork of a public repository. One field separates all three, which is why it is worth a call rather than a look at the Actions tab.
Why did my forked repository never run the scheduled workflow?
Because it never started. GitHub disables scheduled workflows by default when a public repository is forked, so the fork carries the file with a state of disabled_fork. Enabling it is a deliberate choice, and worth thinking about, since the upstream's nightly job may not be something your fork should be doing.
Will a scheduled run count as activity and keep the workflow enabled?
GitHub does not say. The published rule is that scheduled workflows are automatically disabled in a public repository "when no repository activity has occurred in 60 days", and the documentation does not define which events count. Rather than guess, alert on the freshness of the output so you find out within a day either way.

Related guides

References

A disabled cron is a public-repository rule. Private nightlies run on Latchkey at $0.0025/min at 2 vCPU. Start free → 30-day trial · No credit card