# GitHub Actions Workflow Logs (Webhook) Receives GitHub Actions `workflow_run` webhook events and, on each completed run, downloads its logs and streams them chunked into ~1 MB pieces split at newline boundaries. **Sync Type: Push (Webhook)** ## Overview This input ingests GitHub Actions workflow run logs **in response to webhook events** rather than by polling the GitHub API. When a workflow run completes, GitHub delivers a `workflow_run` event with `action: completed`; the input reacts to that event, downloads the run's logs, and emits them as records. It produces the **same record shape** as the polling [GitHub Actions Workflow Logs](/docs/inputs/github/github-actions-workflow-logs) input — so the two are interchangeable downstream — but differs in how and when logs are pulled. ### Why use the webhook input - **Near real-time.** Logs are fetched the moment a run finishes, instead of waiting for the next polling cycle (the polling input syncs on a fixed interval with a small propagation buffer). Expect log records within seconds of a run completing rather than minutes. - **Optimizes GitHub API usage.** The polling input repeatedly lists workflow runs across a lookback window on every sync, regardless of whether anything changed. The webhook input makes **no** list calls — it only calls the API to download logs for runs that actually completed, so it uses far fewer requests against your GitHub REST API rate limit. - **More reliable.** Reacting to a delivered event removes the entire class of polling edge cases (cursor windows, sync-boundary gaps, the maximum-job-execution-time bound). Delivery is at-least-once and de-duplicated downstream by record ID. **This is the preferred input when you want to minimize GitHub API calls and maximize reliability.** Use the polling input instead if you need to **backfill historical runs** (see the limitation below) or cannot expose a webhook endpoint. :::warning[No backfill] The webhook input only ingests runs that complete **after** the webhook is configured — webhooks are forward-only and GitHub does not replay past events. It will **not** backfill logs for runs that already finished. To capture historical runs, use the polling [GitHub Actions Workflow Logs](/docs/inputs/github/github-actions-workflow-logs) input with a **Backfill Start Time**. ::: ## How It Works 1. GitHub delivers a `workflow_run` webhook to the input's ingest URL for every run that is requested, in progress, or completed. 2. The input verifies the delivery's `X-Hub-Signature-256` HMAC against the configured **Webhook Secret** and ignores any event whose `action` is not `completed`. 3. For a completed run that is **in scope** (see **Scope** below), the input downloads the run's log archive from GitHub's CDN, extracts the root-level per-job log files, and emits them chunked into ~1 MB records. Events from out-of-scope repositories are dropped. Log archives are downloaded from GitHub's signed CDN URLs and **do not count against REST API rate limits**. ## Prerequisites Before configuring this input, you need: 1. **A Personal Access Token (PAT) or GitHub App** with permission to read Actions logs for the repositories in scope — see [GitHub authentication docs](https://docs.github.com/en/authentication). For an **Entire Organization** scope, the credential must cover **every** repository in the org (a GitHub App installed org-wide is recommended). - **Classic PAT**: `repo` (or `public_repo` for public repositories), optionally `actions:read`. - **Fine-grained PAT**: `Actions: Read` and `Metadata: Read` on the target repositories (all repositories for an org scope). - **GitHub App**: `Actions: Read` and `Metadata: Read` permissions, installed on the target repository — or installed across **all repositories** in the organization for an org scope. Note the Client ID, Installation ID, and PEM private key. 2. **A webhook secret** — a strong random string you will configure both on the input (as **Webhook Secret**) and on the GitHub webhook (as **Secret**). GitHub uses it to sign each delivery. 3. The ability to **add a webhook** to the repository (or organization) — requires admin access to the repository/org. ## Setup ### Step 1: Create the input in Monad 1. Create a **GitHub Actions Workflow Logs (Webhook)** input and add it to a pipeline. 2. Configure the **Scope** — a single repository (**Single Repository**) or every repository in an org (**Entire Organization**) — along with the **Authentication Method** and a **Webhook Secret**. 3. Copy the **Pipeline ID** from the pipeline details page. ### Step 2: Add the webhook in GitHub 1. Go to **Settings** > **Webhooks** > **Add webhook** — in the **target repository** for a Single Repository scope, or in the **organization's** **Settings** > **Webhooks** for an Entire Organization scope. 2. Fill in: - **Payload URL**: `https://app.monad.com/api/v2/http/send/{pipeline_id}` (replace `{pipeline_id}` with your Pipeline ID) - **Content type**: `application/json` - **Secret**: the same value you set as the input's **Webhook Secret** - **Which events**: select **Let me select individual events**, then check **Workflow runs** (uncheck everything else) 3. Click **Add webhook**. GitHub will send a `ping` event on creation; that (and any non-`workflow_run` event) is acknowledged and ignored. The input begins emitting logs the next time a workflow run completes. ## Configuration ### Settings | Setting | Type | Required | Default | Description | |---------|------|----------|---------|-------------| | Scope | oneOf | Yes | Single Repository | Which repositories' `workflow_run` events this input accepts: a **Single Repository** or an **Entire Organization**. Events from out-of-scope repositories are dropped. See sub-fields below. | | Authentication Method | oneOf | Yes | Personal Access Token | Authentication method used to download logs: Personal Access Token or GitHub App. Needs `actions:read` on the repositories in scope. See sub-fields below. | | Webhook Secret | secret | Yes | - | Shared secret configured on the GitHub webhook. Used to verify the `X-Hub-Signature-256` signature of every delivery before it is accepted. | #### Scope: Single Repository | Field | Type | Required | Description | |-------|------|----------|-------------| | Owner | string | Yes | Repository owner (user or organization name) | | Repository | string | Yes | Repository name (without the owner prefix, e.g., `api-service` not `owner/api-service`) | Only events from this owner/repository are processed; deliveries from any other repository (for example, on an org-wide webhook) are dropped. #### Scope: Entire Organization | Field | Type | Required | Description | |-------|------|----------|-------------| | Organization | string | Yes | The organization (or user) name that owns the repositories. All repositories under this owner are accepted. | Events from **every** repository owned by this organization are processed; deliveries from any other owner are dropped. Pair this with an **organization-level webhook** (GitHub's organization **Settings** > **Webhooks**) so a single input covers all current and future repositories in the org without configuring each one individually. When using organization scope, the configured **Authentication Method** must be able to read Actions logs for **every** repository in the org — a GitHub App installed across the organization, or a PAT whose owner has access to all the repositories. A token scoped to a single repository will fail to download logs for the others. #### Authentication Method: Personal Access Token | Field | Type | Required | Description | |-------|------|----------|-------------| | Personal Access Token | secret | Yes | Personal access token with `repo` and/or `actions:read` scopes. | #### Authentication Method: GitHub App | Field | Type | Required | Description | |-------|------|----------|-------------| | Client ID | string | Yes | The GitHub App's client ID | | Installation ID | string | Yes | The installation ID for accessing the repository | | Private Key | secret | Yes | The GitHub App's private key in PEM format | ## Record Fields Each emitted record represents a single chunk of a job's log file. The record shape is identical to the polling input. | Field | Description | | --- | --- | | `workflow_meta` | Object containing identifying metadata about the run, workflow, and job that produced this log file. The same `workflow_meta` is repeated on every chunk of the same file. | | `workflow_meta.run_id` | Unique identifier for the workflow run within the repository (GitHub's run ID). | | `workflow_meta.run_attempt` | The attempt number for re-runs; starts at 1. | | `workflow_meta.workflow_id` | Unique identifier for the workflow definition itself. | | `workflow_meta.workflow_name` | Human-readable name of the workflow (e.g., "CI/Build"). | | `workflow_meta.run_number` | Sequential run number within the workflow. | | `workflow_meta.head_branch` | Git ref the run was associated with — branch name, tag, or PR ref. | | `workflow_meta.head_sha` | Git commit SHA for the code that ran. | | `workflow_meta.event` | The trigger event (e.g., `push`, `pull_request`, `schedule`). | | `workflow_meta.status` | Run status. Always `completed` — this input only ingests completed runs. | | `workflow_meta.conclusion` | Final result: `success`, `failure`, `cancelled`, etc. | | `workflow_meta.created_at` | RFC3339 timestamp when the run was created. | | `workflow_meta.updated_at` | RFC3339 timestamp of the most recent update to the run. | | `workflow_meta.run_started_at` | RFC3339 timestamp when the run began executing. | | `workflow_meta.html_url` | Link to the run in the GitHub UI. | | `workflow_meta.repository` | Repository identifier in `owner/repo` format. | | `workflow_meta.actor` | GitHub user or app that triggered the run. | | `workflow_meta.triggering_actor` | The actor that directly triggered this particular run. | | `workflow_meta.job_name` | Human-readable name of the job within the workflow. | | `workflow_meta.log_file` | Source filename within GitHub's log archive (e.g., `0_build.txt`). | | `file_id` | Stable, deterministic identifier for the source log file: `{owner}/{repo}/{run_id}-{run_attempt}-{log_file}`. Group records by `file_id` to reassemble a full file. | | `chunk_meta.chunk_index` | Zero-based position of this chunk within the source file. | | `chunk_meta.last_chunk` | `true` on the final chunk of a file; `false` on every earlier chunk. | | `chunk_meta.chunk_size_bytes` | Byte length of the `content` for this chunk. | | `chunk_meta.truncated_lines` | Count of log lines in this chunk that exceeded 1 MB and were truncated. `0` for nearly all real-world logs. | | `content` | The raw log text for this chunk, as UTF-8. | ## Reconstructing full log files To rebuild a complete log file from its chunks: 1. **Group by `file_id`**: Collect all records that share the same `file_id`. 2. **Sort by chunk index**: Within each group, sort records ascending by `chunk_meta.chunk_index`. 3. **Verify completeness**: Confirm the highest-index record has `chunk_meta.last_chunk == true` and that chunk indexes form a contiguous sequence from 0 to N. 4. **Concatenate**: Append the `content` strings in index order. ## Content format GitHub's job log files have a consistent structure preserved verbatim in `content`: - **Leading byte-order mark.** The first chunk of every log file begins with `U+FEFF` (the UTF-8 BOM). - **Per-line timestamp prefix.** Each line is ``, e.g. `2026-04-28T00:31:56.3946773Z Current runner version: '2.334.0'`. - **Inline GitHub annotation markers.** Tags like `##[group]...##[endgroup]`, `##[error]`, `##[warning]`, `##[command]`, and `##[debug]` appear as literal text in `content`. Non-UTF-8 bytes (rare) are replaced with the Unicode replacement character `U+FFFD` when JSON-encoded. ## Limitations - **No backfill.** Only runs completing after the webhook is configured are ingested. See the warning at the top; use the polling input for historical data. - **Scope filtering.** The **Scope** setting accepts events from either one repository (**Single Repository**) or every repository in an org (**Entire Organization**); deliveries from outside the configured scope are dropped. - **Throughput on large orgs.** With an **Entire Organization** scope, every repo's completed runs flow through a single input, and log archives (which can be large) are downloaded sequentially. For organizations with many repositories completing runs concurrently, log records may lag behind run completion under heavy load. If you need to cover a very large or high-volume org, consider splitting it across multiple inputs (for example, by team or repository group) using **Single Repository** scopes. - **Attempt labeling on rapid re-runs.** Each record's `run_attempt` comes from the event, while the log content is the run's latest attempt at fetch time. In the normal timeline these match (the just-completed attempt is the latest when its event fires). If a re-run starts before a prior attempt's event is processed, the content may be the newer attempt's while the metadata names the older — the same at-least-once labeling caveat the polling input carries. - **Delivery reliability.** Ingestion is at-least-once: GitHub retries failed deliveries, and a transient log-download failure is retried. Duplicate deliveries collapse downstream because record IDs (`file_id` + `chunk_meta.chunk_index`) are deterministic — upsert-by-id sinks absorb them transparently; append-only sinks may see bounded duplicates. - **Expired logs.** GitHub retains workflow logs for ~90 days. A completed-run event whose logs have already expired (404 / 410) is skipped without failing. - **Payload Limit.** Github enforces a 25 MB limit on payloads sent from their webhook, thus any payloads sent to the configured url will be dropped as they could not have come from github. ## Troubleshooting ### 1. Deliveries rejected (401) - **Invalid signature**: Confirm the **Secret** on the GitHub webhook exactly matches the input's **Webhook Secret**. GitHub's webhook **Recent Deliveries** tab shows the response; a `401` indicates a signature mismatch or missing `X-Hub-Signature-256`. - **Content type**: Ensure the webhook **Content type** is `application/json`. ### 2. No data - **Webhook not firing**: In the repository's **Settings** > **Webhooks** > **Recent Deliveries**, confirm `workflow_run` events are being delivered with `2x` responses. - **Wrong events selected**: Ensure **Workflow runs** is checked. Other events (e.g. `push`) are acknowledged but produce no logs. - **No completed runs yet**: The input emits only on `action: completed`. A run that is still queued or in progress produces nothing until it finishes. ### 3. Missing logs for some repositories - Events from repositories outside the configured **Scope** are intentionally dropped. For a **Single Repository** scope, confirm the **Owner**/**Repository** match the repo whose webhook is firing — an org-wide webhook delivers events for every repo, but only the in-scope one is processed. For an **Entire Organization** scope, confirm the **Organization** matches the owner of the repositories whose webhook is firing. - For an **Entire Organization** scope, also confirm the **Authentication Method** can read Actions logs for the affected repository — a credential that only covers some repos will fail to download logs for the rest (see authentication errors below). ### 4. Authentication / download errors - **"invalid credentials" / 403 on download**: Verify the PAT or GitHub App can read Actions logs for the in-scope repository (`actions:read` / `Actions: Read`). - **Expired logs**: Logs older than ~90 days are no longer downloadable and are skipped. ## Sample Record ```json { "workflow_meta": { "run_id": 24447590112, "run_attempt": 1, "workflow_id": 233609137, "workflow_name": "Copilot code review", "run_number": 158, "head_branch": "refs/pull/1879/head", "head_sha": "9abf70ec123456789abcdef0123456789abcdef0", "event": "pull_request", "status": "completed", "conclusion": "success", "created_at": "2026-04-15T09:42:40Z", "updated_at": "2026-04-15T09:44:32Z", "run_started_at": "2026-04-15T09:42:40Z", "html_url": "https://github.com/monad-inc/inputs/actions/runs/24447590112", "repository": "monad-inc/inputs", "actor": "Copilot", "triggering_actor": "Copilot", "job_name": "Cleanup artifacts", "log_file": "0_Cleanup artifacts.txt" }, "file_id": "monad-inc/inputs/24447590112-1-0_Cleanup artifacts.txt", "chunk_meta": { "chunk_index": 0, "last_chunk": true, "chunk_size_bytes": 5062, "truncated_lines": 0 }, "content": "2026-04-15T09:44:30Z ##[group]Run actions/upload-artifact@v4\n2026-04-15T09:44:31Z Uploading artifact\n2026-04-15T09:44:32Z Artifact successfully uploaded\n2026-04-15T09:44:32Z ##[endgroup]\n" } ``` ## Related Articles - [GitHub Actions Workflow Logs (polling input)](/docs/inputs/github/github-actions-workflow-logs) - [workflow_run webhook event](https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_run) - [Securing your webhooks (X-Hub-Signature-256)](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries) - [Download workflow run logs](https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#download-workflow-run-logs) - [GitHub Authentication](https://docs.github.com/en/authentication) ## Sync frequency This is a push (receiver) input: Monad ingests records as the source sends them, so there is no polling interval. See [Input Sync Frequency](../../guides/sync-frequency) for details.