# Scheduled GitHub Actions Workflow Silence and Failure Report (`kingii98/scheduled-github-actions-workflow-silence-and-failure-report`) Actor

Watches scheduled GitHub Actions workflows and reports the ones that stopped to fire, that fail again and again, or that GitHub disabled for inactivity. One dataset record for each workflow plus one summary record. HTTP only, no browser, no proxy.

- **URL**: https://apify.com/kingii98/scheduled-github-actions-workflow-silence-and-failure-report.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 run starteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Scheduled GitHub Actions Workflow Silence and Failure Report

GitHub tells you when a workflow run fails. It does not tell you when a
scheduled workflow **stops to run at all**. GitHub also disables a scheduled
workflow in a repository without recent activity, and it sends no alert when
it does so. An uptime monitor cannot see this, because the job that must fire
is the thing that went silent.

This Actor reads the current state of your scheduled workflows from the public
GitHub REST API and reports three conditions:

| Condition | What it means |
| --- | --- |
| **Overdue** | The newest run is older than the interval that you declared for this schedule. |
| **Failing** | The newest runs failed, one after the other, up to your threshold. |
| **Disabled** | GitHub set the workflow to a disabled state, usually `disabled_inactivity`. |

Run it on an Apify schedule every hour or every 3 hours. It needs no browser,
no proxy and no paid API. A GitHub token is optional and stays yours.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `workflows` | array | three public repositories | 1 to 100 records. Each record holds `owner_repo`, `workflow_file` and `expected_max_interval_minutes`. |
| `githubToken` | string (secret) | none | Optional. Necessary for private repositories. It also lifts the anonymous rate limit of 60 requests for each hour. |
| `failureStreakThreshold` | integer | `2` | The Actor raises a failure alert after this many failed runs in a row. |
| `alertWebhookUrl` | string | empty | Optional. One public HTTPS URL. The Actor sends one JSON POST with every new alert of the run. |
| `runEventFilter` | select | `schedule` | `schedule` counts only the runs that the cron trigger started. `any` counts every run. |
| `runsPerWorkflow` | integer | `10` | How many recent runs are read. The failure streak cannot become longer than this. |
| `suppressRepeatAlerts` | boolean | `true` | Keeps one cursor for each workflow, so the same alert is not repeated on every tick. |
| `stateStoreName` | string | `gha-schedule-monitor-state` | Name of the **named** key-value store that holds the cursors. Give two schedules two different names when they must alert on their own. |
| `confirmSilence` | boolean | `true` | Read the run list a second time before a silence alert is raised. See *False silence*. |
| `silenceConfirmDelaySeconds` | integer | `3` | Pause before that second read. It happens once for the whole run. |
| `timeoutSeconds` | integer | `15` | Timeout for one call to the GitHub REST API. |
| `requestsPerSecond` | integer | `5` | Rate limit for the calls to `api.github.com`. |
| `maxResponseBytes` | integer | `2000000` | Hard cap on the bytes read from one API answer. |

#### Example input

```json
{
  "workflows": [
    {
      "owner_repo": "acme/data-platform",
      "workflow_file": "nightly-sync.yml",
      "expected_max_interval_minutes": 1500
    },
    {
      "owner_repo": "acme/infra",
      "workflow_file": "backup.yml",
      "expected_max_interval_minutes": 480
    }
  ],
  "failureStreakThreshold": 2,
  "alertWebhookUrl": "https://hooks.example.com/gha-silence"
}
```

`workflow_file` is the file name of the workflow, such as `nightly-sync.yml`,
or its numeric workflow id. A full path such as
`.github/workflows/nightly-sync.yml` is also accepted and is reduced to the
file name.

Set `expected_max_interval_minutes` a little above the real cadence of the
cron. A GitHub schedule can start some minutes late when the runner queue is
busy, so a daily job is well served by `1500` and not by `1440`.

### Output

One dataset record for each workflow:

| Field | Description |
| --- | --- |
| `ownerRepo`, `workflowFile`, `workflowName`, `workflowId`, `workflowUrl` | Identity of the workflow. |
| `workflowState` | `active`, `disabled_inactivity`, `disabled_manually` or `disabled_fork`. |
| `lastRunConclusion`, `lastRunStatus`, `lastRunAt`, `lastRunId`, `lastRunUrl` | The newest finished run. |
| `expectedMaxIntervalMinutes`, `minutesSinceLastRun`, `minutesOverdue`, `overdue` | The silence check. |
| `failureStreak`, `failureStreakThreshold`, `failing` | The failure check. |
| `disabled` | `true` when GitHub disabled the workflow. |
| `alert`, `alertReasons`, `alertDelivered`, `alertSuppressed` | The alert verdict. `alertReasons` holds `overdue`, `failing`, `disabled` or `never-ran`. |
| `status`, `note`, `httpStatus`, `checkedAt` | `OK`, `ALERT`, or the reason why the workflow could not be read. |

And one summary record:

```json
{
  "recordType": "summary",
  "totalWorkflows": 12,
  "workflowsChecked": 12,
  "overdueCount": 1,
  "failingCount": 0,
  "disabledCount": 1,
  "alertCount": 2,
  "alertsDelivered": 2,
  "alertsSuppressed": 0,
  "errorCount": 0,
  "allOk": false,
  "status": "ATTENTION",
  "silenceConfirmations": 0,
  "requestsSent": 24
}
```

### Alerts

An alert is raised for every workflow that is overdue, failing or disabled.
With `suppressRepeatAlerts` on, the Actor keeps one small cursor for each
workflow in the **named** key-value store `stateStoreName`. The store must be
named: the default key-value store of an Actor belongs to one run only, so a
cursor written there would be lost before the next tick.

The cursor holds the last alerted run id and the last set of reasons, so an
hourly schedule does not repeat the same alert on every tick. A new run, a
changed set of reasons, or a workflow that recovered and then broke again, all
raise a fresh alert. A suppressed repeat sends no webhook POST and is not
charged again.

When `alertWebhookUrl` is set, the Actor sends **one** JSON POST for each run
that holds every new alert of that run. The webhook must be a public HTTPS
URL: loopback, private and reserved addresses are refused, and redirects are
not followed. A webhook that does not answer is written into the summary note;
it never fails the run, because the dataset already holds the same alerts.

### False silence

GitHub answers the workflow run list from a replica that is sometimes **weeks**
out of date. A stale answer names an old run as the newest one, which reads
exactly like a schedule that stopped to fire. This is the one verdict that this
Actor must not get wrong.

With `confirmSilence` on, a workflow that looks silent is read a second time
before the alert is raised, and the newer of the two answers wins. The second
read waits `silenceConfirmDelaySeconds` and carries `Cache-Control: no-cache`,
because the run list is served with a cache lifetime of 60 seconds and a second
read must not repeat the first page. The pause happens once for the whole run.
A healthy workflow is read twice, as usual, and never pays for the third call.

### Rate limit

Without a token, GitHub allows 60 requests for each hour from one IP address.
The Actor sends 2 requests for each workflow, and one more for each workflow
that looks silent, so an anonymous run covers about 30 healthy workflows in one
hour. On shared infrastructure that budget is shared with everything else that
leaves the same address, so an hourly schedule on more than a few workflows
will meet the limit.

Supply your own `githubToken` to raise the limit to 5000 requests for each hour
and to reach private repositories. The token is sent only to `api.github.com`.

A workflow that could not be read because of the rate limit is reported with
`status: "RATE_LIMITED"`, it is **not** charged as a check, and it does not
raise a false silence alert. The run still ends SUCCEEDED.

### Run outcome

A business verdict never fails the run. An overdue workflow, a failing
workflow, a repository that cannot be read, an exhausted rate limit and a
refused webhook URL are all reported in the dataset and in the status message,
and the run ends **SUCCEEDED**. A FAILED run means a real malfunction, or an
input that cannot be parsed.

### Pricing: pay per event

| Event | Unit | Price |
| --- | --- | --- |
| `run-started` | One Actor run. Charged once, after the input is accepted. | $0.02 |
| `workflow-checked` | One workflow read from the GitHub REST API and evaluated. A workflow that could not be read is **not** charged. | $0.002 |
| `silence-or-failure-alert` | One alert for an overdue, failing or disabled workflow, charged the first time it is raised. A suppressed repeat of the same alert is **not** charged again. | $0.03 |

A team with 20 scheduled workflows on an hourly schedule pays about $0.96 for
each day when everything is healthy. The default cap for the maximum total
charge inside one run stops a runaway schedule.

### Limits

- HTTP only. No browser, no proxy, no CAPTCHA service and no paid API.
- 1 to 100 workflows in one run.
- Read only. The Actor sends GET calls to GitHub and never changes anything.
- Redirects are not followed, and every answer is bounded in bytes and in time.

### Development

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `workflows` (type: `array`):

1 to 100 records. Each record holds owner\_repo (for example "python/cpython"), workflow\_file (the file name of the workflow, for example "stale.yml", or its numeric workflow id) and expected\_max\_interval\_minutes (the longest silence that is still normal for this schedule). A workflow that did not run inside that interval is reported as overdue.

## `githubToken` (type: `string`):

Optional. Supply your own token to watch private repositories and to lift the rate limit of 60 requests for each hour that applies to anonymous calls. The Actor never buys a token and never sends the token anywhere except to api.github.com.

## `failureStreakThreshold` (type: `integer`):

The Actor raises a failure alert after this many failed runs in a row.

## `alertWebhookUrl` (type: `string`):

Optional. One public HTTPS URL. The Actor sends one JSON POST with every alert of the run. Private, loopback and reserved addresses are refused. Leave it empty to read the alerts in the dataset only.

## `runEventFilter` (type: `string`):

Which runs count as a firing of the schedule. "schedule" reads only the runs that the cron trigger started, which is the correct choice for a nightly job. "any" reads every run of the workflow.

## `runsPerWorkflow` (type: `integer`):

How many recent runs are read for each workflow. The failure streak cannot become longer than this number.

## `suppressRepeatAlerts` (type: `boolean`):

Keep one cursor for each workflow in the key-value store, so the same alert is not sent again on every schedule tick. A changed alert reason or a new run always sends a new alert.

## `stateStoreName` (type: `string`):

Name of the named key-value store that holds one alert cursor for each workflow. The store must be named, because the default store of an Actor belongs to one run only and would lose the cursor on every tick. Give two schedules two different names when they must alert on their own.

## `confirmSilence` (type: `boolean`):

Read the run list a second time before a silence alert is raised. GitHub sometimes answers the run list from a replica that is weeks out of date, which reads exactly like a schedule that stopped to fire. This second read removes that false alert. It costs one extra API call, and only for a workflow that already looks silent.

## `silenceConfirmDelaySeconds` (type: `integer`):

How long the Actor waits before it reads the run list of the silent-looking workflows again. The GitHub run list is served with a cache lifetime of 60 seconds, so a second read without a pause can be answered from the same page as the first. The pause happens once for the whole run, and not once for each workflow.

## `timeoutSeconds` (type: `integer`):

Timeout for one call to the GitHub REST API.

## `requestsPerSecond` (type: `integer`):

Rate limit for the calls to api.github.com. The calls are sent one after the other, so this Actor cannot act as a load generator.

## `maxResponseBytes` (type: `integer`):

Hard cap on the bytes read from one API answer. A larger answer is reported as an error for that workflow.

## Actor input object example

```json
{
  "workflows": [
    {
      "owner_repo": "python/cpython",
      "workflow_file": "stale.yml",
      "expected_max_interval_minutes": 1440
    },
    {
      "owner_repo": "home-assistant/core",
      "workflow_file": "stale.yml",
      "expected_max_interval_minutes": 1440
    },
    {
      "owner_repo": "rust-lang/rust",
      "workflow_file": "ghcr.yml",
      "expected_max_interval_minutes": 2880
    }
  ],
  "failureStreakThreshold": 2,
  "alertWebhookUrl": "",
  "runEventFilter": "schedule",
  "runsPerWorkflow": 10,
  "suppressRepeatAlerts": true,
  "stateStoreName": "gha-schedule-monitor-state",
  "confirmSilence": true,
  "silenceConfirmDelaySeconds": 3,
  "timeoutSeconds": 15,
  "requestsPerSecond": 5,
  "maxResponseBytes": 2000000
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "workflows": [
        {
            "owner_repo": "python/cpython",
            "workflow_file": "stale.yml",
            "expected_max_interval_minutes": 1440
        },
        {
            "owner_repo": "home-assistant/core",
            "workflow_file": "stale.yml",
            "expected_max_interval_minutes": 1440
        },
        {
            "owner_repo": "rust-lang/rust",
            "workflow_file": "ghcr.yml",
            "expected_max_interval_minutes": 2880
        }
    ],
    "failureStreakThreshold": 2,
    "alertWebhookUrl": "",
    "runEventFilter": "schedule",
    "runsPerWorkflow": 10,
    "suppressRepeatAlerts": true,
    "stateStoreName": "gha-schedule-monitor-state",
    "confirmSilence": true,
    "silenceConfirmDelaySeconds": 3,
    "timeoutSeconds": 15,
    "requestsPerSecond": 5,
    "maxResponseBytes": 2000000
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/scheduled-github-actions-workflow-silence-and-failure-report").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "workflows": [
        {
            "owner_repo": "python/cpython",
            "workflow_file": "stale.yml",
            "expected_max_interval_minutes": 1440,
        },
        {
            "owner_repo": "home-assistant/core",
            "workflow_file": "stale.yml",
            "expected_max_interval_minutes": 1440,
        },
        {
            "owner_repo": "rust-lang/rust",
            "workflow_file": "ghcr.yml",
            "expected_max_interval_minutes": 2880,
        },
    ],
    "failureStreakThreshold": 2,
    "alertWebhookUrl": "",
    "runEventFilter": "schedule",
    "runsPerWorkflow": 10,
    "suppressRepeatAlerts": True,
    "stateStoreName": "gha-schedule-monitor-state",
    "confirmSilence": True,
    "silenceConfirmDelaySeconds": 3,
    "timeoutSeconds": 15,
    "requestsPerSecond": 5,
    "maxResponseBytes": 2000000,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/scheduled-github-actions-workflow-silence-and-failure-report").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "workflows": [
    {
      "owner_repo": "python/cpython",
      "workflow_file": "stale.yml",
      "expected_max_interval_minutes": 1440
    },
    {
      "owner_repo": "home-assistant/core",
      "workflow_file": "stale.yml",
      "expected_max_interval_minutes": 1440
    },
    {
      "owner_repo": "rust-lang/rust",
      "workflow_file": "ghcr.yml",
      "expected_max_interval_minutes": 2880
    }
  ],
  "failureStreakThreshold": 2,
  "alertWebhookUrl": "",
  "runEventFilter": "schedule",
  "runsPerWorkflow": 10,
  "suppressRepeatAlerts": true,
  "stateStoreName": "gha-schedule-monitor-state",
  "confirmSilence": true,
  "silenceConfirmDelaySeconds": 3,
  "timeoutSeconds": 15,
  "requestsPerSecond": 5,
  "maxResponseBytes": 2000000
}' |
apify call kingii98/scheduled-github-actions-workflow-silence-and-failure-report --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/scheduled-github-actions-workflow-silence-and-failure-report"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/rHI9kUBLQhoKwLSAa/builds/qfpIdCSvjuiTMuVnC/openapi.json
