Audit Log

An audit log is an append only record of who changed what and when. What an entry carries, how a GitHub Actions team queries one, and how to export it.

An audit log is an append only record of who changed what and when, kept so that a change can be reviewed after the fact. Each entry names an actor, an action, a target, and a timestamp, and entries are added rather than edited, so the sequence read a year later matches the sequence written at the time.

For a team running GitHub Actions, the audit log is where configuration questions land: which account added a runner, who moved a repository into a runner group, and when a permission was widened. Job output lives elsewhere.

Definition

An audit log is defined by four properties, and a record missing any one of them answers a reviewer's question only partly.

PropertyWhat it meansWhy a reviewer needs it
Append onlyEntries are added; existing entries are never rewritten or deleted in placeA record that can be edited proves nothing about the past
AttributedEvery entry names the identity that acted: a user account, an application, or a machine identity"Someone changed the setting" closes no review
TimestampedEvery entry carries a fixed time, normally in UTCOrdering and windows such as "during Q2" depend on it
Retained and readableEntries survive for a defined period and can be read by people other than the actorSelf-reported history is not independent

Application logs, metrics, and traces exist to explain what a running system is doing right now, and they are routinely sampled, rotated, and dropped. An audit log serves the opposite time horizon: it is read months later by someone reconstructing a decision, often during an access review or an incident postmortem.

What an entry carries

GitHub exposes organization audit events through the REST endpoint GET /orgs/{org}/audit-log, documented in the GitHub REST API reference for organization audit logs, checked on 2026-08-13. Each entry in the response is a flat JSON object with a stable set of fields.

FieldContent
actionThe event name in category.verb form, such as self_hosted_runner.created
actorThe account that performed the action
actor_ipThe source address, present when the organization has enabled IP display
created_atEvent time as a Unix timestamp in milliseconds
orgThe organization the event belongs to
repoThe repository the event targeted, when the action has one
userThe account the action was performed on, for membership and permission events
_document_idA unique id per entry, useful for deduplicating repeated exports

Two parameters shape the result set. include selects the event families to return and accepts web, git, or all. phrase carries a search string built from qualifiers, the same syntax the web interface uses.

Qualifiers that answer a review question

QualifierExampleMatches
action:action:self_hosted_runnerEvery event in that category, or a single event with the full category.verb form
actor:actor:dana-engEvents performed by one account
repo:repo:octo-org/apiEvents targeting one repository
created:created:2026-04-01..2026-06-30Events inside a date window
country:country:DEEvents by source country

The qualifier list and the date range syntax are documented in GitHub's guide to reviewing the audit log for an organization, checked on 2026-08-13. The event names themselves, grouped by category, are in audit log events for your organization. Runner changes sit in the self_hosted_runner and self_hosted_runner_group categories.

Retention is the property teams discover late. GitHub keeps audit events for a documented window rather than forever, so a review that reaches back further than that window needs the events copied somewhere the organization controls before they age out.

Example

A reviewer opens a quarterly access review with a narrow question: which account changed a runner setting in the last quarter, and when. The question has two halves, an actor and a time, which is exactly the shape an audit log is built to answer.

In the web interface the path is organization Settings, then Logs, then Audit log, with this search phrase:

action:self_hosted_runner created:2026-04-01..2026-06-30

The same query through the API returns JSON that can be attached to the review:

gh api --paginate "/orgs/octo-org/audit-log" \
  -f include=web \
  -f phrase="action:self_hosted_runner created:2026-04-01..2026-06-30"

A matching entry looks like this, and it closes both halves of the question:

{
  "action": "self_hosted_runner.created",
  "actor": "dana-eng",
  "org": "octo-org",
  "created_at": 1780531200000,
  "_document_id": "aBc123-example"
}

Running that query by hand once a quarter works until the window in question is older than the retention period. A scheduled workflow keeps the evidence instead:

name: export-audit-log
on:
  schedule:
    - cron: '0 6 1 * *'
  workflow_dispatch:

permissions:
  contents: read

jobs:
  export:
    runs-on: ubuntu-latest
    steps:
      - name: Pull the last month of runner configuration events
        env:
          GH_TOKEN: ${{ secrets.AUDIT_LOG_TOKEN }}
        run: |
          since=$(date -u -d '1 month ago' +%Y-%m-%d)
          gh api --paginate "/orgs/${{ github.repository_owner }}/audit-log" \
            -f include=web \
            -f phrase="action:self_hosted_runner created:>=${since}" \
            > runner-events.json

      - name: Keep the export with the review record
        uses: actions/upload-artifact@v4
        with:
          name: runner-audit-events
          path: runner-events.json
          retention-days: 90

Three details in that workflow matter. The token comes from a secret because the GITHUB_TOKEN minted for a run is scoped to one repository and cannot read organization audit events; the endpoint requires an organization owner and the read:audit_log scope. --paginate is present because a quarter of events for a large organization exceeds one page. And _document_id in each entry lets a downstream job drop duplicates when two monthly runs overlap at a boundary.

An artifact is a first step rather than an archive. Copying the export into storage the workflow cannot rewrite, and that a repository administrator cannot clear, keeps the append only property intact once the events leave GitHub.

FAQ

What separates an audit log from an application log?

Purpose and write behavior. An application log records what a program did so an engineer can debug it, and it can be rotated, sampled, or rewritten. An audit log records who changed a configuration or a permission so a reviewer can reconstruct that change later, so entries are written once, carry an identity and a timestamp, and are readable by people who did not make the change.

Which GitHub Actions changes appear in an organization audit log?

Configuration and permission changes rather than job output. Adding or removing a self-hosted runner, moving a runner between runner groups, changing Actions settings for a repository, and changing who can administer those settings all produce entries. What a job printed lives in the workflow run logs instead, under a separate retention setting.

Can a workflow read the audit log through the REST API?

Yes, with a token that carries the read:audit_log scope and belongs to an organization owner. The GITHUB_TOKEN minted for a workflow run is scoped to one repository and does not carry that scope, so a scheduled export job reads the token from a secret instead.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.