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.
- Take the
lifecycle_idfrom the result or error of the call. - In the console, open Governance → Audit & Lineage for the platform audit trail.
- Over the API, read one call with
GET /v1/lifecycles/{lifecycle_id}. - Read the audit stream with
GET /v1/audit, and keep each response, because the gateway's trail does not survive a restart. - 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
- The gateway mints a
lifecycle_idthe moment a call arrives, before anything is decided. - The call passes the stages below in a fixed order, and each is recorded.
- The call writes audit events, each hashed together with the hash of the one before it.
- 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_idon the SDK error object - the pending payload of an approval
- 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 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.
curl -s "https://fabric.wexa.ai/v1/lifecycles/qlc_000412" \
-H "Authorization: Bearer $TOKEN"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.
curl -s "https://fabric.wexa.ai/v1/audit" \
-H "Authorization: Bearer $TOKEN" \
> audit-$(date +%Y%m%dT%H%M%S).jsonGET /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:
{
"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.deniedorapproval.approved, and its target. - where from: the surface (
mcp,restorsystem) and, in the payload, the originating client, its version and the credential or MCP connection. - scope and call: the organization and project, the
lifecycle_idand the trace id. - kind: one of
Auth,Tool calls,Policy,ApprovalsorSystem, 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 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
curl -s "https://fabric.wexa.ai/v1/audit/verify" \
-H "Authorization: Bearer $TOKEN"{ "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_idandtrace_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 withoutchain_intactturning 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:
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:
- Poll
GET /v1/auditon a schedule and archive each response with a timestamp, in storage your retention policy covers. - Call
GET /v1/audit/verifyat each capture and archive the verdict beside the events. Run weeks later against an archive it proves nothing about the gateway. - Record the gateway's start time with each archive. An archive whose first event is
aud_000001began at a restart, and the gap before it cannot be evidenced.
Related: policy and approvals covers what decides a call at S5 and how a held call is decided, observability shows the same stages as a trace, and quota and credits covers what S3 enforces and what a caller sees when it refuses.