> Source: https://wexa.ai/docs/governance/audit-and-lifecycle

# Lifecycle and audit

Every tool call is recorded as a **lifecycle**, the same stages in order, and as events in a hash-chained **audit** trail, so you can answer who did what, under which scope, and whether the record has been altered. Any project-scoped member can read both.

1. Take the `lifecycle_id` from the result or error of the call.
2. In the console, open **Governance → Audit & Lineage** for the platform audit trail.
3. Over the API, read one call with `GET /v1/lifecycles/{lifecycle_id}`.
4. Read the audit stream with `GET /v1/audit`, and keep each response, because the gateway's trail does not survive a restart.
5. Check the record with `GET /v1/audit/verify`.

## Limits

| Limit | What to expect |
| --- | --- |
| Audit trail storage | Gateway memory only. A restart, crash or redeploy empties it, and an empty chain still verifies as intact. |
| `GET /v1/audit` | No parameters: no date range, filter, pagination or export. |
| Hash coverage | Not `role`, `source`, `org_id`, `project_id` or `trace_id`. |
| Another organization's lifecycle | Returns `404`. |
| Policy decisions | Filtered by organization only, and not durable either. |

**How a call is recorded**

## How it works

1. The gateway mints a `lifecycle_id` the moment a call arrives, before anything is decided.
2. The call passes the stages below in a fixed order, and each is recorded.
3. The call writes audit events, each hashed together with the hash of the one before it.
4. You retrieve a lifecycle by id, the audit stream, and a verification of the chain.

The `lifecycle_id` is the thread. It turns "an agent did something odd on Tuesday" into one retrievable record, so keep it. It appears in:

- the successful result, `{ "lifecycle_id": ..., "result": ... }`
- the error body of a refused call, and `lifecycleId` / `lifecycle_id` on the SDK error object
- the pending payload of an [approval](/docs/governance/policy-and-approvals)
- the policy decision, every audit event, and the lifecycle record and its trace

| Stage | What it does |
|---|---|
| `S1:authenticate` | The transport verified the credential. Recorded so the trail is complete. |
| `S2:resolve-scope` | Organization, department, project and role are pinned, and the tool's required grant is checked. |
| `S3:rate-quota` | The [quota](/docs/administration/usage-quota-and-billing) buckets for the project and organization are consumed. |
| `S4:validate-input` | The tool checks its arguments and reports which graph labels the call touches. |
| `S5:policy` | The policy engine returns allow, deny or allow+gate, and the decision is recorded. |
| `S6:approval` | A gated call parks here, a resumed call satisfies the gate here, and an ungated call skips it. |
| `S7:dry-run` | The tool simulates the call and summarizes what it would do. |
| `S8:execute` | The work happens. |
| `S9:post-process` | The result is shaped and redacted before it leaves. |
| `S10:record` | The audit event closing the call is appended. |

Each stage has a status of `ok`, `failed`, `pending` or `skipped`, and a detail line. The order makes the guarantees legible: scope is pinned before quota is consumed, policy runs after validation so it can see the labels, execution happens after the gate, and redaction happens after execution, never instead of it.

A resumed call redeems its token between `S3` and `S4`, after quota and before validation. That is why quota exhaustion is the only failure a resume can safely be retried through.

A failed stage stops the call, and its name becomes the `error` field, which is how both SDKs choose a typed error: `S3:rate-quota` becomes `QuotaExceeded`, `S5:policy` becomes `PolicyDenied`, `S6:approval` becomes `ApprovalError`.

**Retrieve one call and the audit trail**

## Retrieve one call

In the console, the platform audit trail is under **Governance → Audit & Lineage**. Over the API, read a lifecycle by id.

**Retrieve a lifecycle**

**REST API**

```bash
curl -s "https://fabric.wexa.ai/v1/lifecycles/qlc_000412" \
  -H "Authorization: Bearer $TOKEN"
```

**TypeScript SDK**

```ts
const record = await fabric.lifecycle('qlc_000412')
```

**Python SDK**

```python
record = fabric.lifecycle("qlc_000412")
```

The record has the tool, resolved scope, surface, project mode, an overall status (`completed`, `denied`, `pending` or `failed`), start and end times, and each stage's status, detail and timestamp. A record from another organization returns `404`, not an empty record.

## Retrieve the audit trail

The SDKs have no audit method, so Python and TypeScript call the route directly.

**Retrieve the audit trail**

**REST API**

```bash
curl -s "https://fabric.wexa.ai/v1/audit" \
  -H "Authorization: Bearer $TOKEN" \
  > audit-$(date +%Y%m%dT%H%M%S).json
```

**TypeScript SDK**

```ts
const res = await fetch('https://fabric.wexa.ai/v1/audit', {
  headers: { Authorization: `Bearer ${process.env.FABRIC_TOKEN}` },
})
const { events } = await res.json()
```

**Python SDK**

```python
import os, urllib.request, json

req = urllib.request.Request(
    "https://fabric.wexa.ai/v1/audit",
    headers={"Authorization": f"Bearer {os.environ['FABRIC_TOKEN']}"},
)
events = json.load(urllib.request.urlopen(req))["events"]
```

`GET /v1/audit` takes no parameters and returns every event visible to you, in the order written. **There is no date range, no filter by actor or action, no pagination and no export.** A `limit` or `actor` query string is accepted and ignored. To produce evidence, fetch the whole array, filter it yourself and keep the file.

Visibility is by scope, not by role. An event is returned when its organization matches yours, and, when your token names a project, when its project matches too. Events with no organization or project are system-level and visible to everyone in the organization.

One real event:

```json
{
  "id": "aud_000016",
  "ts": "2026-09-15T15:56:18.332011859+05:30",
  "actor": "bob",
  "role": "PROJECT_MEMBER",
  "action": "create-ontology.approval.redeemed",
  "target": "ar_6abb9f3c0b5ae73d6cdd6d41",
  "source": "rest",
  "kind": "Approvals",
  "lifecycle_id": "qlc_000005",
  "org_id": "org_d18",
  "project_id": "proj_d18",
  "payload": { "args_keys": ["mode", "domain", "nodes"] },
  "payload_hash": "022689d55b5c02b6148cc266d9f9d6fc9ea105d3f16ad5eb6dbb66a19e4d2692",
  "prev_hash": "b6e28fdd59d5f3589d8aa7fe17a2b08957d55d2ec396b120d080c5fe3ee3f706",
  "hash": "9807e8645c13740fc158a50e392dcd91f762ecd57bf882fc94c3da158a4eedfe"
}
```

A complete event carries:

- **who:** the acting user and role.
- **what:** an action such as `query-context.executed`, `create-ontology.policy.denied` or `approval.approved`, and its target.
- **where from:** the surface (`mcp`, `rest` or `system`) and, in the payload, the originating client, its version and the credential or MCP connection.
- **scope and call:** the organization and project, the `lifecycle_id` and the trace id.
- **kind:** one of `Auth`, `Tool calls`, `Policy`, `Approvals` or `System`, the closest thing to a category filter you get.

Consequential tools get the full payload. A call to a tool that only reads prose, such as `docs`, is marked lightweight. What the payload holds depends on the call:

- **Executed call:** `args_keys`, the **names** of the arguments and not their values.
- **Refusal:** the stage and reason.
- **Call that touched the context graph:** also the nodes and relationships involved: labels, identity keys, the reference value of those keys, the operation and the per-node verdict. The node's own property values are never written.

So the trail shows that a call was made and which fields it carried. For what data it touched, go to the [context graph](/docs/context/context-graph) and the lifecycle record.

A denial writes an event, and so does requesting an approval, redeeming its token, and approving or rejecting over REST. A decision made in the Inbox is recorded in the platform audit trail.

**Verify the chain and answer an auditor**

## Verify the chain

```bash
curl -s "https://fabric.wexa.ai/v1/audit/verify" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{ "chain_intact": true, "broken_at": "" }
```

Each event's hash covers these fields: `id`, `ts`, `actor`, `action`, `target`, `kind`, `lifecycle_id`, the payload hash and the previous event's hash.

Verification walks from the start and checks, in order, that the payload re-hashes to `payload_hash`, that `prev_hash` equals the previous event's hash, and that the event re-hashes to `hash`. The first failure stops the walk and its id is returned as `broken_at`.

- **Some recorded fields are outside the hash:** `role`, `source`, `org_id`, `project_id` and `trace_id`. A passing verification proves who did what, when and in which order is unaltered. It does not prove which project or organization an event belongs to, the actor's role, or the surface. A claim that evidence shows an event belonged to project X is not supported, because the project id could change without `chain_intact` turning false. The trace id is out by design, so events written before the field existed still verify.
- **The payload is covered through its own hash,** including the originating client, version and connection.
- **Verification is not scope-filtered.** It attests the whole chain the gateway holds, so the answer is about the record's integrity and not your events specifically.

## Answer an auditor's question

"Did this happen, who allowed it, and has the record been edited" takes these calls, joined by the `lifecycle_id`:

```bash
curl -s "https://fabric.wexa.ai/v1/lifecycles/qlc_000412" -H "Authorization: Bearer $TOKEN"
curl -s "https://fabric.wexa.ai/v1/policy-decisions" -H "Authorization: Bearer $TOKEN"
curl -s "https://fabric.wexa.ai/v1/audit/verify" -H "Authorization: Bearer $TOKEN"
```

Policy decisions are recorded and retrieved separately. Every evaluation appears, including the ones that matched no rule, such as `"rule": "default", "verdict": "allow"`. A call no rule addressed is allowed and recorded as allowed by default. The decision log is filtered by organization only, not project, and is not durable either.

**Limits and behavior, and what an auditor can have**

## Limits and behavior

The audit trail is an append-only slice in the gateway's memory. It is not written to a database, a file or object storage. A comment in the source describes production flushing the chain to write-once storage, and **no code does this.** Run against one gateway, in order:

```
# before
GET /v1/audit          -> 144 events
GET /v1/audit/verify   -> {"broken_at":"","chain_intact":true}

# gateway process stopped and started again, same binary, same port

# after
GET /v1/audit          -> {"events":[]}
GET /v1/audit/verify   -> {"broken_at":"","chain_intact":true}
```

A restart, crash, deployment or pod rescheduling destroys the trail silently. And an empty chain verifies as intact, so `chain_intact: true` never means the evidence is present. It answers whether what is here has been altered.

Approval events are also written to the platform audit trail, which is stored and survives a gateway restart. For every held call it records the approval being requested, decided, consumed and expired. A REST decision carries a flag for self-approval, and requests and decisions for every other kind of approval are recorded there too.

| You can give an auditor | You cannot give one, today |
|---|---|
| An ordered, tamper-evident record of consequential events since the last gateway restart: every tool call, every refusal with stage and reason, every credential issue and revoke. | Gateway events from before the last restart. There is no retention period because there is no retention. |
| Every approval's request and decision, with both parties, plus use and expiry for a held call, from the platform audit trail. | A cryptographic assurance of an event's project, organization or actor role. |
| A verification result, and the `lifecycle_id` joining one call's events. | A filtered or date-ranged extract, argument values, or an assurance of completeness. Nothing detects a gap. |

To make this workable, build the retention yourself:

1. Poll `GET /v1/audit` on a schedule and archive each response with a timestamp, in storage your retention policy covers.
2. Call `GET /v1/audit/verify` at each capture and archive the verdict beside the events. Run weeks later against an archive it proves nothing about the gateway.
3. Record the gateway's start time with each archive. An archive whose first event is `aud_000001` began at a restart, and the gap before it cannot be evidenced.

Related: [policy and approvals](/docs/governance/policy-and-approvals) covers what decides a call at `S5` and how a held call is decided, [observability](/docs/governance/observability) shows the same stages as a trace, and [quota and credits](/docs/administration/usage-quota-and-billing) covers what `S3` enforces and what a caller sees when it refuses.
