> Source: https://wexa.ai/docs/sdks-and-apis/how-every-tool-call-works

# How every tool call works

Every Wexa tool is the same tool on four surfaces: the Wexa MCP server, the REST API, the TypeScript SDK and the Python SDK. Each tool page shows only what is specific to that tool. Everything else is here, once.

## Naming on each surface

A tool has one wire name, written in lowercase with hyphens: `run-agent`. Each surface spells it its own way.

| Surface | Name for `run-agent` | Rule |
| --- | --- | --- |
| Wexa MCP server | `run-agent` | The wire name. |
| REST API | `POST /v1/run-agent` | `POST https://fabric.wexa.ai/v1/` plus the wire name. |
| TypeScript SDK | `fabric.runAgent()` | The wire name in camel case. |
| Python SDK | `fabric.run_agent()` | The wire name with underscores. |

Argument names are the wire names on every surface; only method names change.

**More naming**

`create-ontology` is the one tool whose REST route differs: both SDKs post to `/v1/ontology`, and `/v1/create-ontology` answers too.

Both SDKs also have `fabric.call(name, ...)`, which takes either spelling, `run-agent` or `run_agent`.

## Return shape

Every tool returns the same data on every surface, wrapped differently on each. Code ported by hand between the two SDKs gets `undefined` rather than an error, because the id names differ.

| Surface | What you get |
| --- | --- |
| REST API | `{"lifecycle_id": "qlc_...", "result": { ... }}`. Read `result`. |
| TypeScript SDK | The result, already unwrapped: `const result = await fabric.runAgent({...})`. There is no `.result` to read: `result.result` is `undefined`. The ids are non-enumerable properties named `lifecycleId` and `traceId`, so `Object.keys()` and `JSON.stringify()` omit them. |
| Python SDK | The result, already unwrapped. The ids are attributes named `lifecycle_id` and `trace_id`. |
| Wexa MCP server | `{"content": [{"type": "text", "text": "..."}], "isError": false}`, where `text` is the REST body as a JSON string. Parse it. |

## Lifecycle and governance

Every call passes the same stages, in order, and each is recorded: authenticate, resolve scope, rate and quota, validate, policy, approval, dry run, execute, post-process, record. A call that fails a stage stops there. The `lifecycle_id` in the response is the handle to that call's record. Keep it, and quote it when asking why a call was allowed, refused or held.

A tool that changes something is consequential. It is audited in full, and a policy rule may hold it for a person to approve. A tool page shows a "Can be held for approval" badge under its title when the write gate can hold its tool. A held call returns status `202` on REST, and the SDKs raise `ApprovalRequired`, which carries the approval id. The Wexa MCP server returns a non-error result (`isError: false`) holding the pending-approval payload instead. When a project admin turns on the project's write gate, an API key or an SDK session also has its consequential writes held. The write gate never holds a read, but a policy rule may still hold or refuse any tool, and every call uses quota.

To wait for the decision and carry on, pass `wait_for_approval=True` in Python or `waitForApproval: true` in TypeScript. The wait is 600 seconds by default.

The platform fills in four arguments from your credential: `project_id`, `projectID`, `organization_id` and `executed_by`. They are server-bound arguments. Both SDKs refuse them before the request leaves your process. See [what scope a call carries](/docs/get-started/core-ideas#know-what-scope-a-call-carries).

Each tool page names the grant its tool needs. For the stages in full, see [lifecycle and audit](/docs/governance/audit-and-lifecycle).

## Errors

Both SDKs raise the same typed errors, all extending `WexaError`. They choose the class from the gateway's error string, then the HTTP status, so one refusal is the same class in both. In Python the timeout class is `TimeoutError_`. A parameter's rule sits in its row on a tool page; a refusal tied to no parameter sits in its collapsed Errors block.

| Class | Status | Raised when |
| --- | --- | --- |
| `AuthError` | 401 | Missing, malformed or revoked credential. |
| `ForbiddenError` | 403 | The credential lacks the tool's grant or the project's scope. |
| `PolicyDenied` | 403 | A policy rule refused the call. Extends `ForbiddenError`. |
| `ValidationError` | 400 / 422 | A value breaks its parameter's rule, or the body is malformed. |
| `ApprovalRequired` | 202 | A person must approve the call first. |
| `ApprovalError` | 403 | The resume token is bad, expired or spent. |
| `QuotaExceeded` | 429 | Over the request window or credit balance. |
| `NotFound` | 404 | An id or name matches nothing in your project. |
| `ConflictError` | 409 | The change collides with current state. |
| `UpstreamError` | 502 | A downstream service refused or was unreachable. |
| `TimeoutError` | 504 | An upstream deadline was exceeded. Extends `UpstreamError`. |
| `ConfigurationError` | 502 / 503 | The tool is not enabled in your deployment. |
| `TransportError` | none | No response arrived. TypeScript only: Python raises `OSError`, which is not a `WexaError`. |

These are the defaults for every tool, so a tool page does not restate them. It names any exception in a parameter's row or its Errors block:

- An id from another project is `NotFound` (404). It reads as an id that never existed.
- A value that breaks its parameter row's rule is `ValidationError` (422): missing, empty, the wrong type or format, out of range.
- A call that the write gate or a policy rule holds is not a failure. It raises `ApprovalRequired` (202 on REST). The "Can be held for approval" badge marks the tools the write gate can hold; a policy rule may hold any tool, for example a read of classified data. See [lifecycle and governance](#lifecycle-and-governance) and [policy and approvals](/docs/governance/policy-and-approvals).

On REST you get the status and an `error` and `error_description` pair. On the Wexa MCP server a refusal comes back as `isError: true`, with the stage and message in `content[0].text`. An unknown tool name is a JSON-RPC error instead.

Retries are off unless you pass `retry`. Only `UpstreamError` (so `TimeoutError` too) and `QuotaExceeded` are retried; `ConfigurationError` never is. The gateway's `Retry-After` is honoured. For the full set of classes and what to retry, see [errors and retries](/docs/get-started/errors-and-retries).

## Identifiers

Wexa issues opaque ids and checks their format before a tool runs, so an id in the wrong argument is refused. Read the id from the output field that names it and pass it unchanged. [Which identifier goes where](/docs/sdks-and-apis/identifiers) covers the two cases that need care.

## Find a tool

Every tool has a page in the sidebar, except the documentation tool `docs`. The `tools` array of `GET /v1/connection-info` lists the tools your deployment registers; see [discovery](/docs/sdks-and-apis/discovery).

**Sign-in calls are not governed tools**

`login`, `refresh-session` and `logout` sign a Wexa user in through an SDK. They run before any tool call, on the REST API and both SDKs only, and they do not pass the lifecycle above. See [login](/docs/sdks-and-apis/login), [refresh-session](/docs/sdks-and-apis/refresh-session) and [logout](/docs/sdks-and-apis/logout).

**See also**

[Python SDK](/docs/sdks-and-apis/python-sdk), [TypeScript SDK](/docs/sdks-and-apis/typescript-sdk), [REST API](/docs/sdks-and-apis/rest), [Wexa MCP server clients](/docs/sdks-and-apis/mcp-clients), [errors and retries](/docs/get-started/errors-and-retries) and [which identifier goes where](/docs/sdks-and-apis/identifiers).
