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

# Lifecycle and audit

Every tool call is run by one engine, whichever surface made it, and that engine records what it did
at each step. The record is the **lifecycle**. What happened, who did it and under what scope also
goes to the **audit** record, which is hash-chained so that tampering with it is detectable.

One identifier ties the two together and ties both back to the caller.

## `lifecycle_id` is the thread

The gateway mints a `lifecycle_id` the moment a call arrives and before anything is decided about it.
That identifier then appears in every place the call leaves a mark:

- in the successful result, which every tool returns as `{ "lifecycle_id": ..., "result": ... }`
- in the error body of a refused call, and in `lifecycleId` / `lifecycle_id` on the SDK error object
- in the pending payload when the call parks at an [approval](/docs/concepts/policy-and-approvals)
- on the policy decision written for the call
- on every audit event the call produces
- on the lifecycle record itself, and on its trace

Keep it. It is the one value that turns "an agent did something odd on Tuesday" into a specific
retrievable record.

## The ten stages

A governed call passes through ten stages in a fixed order. Each is recorded with a status — `ok`,
`failed`, `pending` or `skipped` — and a short detail line.

| Stage | What it does |
|---|---|
| `S1:authenticate` | The credential was verified by the transport. Recorded here so the trail is complete. |
| `S2:resolve-scope` | The organization, department, project and role are pinned, and the tool's required grant is checked. |
| `S3:rate-quota` | The [quota](/docs/concepts/quota-and-credits) buckets for the project and the organization are consumed. |
| `S4:validate-input` | The tool checks its own 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; an ungated call skips it. |
| `S7:dry-run` | The tool simulates the call and returns a human-readable summary of 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. |

The order is what makes the guarantees legible. Scope is pinned before quota is consumed; quota is
consumed before the tool has looked at the arguments; policy runs after validation, so it can see the
labels; execution happens after the gate, never before it; and redaction happens after execution,
never instead of it.

A resumed call redeems its token between `S3` and `S4` — after quota, before validation. That single
detail is why quota exhaustion is the only failure a resume can be safely retried through, and it is
spelled out on the [policy and approvals](/docs/concepts/policy-and-approvals) page.

Stages that fail stop the call. The stage name becomes the `error` field the caller sees, which is
how both SDKs choose a typed error: `S3:rate-quota` becomes `QuotaExceeded`, `S5:policy` becomes
`PolicyDenied`, `S6:approval` becomes `ApprovalError`.

## Retrieving one

```bash
curl -s "$FABRIC/v1/lifecycles/qlc_000412" \
  -H "Authorization: Bearer $TOKEN"
```

The record comes back with the tool, the resolved scope, the surface the call arrived on, the project
mode it ran under, an overall status of `completed`, `denied`, `pending` or `failed`, the start and
end times, and the ordered list of stages with each one's status, detail and timestamp.

A record belonging to another organization returns `404`, not an empty record.

## What the audit record holds

The audit record is append-only. Each event carries:

- **who** — the acting user and their role
- **what** — an action name such as `query-context.executed`, `create-ontology.policy.denied` or
  `approval.approved`, and the target it applied to
- **where from** — the surface (`mcp`, `rest` or `system`), and, folded into the payload, the
  originating client and its version and the credential or MCP connection the call arrived on
- **under what scope** — the organization and project
- **which call** — the `lifecycle_id`, and the trace identifier as a cross-reference
- **a classification** — one of `Auth`, `Tool calls`, `Policy`, `Approvals` or `System`
- **a payload**, and the hashes described below

Consequential tools get the full payload. A call to a tool that only reads prose, such as
`docs`, is marked lightweight. When a call touched the context graph, the audit
payload also carries the list of nodes and relationships involved — their labels, identity keys and
the reference value of those keys, with the operation and the per-node verdict. Identity only: the
node's own property values are never written to the audit record.

Governance events are audited alongside tool calls. A denial writes an event of its own; requesting an
approval and redeeming the resume token each write theirs, and so do approving and rejecting over the
REST API. A decision made in the console's Approvals Inbox is recorded in the platform audit trail,
under **Governance → Audit & Lineage**, together with every approval's request, use and expiry.

## Retrieving it

```bash
curl -s "$FABRIC/v1/audit" \
  -H "Authorization: Bearer $TOKEN"
```

The stream is filtered to what the caller may see. Events belonging to another organization are never
returned. A project-scoped credential sees only that project's events, so one project's view can never
show a sibling project's activity; an organization-scoped credential keeps the organization-wide view.
Events with no organization or project — system-level ones — stay visible to everyone in the
organization.

## What verifying the chain actually checks

Each event is hashed, and each hash covers the hash of the event before it. Nine things go into that
hash: the event's identifier, its timestamp, the actor, the action, the target, the classification,
the `lifecycle_id`, the hash of the payload, and the hash of the previous event. That is what makes
tampering evident — change any one of those and the event's own hash no longer matches, and every
later hash in the chain is built on a value that has moved.

```bash
curl -s "$FABRIC/v1/audit/verify" \
  -H "Authorization: Bearer $TOKEN"
```

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

Verification walks the chain from the beginning and performs exactly three checks on every event, in
this order:

1. **The payload hash still matches the payload.** The payload is re-serialized and re-hashed and
   compared with the stored value.
2. **The link is intact.** The event's recorded previous-hash must equal the hash of the event before
   it.
3. **The event hash still matches the event.** The hash is recomputed over the event's identifier,
   timestamp, actor, action, target, classification, `lifecycle_id`, payload hash and previous hash,
   and compared with the stored value.

The first event that fails any of these stops the walk, and its identifier is returned as
`broken_at`. `chain_intact` is true only when every event passed all three.

Three properties of this are worth knowing precisely:

- **Five recorded fields are outside the hash.** The role, the surface, the organization, the project
  and the trace identifier are stored on the event and are *not* covered by it, so verification does
  not attest them. The trace identifier is out by design — it is a cross-reference annotation, and
  keeping it out means events written before the field existed still verify and that adding it can
  never break tamper-evidence. The other four are simply not part of the hashed set. If your
  attestation needs to cover which project an event belongs to, that is not something this chain
  proves today.
- **The payload is covered, through its own hash.** That includes the provenance the gateway folds
  into every event — the originating client, its version and the connection the call arrived on — so
  those genuinely are tamper-evident even though they are not named in the list above.
- **Verification is not scope-filtered.** `GET /v1/audit` shows a caller their own slice;
  `GET /v1/audit/verify` attests the whole chain the gateway holds. That is deliberate — a chain can
  only be verified as a chain — but it means the answer is about the record's integrity, not about
  the caller's events specifically.

## Using the pair to answer a question

An auditor's question is usually of the form "did this happen, who allowed it, and can you show me it
has not been edited". The three calls that answer it are:

```bash
# 1. What happened to that call, stage by stage.
curl -s "$FABRIC/v1/lifecycles/qlc_000412" -H "Authorization: Bearer $TOKEN"

# 2. Which rule decided it, and what it reasoned.
curl -s "$FABRIC/v1/policy-decisions" -H "Authorization: Bearer $TOKEN"

# 3. Whether the record it is written into is still intact.
curl -s "$FABRIC/v1/audit/verify" -H "Authorization: Bearer $TOKEN"
```

Every one of those is joined by the `lifecycle_id` the caller was handed at the start.

## Where to look next

**Governance — Policy decisions and approvals**
What decides a call at `S5`, and the full path from a gated call to a person's decision.

**Governance — Observability**
The same ten stages as a trace, and how to use one to find where the time went.

**Governance — Quota and credits**
What `S3` is enforcing, and what a caller sees when it refuses.

**Foundations — Organizations, departments and projects**
The boundaries the audit stream is filtered against.
