# Scheduled workflow disabled after 60 days of inactivity

> A scheduled workflow disabled after 60 days stops firing with no red run to inspect. Confirm the state from the API, re-enable it, keep it awake.

Source: https://latchkey.dev/learn/github-actions/github-actions-schedule-disabled-60-days-inactivity  
Updated: 2026-09-20

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.

## 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.
```

## 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.

## 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.

## 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.

| `state` | What put it there | How it clears |
| --- | --- | --- |
| `active` | Normal | Nothing to do; if the cron is still not firing, the cause is elsewhere |
| `disabled_inactivity` | A public repository with no activity for 60 days | Enable the workflow |
| `disabled_fork` | The repository is a fork of a public repository | Enable it, if you actually want the fork running it |
| `disabled_manually` | Somebody turned it off | Enable it, and find out why it was turned off |
| `deleted` | The workflow file is gone | Restore the file |

> A workflow reading `active` that still never fires is a different problem. GitHub documents two constraints on this trigger: "Scheduled workflows will only run on the default branch", and a scheduled run is subject to a delay rather than being guaranteed on the minute.

## 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.

> GitHub does not define what counts as "repository activity" in this sentence, so treat the 60 days as the documented rule and anything more specific about which events reset the clock as unverified.

## 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
```

> The last command needs a `workflow_dispatch` trigger on the file. Adding one alongside the schedule is worth doing anyway, because it is what lets you backfill after an outage without editing the cron.

## 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.

## FAQ

### 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.

## References

- [GitHub Actions: events that trigger workflows, the schedule event and its notes](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule)
- [GitHub Actions: disabling and enabling a workflow, in the UI, the API and the CLI](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/disable-and-enable-workflows)
- [GitHub REST API description: the workflow schema and its five state values](https://github.com/github/rest-api-description/blob/main/descriptions/api.github.com/api.github.com.json)
- [camptocamp/docker-postgres#28: the banner text from a repository disabled for four months](https://github.com/camptocamp/docker-postgres/issues/28)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
