Audit Log
The audit log under Settings → Audit Log is the filterable record of who changed what: create, update, and delete actions on resources, plus lifecycle events such as runs being cancelled, secrets rotated, tokens revoked, and configuration imported - each with its actor, outcome, and timestamp.
Reading the log
Each row answers the questions an auditor asks. Time shows the moment to the second, in UTC and local time, with how long ago it was. Organization names the tenant, or Platform for events that concern the platform rather than a single organization. Action is the verb, with the event type underneath. Object is the kind of thing changed, its display name (a link to the object where it still exists), and its id. Changes counts the fields that differ and names the first few. Actor shows who acted, how they authenticated, and the client address. Outcome is success, failure, denied, conflict, or attempt.
Pagination, sorting, and filtering all happen server-side, so the views stay fast on large histories. Filter by organization, action, object type, outcome, or event type, search the actor and the object (by type, id, or name), and limit the range with the 24-hour, 7-day, and 30-day presets or an explicit from/to (entered in local time); sort by time, action, object type, actor, or outcome.
Inspecting an entry
Clicking a row opens the entry. Its When card lists the time in UTC, local time, relative form, and the raw timestamp; Who the actor, their subject, the authentication method, the client address, and the user agent; What the object, its id, the organization, the event type, the request (method and path) that caused it, and the entry id. Details lists every event-specific key the entry recorded, and Changes shows a field-by-field table of what changed (the old value beside the new one, digests for values too large to store) with a text diff of the full snapshots one tab away. An entry with only a before or only an after state shows that state; an entry with no snapshot says so. Copy JSON copies the whole entry and Copy link a URL that reopens it.
An object's own history
The log is not the only way in. The secret, variable, Git repository, secret backend, organization, file repository, and registry credential pages each carry a History card listing the most recent entries about that object, with a link into the log filtered to it. The card opens the same entry dialog described above. It is shown only to callers who may read the audit log, and stays hidden while that is still unknown, so a caller who may not read the log gets a page without a card rather than a refused request.
Filtering and sharing links
The query string is the view: filters, the date range, the sort, the page, and the open entry are all in the URL, so a filtered investigation or a single entry can be bookmarked or pasted into a ticket. Opening a link whose entry no longer exists (retention removed it) shows a notice rather than an empty dialog. Resource pages link into the log with their own object pre-filtered.
What is recorded
Every entry names an action (the verb: create, update, soft_delete, restore, start, approve, and so on), an outcome (success, failure, denied, attempt, or conflict), the resource type it concerns, and an event type of the form namespace.event (for example entity.updated or run.started). These vocabularies are fixed lists, published in the vocabulary reference as AuditAction, AuditOutcome, and AuditResourceType. A code change that invents a new action, resource type, or event type fails the test suite, where HEGEMONY_AUDIT_STRICT_VOCABULARY is on; in production the entry is stored as written and the drift is logged. An unknown outcome is the exception: it is rejected whatever that setting says, because the column carries a CHECK constraint and storing one would fail inside the caller's own transaction.
A run start is always the start action; how it was triggered (manually, from a schedule's Run Now, by the scheduler, or by a webhook) is the entry's trigger_source detail; a scheduler-started run is filed under the schedule's organization, and a due occurrence that produced no run (its flow was deleted, or the flow was at capacity) is recorded as schedule.skipped. A manual run an admin forced past a flow that refuses them carries manual_run_override beside its trigger_source; the key is present only when the override was used, so searching for it finds exactly those runs. An approval is recorded at every step from every path: requested, approved or rejected (by a person, or by the worker), expired (by the worker or the timeout job), or cancelled, each with the request's state before and after. Asking an inventory provider what its own instance serves is recorded as discover, with the object types the answer named; it is a separate verb from sync because it reads the shape of the source, not its rows, and stores no inventory. A platform sync plan, export, or apply is recorded whether a person or the scheduler started it: the entry's trigger_source says which, and scheduled operations are attributed to system:scheduler. Flow authoring is recorded at every step: saving a draft, restoring a version or reverting to the committed one (each with the graph's digest before and after), duplicating a flow, pulling it from or pushing it to Git (with the commit identifiers and the sync outcome, even when nothing changed or the sync was rejected), and adding, changing, or removing its notification subscriptions. Bulk attachment uploads and renames record one entry per item plus a summary, tied together by a batch_id detail. Storage changes are recorded the same way: purging a binary artifact's content (with the object reference and hash that were removed, and a failed purge as a failure), creating, renaming, or deleting a file repository folder, moving a file between folders, and provisioning an organization's default file repository. Adding, changing, or removing a registry credential is recorded with the registry host and the password's secret reference, never a value. The login test leaves no entry: it changes nothing but the time of the last successful test, which the credential's own page shows. A notification destination's configuration is part of its snapshot, with credential values redacted; a secret update is recorded even when it changed nothing; and the one-time instance bootstrap import is recorded as system:bootstrap. Restoring a soft-deleted object records the object as it stands after the restore; platform sync profiles record their creation, every change, and their removal with before and after snapshots; and a repository-wide Git sync records one summary entry on the repository next to the per-flow entries. Every mutating route is now either audited or exempt for a stated reason; the audit events reference lists both. Entries written before this vocabulary existed were migrated onto it, and each keeps its original spelling in its details under a legacy_outcome, legacy_action, or legacy_event_type key.
Who is recorded
Every entry names the actor (username and subject), how they authenticated (jwt for an identity-provider token, pat for a personal access token, run_token for a run step's short-lived token, internal for the worker and scheduler, system for background jobs, dev_bypass in development mode, public for inbound webhooks), the client address, and the user agent. Background jobs are recorded as system:<kind>, for example the scheduler or a maintenance job, so their entries stay attributable without pretending a person acted. A run step's token is recorded as run:<run id>/<step id>: for example a tf.apply step writing managed state, whose entry also names the run and step in its details. A container step's pulls from the platform registry leave no entry: the worker's grant of that step's registry token is exempt, the registry route only answers reads, and the step's run events record which image was served and from where. Deleting a tag or a repository on the Container Images pages is recorded as container_image: a tag delete with the repository, the tag, its digest and size, a repository delete with the repository, the tags removed and the size. Browsing is not recorded.
The client address is the connection's peer. Behind a proxy that is the proxy's address; set HEGEMONY_TRUST_PROXY_HEADERS when the proxy sets X-Forwarded-For (the bundled UI's nginx does) to record the caller's address instead. Unauthenticated webhook calls record a masked address.
What the details hold
An entry's details carry the object's before and after states, a changes map listing exactly the fields that differ, the request (method and path) the change came from, and any event-specific keys.
The changes map names the leaf that changed, not the object holding it. One edited key inside a configuration reads as config_json.smtp_host, with the old and new value beside each other, rather than as both configurations side by side for you to compare yourself. Four things are reported whole instead: a list (a reordered list is one change, not one per element), a value that changed type, a value stored as a digest, and an object nested more than four levels deep or rewritten so completely that naming every leaf would be longer than showing the object.
Credential values never appear: keys that hold a password, secret, token, or key are replaced with a redaction marker, while references to them (a password_ref) stay visible, because a changed reference is exactly what an auditor needs to see. A rotated credential still shows as changed, with the marker on both sides — the diff is taken before redaction, so a rotation cannot hide by making both sides look identical.
Values too large to store, such as a flow's graph, are recorded as a digest and size, so an edit still shows as a change. An entry whose details exceed HEGEMONY_AUDIT_DETAILS_MAX_BYTES keeps its changes and replaces the snapshots with digests, marked truncated.
What changed in a flow
A digest says a flow's definition changed but not what changed in it, so an entry that edited one also carries definition_diff: a unified diff, the same patch format git shows, over the flow's canonical YAML — the form the flow exports as and the form stored in a Git-synced repository. Reading a change in the audit log and reading it in a pull request therefore show the same thing.
Because the canonical form strips fields sitting at their defaults and orders keys the same way every time, the patch holds only real changes: a renamed step is one replaced line, an added step is a few added lines, and reordering nothing produces no diff at all. A patch longer than 400 lines is cut there and marked truncated with the full line count, so rebuilding a flow from scratch cannot push an entry over its size budget. An edit that changed only the name or the description adds no definition_diff key at all.
Reliability guarantees
An entry is written in the same database transaction as the change it describes, so the two are committed together or not at all: a change never lands without its entry, and an entry never claims a change that was rolled back. When a request rolls back with entries pending, or a session closes before committing them, each entry's full payload is written to the structured log under the hegemony.audit logger instead, so an attempt is never silently lost. Facts that must survive whatever the request does, such as a decision whose workflow signal failed after it was saved, or a conflict resolution rejected because the plan changed underneath it, are written through an independent session at once.
Application code cannot modify or delete an entry: the ORM refuses at flush time. Set HEGEMONY_AUDIT_LOG_MIRROR to also write every committed entry to the structured log for shipping to an external system.
Access decisions
A refused request is recorded too. When a signed-in caller lacks the role, the organization membership, or the platform standing a route requires, the 403 is written as an authz.denied entry (action access, outcome denied) whose object is the route itself, for example POST /orgs/{org_ref}/members, with the roles required, the roles the caller had, the reason, and the organization concerned. A personal access token that is revoked or expired still names its owner, so its 401 is an auth.failed entry attributed to that token. Requests with no credential, a malformed header, an unknown token, or an invalid identity-provider token name nobody; they go to the structured log under hegemony.audit only.
A route an administrator switched off through a permission override is recorded the same way. That check runs before authentication, so the entry names the address the request came from rather than a person, but a kill switch firing is exactly the refusal an auditor asks about.
Repeated denials by the same caller on the same route within HEGEMONY_AUDIT_DENIAL_THROTTLE_SECONDS (ten seconds by default) are counted rather than written one by one: the first entry after the window carries a suppressed_count, so a retry loop is one entry with a number, never a flood and never a gap.
Sign-in itself leaves a trace. The first request from a new identity records user.provisioned for the local user row it created; joining the default organization on first login records org.member_added with source: auto_join; and memberships granted, re-ranked, or removed through an identity-provider group mapping record org.member_added, org.member_role_changed, or org.member_removed with source: idp, each with the membership's state before and after.
Retention and immutability
Entries are kept forever unless HEGEMONY_AUDIT_RETENTION_DAYS sets a window. With a window set, the audit_log_retention maintenance job deletes older entries in bounded batches on its own interval, and the Stats & Cleanup panel can run the same pass on demand; either way the deletion is recorded as an audit_log.pruned entry naming how many rows went and up to which date. The maintenance jobs reference lists the job's interval setting.
On PostgreSQL the table is append-only at the database level: a trigger refuses every update and delete except from the retention pass. That guards against accidental changes from application code, not against someone holding the database credential itself.
Who can see it
Viewing the audit log requires the admin role - every audit-log endpoint is admin-only. The permissions reference lists the full role matrix.
Related
- Organizations - how events are scoped to an organization and what platform-level means.
- Permissions reference - the role matrix.