---
title: "The audit log"
description: "What the Overlane audit log records, how its hash chain shows tampering, and how to read, verify and export it from the web app or the API."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.overlane.app/llms.txt
> Use this file to discover all available pages before exploring further.

# The audit log

The audit log lists every admin, policy and token action in your workspace: who did it, what it touched, when, and whether it worked. Owners and admins read it in **Settings > Audit log**. Nobody else can.

## What is recorded

One row per action, with these fields and nothing else: the time, the actor (a person's ID or an agent token's ID), whether the actor was a user or an agent, their role, the action, its category, the target's type and ID, and the result.

| Result | Meaning |
| --- | --- |
| `success` | It happened. |
| `denied` | The role or tenant check refused it. A forced request from a viewer shows up here. |
| `failure` | It was allowed, but the service could not do it. |

```js run
const categories = new Set(Object.values(AUDIT_ACTIONS));
assert.deepEqual([...categories].sort(), ["admin", "policy", "token"]);
assert.equal(AUDIT_ACTIONS["token.created"], "token");
assert.equal(AUDIT_ACTIONS["member.role_changed"], "admin");
assert.equal(AUDIT_ACTIONS["release.promoted"], "policy");
```

| Category | Examples |
| --- | --- |
| `admin` | `member.removed`, `member.role_changed`, `invitation.created`, `invitation.revoked`, `audit.exported`, `data.exported`, `channel.promoted` |
| `policy` | `run_policy.updated`, `project.updated`, `release.published`, `release.promoted`, `release.withdrawn`, `release.pinned`, `release.rolled_back`, `memory_settings.updated`, `issue_sync.updated`, `loop.created`, `loop.updated`, `loop.deleted`, `bot.registered`, `bot.revoked`, `bot.key_rotated`, `feature_override.set`, `feature_override.cleared`, `usage_policy.updated`, `budget.set`, `autonomy.changed`, `decision_settings.updated`, `alert_rule.updated`, `webhook.created` |
| `token` | `token.created`, `token.revoked`, `api_key.created`, `api_key.revoked`, `ci_secret.set`, `connector.connected` |

The list is not complete; the log records every action in these three categories.

A row holds identifiers only. It never contains a request body, a token secret, an invitation link or memory text.

## Tamper evidence

Rows of a workspace form a hash chain. Each row's hash covers the previous row's hash and its own fields, and rows are numbered 1, 2, 3 with no gaps. Edit a row, remove one or reorder them and the chain no longer checks out. The check is a pure function, the same one the service, the web app and an offline reader use.

```js run
assert.equal(verifyAuditChain("tm-1", AUDIT_FIXTURE).ok, true);
const edited = AUDIT_FIXTURE.map((row, i) => (i === 1 ? { ...row, actor: "someone-else" } : row));
const result = verifyAuditChain("tm-1", edited);
assert.equal(result.ok, false);
assert.equal(result.brokenAt, 2);
assert.equal(result.reason, "bad_hash");
const removed = AUDIT_FIXTURE.filter((_, i) => i !== 1);
assert.equal(verifyAuditChain("tm-1", removed).reason, "bad_sequence");
```

A chain cannot show that the newest rows were cut off, since a shorter chain still verifies. To notice that, keep the head hash from an earlier export or check; the log shows the current head.

## Read, filter, verify and export

In the app, filter by category and result, press **Verify log** to check the whole chain, and press **Export** to download every row.

Through the API the same three reads exist for owners and admins: the audit list at `/v1/tenants/{tenantId}/audit` (filters `category`, `result`, `action`, and paging with `after` and `limit`), `/v1/tenants/{tenantId}/audit/verify`, and `/v1/tenants/{tenantId}/audit/export`.

The export is newline-delimited JSON, one row per line with its hashes, so you can check it later without the service:

```js run
const lines = AUDIT_FIXTURE.map((row) => JSON.stringify(row)).join("\n") + "\n";
const rows = lines.split("\n").filter(Boolean).map((line) => AuditEvent.parse(JSON.parse(line)));
assert.equal(rows.length, 3);
assert.equal(verifyAuditChain("tm-1", rows).ok, true);
assert.equal(auditPaths.export("tm-1"), "/v1/tenants/tm-1/audit/export");
```

Source: https://docs.overlane.app/web-app/audit-log/index.mdx
