On this page

Governance

Policy and approvals

Every call passes a policy decision before it does anything: allow, deny, or hold for an approval from a person. Only a project admin can change the write gate and approval gates. Anyone with the review capability can work the Inbox.

  1. To hold consequential writes made with an API key or SDK session, open Settings → Projects and switch on Hold consequential writes from API keys and SDK sessions for approval.
  2. To decide a held call, open Actions → Approvals (/actions/approval-flows) and stay on the Inbox tab.
  3. Read the entry: its summary, why it was raised, who requested it, its risk and its deadline.
  4. Choose Approve (a reason is optional) or Reject (a reason is required, and the requester sees it).
  5. To decide several low-risk entries at once, select them and choose Approve all or Reject.

Limits

LimitValue
Write gateOff by default. A change can take up to 30 seconds to apply.
Deadline, held tool call48 hours, then overdue but still decidable.
Expiry, held tool call96 hours, then expired: a decision returns 409.
Resume tokenSingle use, for the same tool, project and user.
Approval service unreachableThe call fails at S6:approval with 503 and nothing is held.
How a call is decided

How it works

  1. The call arrives and passes the credential, scope, quota and argument checks.
  2. At stage S5:policy the engine runs an ordered chain of rules. The first rule to return a verdict wins: allow, deny or allow+gate.
  3. A denied call stops and never executes. A gated call parks and waits for a person.
  4. A person approves or rejects it in the console or over the API.
  5. The original caller re-sends the call with its resume_token, and the approved call runs.
  6. Each person who may decide it gets a console notification, and the requester gets a notification with the decision and the reason.

The policy decision sits below the Wexa MCP server, the REST API and both SDKs, so there is no fast path around it. A call refused on one surface is refused on the others for the same reason. Only the shape of the refusal differs. The stage is described in lifecycle and audit.

Every evaluation is recorded, allow included, with the rule that fired, its reasoning, the subject, the tool, the project and the lifecycle_id. Read them at GET /v1/policy-decisions, filtered to your organization.

A call is refused only when a rule refuses it. If no rule returns a verdict, the call is allowed and recorded against the rule name default. A tool does not have to be on a permitted list. A decision reading default and allow shows that no rule spoke, not that a rule permitted the call.

Anyone calling Wexa meets this behavior. Project admins decide approvals, and a project member decides one only when the rules in who may decide allow it. You need a project-scoped credential to list and decide approvals over the API.

See which rules apply

These rules run in this order. The last appears only when the deployment synchronizes an external data catalog.

RuleWhat it does
Required grantDenies a call whose token lacks the grant the tool demands. The same check runs earlier, when scope is resolved, so this rule is defence in depth: the tool still cannot run if the earlier check is loosened.
Restricted labelsDenies a call touching a graph label the operator classified as restricted. The set is deployment configuration and ships with PaymentInstrument and Salary.
Consequential writes by API keys and SDK sessionsGates every consequential tool except query-context when called with an API key or SDK session, admin keys included. Off by default in every project.
Ontology commits in Advanced projectsGates a commit by a member who is not an administrator. In Simple mode the commit flows through, still evaluated and audited. An administrator's commit in Advanced mode is allowed, and recorded as its own decision.
Connector actionsGates a run-connector-action call when the platform does not mark that action safe to run without approval.
Classified catalog dataGates a call referencing a column the catalog classed as personal or sensitive. An administrator's call is allowed and audited. In a Simple-mode project it is gated whatever the role.

The write-gate rule runs before the ontology rule, so an ontology commit made with an API key or SDK session is held while the gate is on, in either mode.

Calls from a person signed in to the console are not held by the write gate.

These tools demand a grant:

ToolGrant
query-context, search-code, fetch-code, connector-readfabric:query.read
create-ontologyfabric:ontology.write
docsfabric:docs.read
run-agentfabric:agent.run

Handle a policy denial

A denial names the stage that refused and why, and carries the lifecycle_id so you can retrieve the record.

A denial comes back as an ordinary tool result with isError set, so the model can read it and choose differently:

{
  "content": [
    {
      "type": "text",
      "text": "error (S5:policy): policy denied: label \"Salary\" is classified Restricted"
    }
  ],
  "isError": true
}

Both SDKs pick the error class from the stage name before falling back to the HTTP status, so a policy refusal is always PolicyDenied.

Where approvals come from and how to turn the write gate on

See where approvals come from

A tool call is held when one of these applies:

  • A consequential write made with an API key or SDK session in a project that has turned its write gate on. Admin keys are included.
  • A non-administrator committing a change to the project ontology in an Advanced-mode project.
  • A High-risk connector action, or one an approval gate matches. By default only actions that move money, change access (permissions, roles, ownership, credentials), destroy at scale (a whole repository, database, table, project or account, or a forced or permanent delete) or act in bulk are High. Other reads, writes, updates and deletes run without approval. See run-connector-action.
  • A call touching data the catalog classed as sensitive, where catalog synchronization is on.

The rest of the platform raises approvals into the same Inbox: flow previews, anomalies (continue, restart or terminate the run), analytics gates, governance gates, permission requests, capability activations, marketplace submissions, policy revisions, proactive cards, app requests made with request-approval, promoted agent actions, connector access and ontology proposals. Deciding resumes or stops the paused work. Every approval, whatever raised it, goes through one approval module into one Inbox.

Turn the write gate on

With the gate on, a consequential tool call made with an API key or SDK session waits for approval. That is every consequential tool except query-context.

It includes table row inserts and updates, table create and rename, trigger changes and connector actions, plus tools that were already consequential, such as delete-table-rows and save-context. Calls from a person signed in to the console are not held, and neither are calls made with any other kind of sign-in.

Turn it on in the console

Open Settings → Projects and switch on Hold consequential writes from API keys and SDK sessions for approval. It is off by default. A change can take up to 30 seconds to apply.

Turn it on over the API

curl -s -X POST https://fabric.wexa.ai/v1/set-write-gate \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"enabled": true}'

While the gate is on, this change made from an SDK is itself held for approval, so an SDK session cannot switch off the gate that holds it. whoami returns write_gate and write_gate_source (setting, default or unreadable, in which case writes are held).

When the write gate is on and API keys run unattended jobs, keep the Inbox staffed. A project switched back to Simple stops holding non-administrator ontology commits, but the write gate still holds commits made with an API key or SDK session. See project mode administration.

Define an approval gate

A gate matches connector actions by action_pattern (a glob over system.action, such as linear.delete_*) or by action_def_id. Its rule is one of:

  • always: every matched call needs approval, whatever the action's risk.
  • expr: a matched call needs approval when condition holds.
  • auto_approve: low-risk calls are approved without a person.
  • never: the matched actions are exempt from their default approval. A gate that requires approval still wins over an exemption.

always, expr and auto_approve need an approver ({ type: "user" | "group" | "role", id }). never does not.

TypeScriptPythonWhat it does
getApprovalSettings()get_approval_settings()The write gate and the approval gates.
setWriteGate({ enabled })set_write_gate(enabled=...)Turns the write gate on or off. Admins only.
saveApprovalGate({ rule, action_pattern, ... })save_approval_gate(...)Creates a gate, or replaces one with gate_id. Admins only.
deleteApprovalGate({ gate_id })delete_approval_gate(gate_id=...)Deletes a gate. Admins only.
listActionRisks({ system, risk, gated_only })list_action_risks(...)Connector actions with their risk and whether a call needs approval.

The Gates tab of the Inbox lists a project's gates with the approvals each is holding. Approve and Reject on a gate decide its oldest pending approval.

Catch a held call and work the Inbox

Catch a held call

When a rule returns allow+gate the call neither fails nor proceeds. The gateway asks the tool to describe itself for a human reader, then records an approval.

The approval holds the tool name, that description, your full scope, the arguments as submitted, the lifecycle_id and where the call came from (client, version and credential or MCP connection). The lifecycle record is marked pending and an audit event is appended.

The approval id is ar_ followed by 24 hexadecimal characters. The resume token is hrt_, the approval id, a dot, and a 64-character secret. It is single use, only the caller receives it, and it is not part of the approval as an approver sees it.

Not an error. The same object arrives as the text of a successful tool result with isError false:

{
  "content": [
    {
      "type": "text",
      "text": "{\"status\":\"pending_approval\",\"approval\":{\"approval_id\":\"ar_6abb9f3c0b5ae73d6cdd6d41\",\"resume_token\":\"hrt_ar_6abb9f3c0b5ae73d6cdd6d41.3f9c2e7a51b04d8e96a1c7f0d2b5e8a4c6f1093e7d2a5b8c4e0f6a1d9b3c7e25\",\"lifecycle_id\":\"qlc_000412\",\"message\":\"approval required: ...\"}}"
    }
  ],
  "isError": false
}

In the Inbox and in GET /v1/approvals the held call looks like this, shortened:

{
  "id": "ar_6abb9f3c0b5ae73d6cdd6d41",
  "tool": "insert-table-rows",
  "why": "consequential write by an SDK caller requires approval",
  "args": { "table_id": "tbl_orders", "rows": [ { "col_status": "shipped" } ] },
  "status": "pending",
  "expires_at": "2026-10-01T10:15:00Z",
  "lifecycle_id": "qlc_000412"
}

expires_at is the deadline.

Approvals need the platform's approval service. If it cannot be reached, a call that would be held fails at S6:approval with 503, the message ends with nothing was held and the call did not run, and nothing waits in the Inbox. Make the call again later.

Work the Inbox

Open Actions → Approvals (/actions/approval-flows). The Inbox tab lists every approval for the selected project, of every type, with your organization's organization-level approvals. The tab's count is the number of open approvals. Filter by status, risk, requester, date range and type, and sort by priority, newest or risk.

Each entry shows:

  • A one-line summary, its type, and an Org tag for an organization-level approval.
  • Why it was raised, and who requested it (the user, and the agent when an agent acted for them).
  • How long it has waited and the time left, or how long it is overdue.
  • The client and connector it came from, its risk (Low, Medium or High), and who it is with and who delegated it.

While a decision is applied, the entry shows Deciding… briefly. A request from the console that names an approver, with a field such as user_id, approver or decided_by, is refused with 400:

400 {"error":"user_id is a server-bound argument: the approver is taken from your signed-in scope and may not be supplied"}

Decide an approval in the console

Full context on an entry shows the status, deadline and expiry time, the gate, the source, the payload and inputs, and, once decided, who decided it and the reason they gave. For a held tool call, read the complete arguments that will run in args on GET /v1/approvals/{id} before you approve.

Other ways to decide:

  • Bulk. Select low-risk entries or use Select all low-risk, then Approve all or Reject. Rejecting in bulk needs one reason for all of them. Medium and High risk approvals are decided one at a time.
  • Anomalies. Continue and Restart from agent approve with that choice. Terminate rejects the run and needs a reason.
  • Delegate. Hand the approval to a user or a role such as org_admin, with an optional note. You need the same authority to delegate as to decide.

A member without the review capability sees a note that their approvals go to their administrators.

If you are unsure of a call's arguments, reject it. Rejecting costs the requester one new request.

Decide an approval over the API

List what is waiting, then decide it. The approver is taken from your credential.

curl -s "https://fabric.wexa.ai/v1/approvals?status=pending" \
  -H "Authorization: Bearer $TOKEN"

curl -s -X POST "https://fabric.wexa.ai/v1/approvals/ar_6abb9f3c0b5ae73d6cdd6d41/approve" \
  -H "Authorization: Bearer $TOKEN" -d '{"reason":"checked the schema"}'

Re-send the original call with resume_token and nothing else.

Request and response fields are in the governance API reference.

Who may decide, and resuming the call

Who may decide

  • The approver is you. It comes from your signed-in scope, never from the request body. The REST endpoints read only reason (or comment). A body naming a decider is refused with 400.
  • Your own request. You may approve it only in a project you administer, as its project admin or an organization admin. An API key acts for the admin who created it, so that admin may approve the key's held calls in a project they administer. Everyone else is refused with you cannot approve your own request.
  • Someone else's request. You must be above the requester in your organization's hierarchy. If the gate names a role, your role must meet it. If it names a person, only that person may decide.
  • Organization-level approvals, such as marketplace submissions and policy revisions, need an organization admin, and nobody may decide their own.
  • Out of scope. An approval in another organization, or another project for a project-scoped credential, returns 404.
  • A caller who may not decide gets 403, and the approval stays pending.

Resume the call

Re-invoke the same tool with resume_token and nothing else. Redemption is checked before the call is re-evaluated.

  • Single use. A second attempt fails.
  • Bound to the original. The token redeems only for the same tool, project and user. A mismatch returns the same failure as an unknown token.
  • The approved arguments win. They replace whatever you send, so what runs is what the approver read and nothing can change between approval and execution.

The redeemed call continues through simulation and execution, and the redemption is audited naming the approval and its decider.

With retry enabled, both SDKs catch the pending outcome and send only the resume token on the next attempt. When asked to wait, they poll until the first final status. approved resumes. rejected, expired, overdue and consumed end the wait with ApprovalError.

An overdue approval can still be approved in the console, so keep the resume token if you want to resume later.

If the approval was rejected, expired, already used or the token is wrong, redemption fails at S6:approval with 403 and the same answer for a different user, tool or project. The call never executed:

{"error":"S6:approval","error_description":"resume rejected: approval not approved (not approved, already used, or not yours)"}

Deadlines and statuses

RiskDeadlineExpires
High24 hours48 hours
Medium48 hours96 hours
Low72 hours144 hours

A held tool call is Medium risk. A gate can set its own review time, which replaces the default.

Past its deadline an approval is overdue and can still be decided. If the gate names someone to escalate to, it moves to them after the time the gate sets. At twice the deadline it is expired, a decision returns 409, and a held call must be made again. A requester whose approval expired has to ask again.

StatusMeaning
pendingWaiting for a decision.
overduePast its deadline and still waiting. It can still be decided.
approvedApproved. The paused work resumes.
rejectedRejected. The paused work stops.
expiredPast twice its deadline. It can no longer be decided.

An approved held call carries consumed: true once its resume token is used, which the SDKs report as the status consumed.

A decision happens once, so deciding an approval that is already decided or expired returns 409 with the reason, for example approval is already approved.

If the paused work cannot apply your decision, it is not recorded and the approval stays open so you can try again.

What the trail records for a held call

Read what the trail records

Every stage of a held call is recorded. The gateway's own trail, at GET /v1/audit, has these events for an insert-table-rows call:

ActionActorKind
insert-table-rows.approval.requestedthe callerApprovals
approval.approved or approval.rejectedthe approver, when they decide over RESTApprovals
insert-table-rows.approval.redeemedthe callerApprovals
insert-table-rows.executedthe callerTool calls

The approval's own events (requested, decided, consumed and expired) go to the platform audit trail, under Governance → Audit & Lineage, whether the decision was made in the Inbox or over REST. A REST decision is flagged when it was self-approved. How long each trail lasts is in lifecycle and audit.

You cannot build an alert on denials from the gateway's metrics: gateway.policy.denials.total exists in the gateway's metrics code and nothing writes to it. Use the Inbox or GET /v1/approvals?status=pending. See observability.

Related: lifecycle and audit follows a call by its lifecycle_id, request-approval raises an approval from your own app, and project mode explains why two of the rules behave differently in Simple and Advanced.