On this page

SDKs and APIs

Using the Python SDK

wexa is the Python client for the Wexa gateway. It has no dependencies and one calling convention: tool arguments are keyword arguments. You need a workspace URL and a credential the gateway accepts, such as an API key.

  1. Install the package.

    pip install wexa
  2. Set your workspace URL and credential, or pass them to the constructor. Get an API key as the Authentication page shows, or follow the Quickstart.

    export WEXA_WORKSPACE=https://fabric.wexa.ai    # your workspace URL
    export WEXA_API_KEY=fab_sk_…                    # any bearer credential the gateway accepts
  3. Construct one client. It discovers the API base in its constructor, so build it once and reuse it.

    from wexa import Fabric
    
    fabric = Fabric()                                   # reads the two environment variables
    # or
    fabric = Fabric(workspace="https://fabric.wexa.ai", api_key=my_key)
  4. Verify the client. A reply means the workspace URL resolves and the credential is accepted.

    print(fabric.whoami())   # your user, role, project and grants
  5. Call a tool as a method named after it, with underscores, or by name with call().

    rows = fabric.query_context(
        query="MATCH (c:Customer) WHERE c.project_id = $project_id RETURN c.name AS name LIMIT 5"
    )
    print(rows.lifecycle_id, rows.trace_id)
  6. Catch WexaError and OSError for failures.

Limits

LimitValue
Python version3.9 or later.
timeout70 seconds. Governed tool routes cap near 60s upstream, so the server's own deadline fires first.
retries3 attempts in total when a call passes retry=True. Off by default.
Retried errorsUpstreamError (so TimeoutError_) and QuotaExceeded only.
Server-bound argumentsproject_id, projectID, organization_id and executed_by are refused before the request is built.
Failures to catchWexaError and OSError: a refused connection is not a WexaError.
Install, configure and construct

Install

Install the latest release (see the changelog for what each one changed). Confirm what you actually got:

import wexa
print(wexa.__version__)   # prints the installed version

Configure

WEXA_WORKSPACE is the workspace URL, not the API base. The client discovers the API base from GET /v1/connection-info itself. Trailing slashes are stripped for you.

Construct the client

The constructor takes workspace and api_key, and reads WEXA_WORKSPACE and WEXA_API_KEY when you pass none. Trailing slashes are stripped for you.

Constructor options

OptionDefaultWhat it does
workspaceWEXA_WORKSPACEWorkspace URL.
api_keyWEXA_API_KEYBearer credential.
timeout70Seconds. Governed tool routes cap near 60s upstream, so 70 lets the server's own deadline fire first.
retries3Attempts when a call passes retry=True. Total, not extra.

Discovery happens in the constructor

Unlike the TypeScript client, the Python client performs the /v1/connection-info round trip inside __init__, so fabric.base is populated the moment the object exists:

fabric = Fabric()
print(fabric.base)      # https://fabric.wexa.ai/v1
print(fabric.issuer)    # https://fabric.wexa.ai

That means constructing a client is a network operation. Construct it once and reuse it; do not build one per request.

Verify the install

from wexa import Fabric

fabric = Fabric()
print(fabric.whoami())

It prints, for example:

{'dept_id': 'dept_c10', 'org_id': 'org_c10', 'project_id': 'proj_c10',
 'role': 'OWNER', 'user_id': 'u_c10',
 'grants': ['fabric:query.read', 'fabric:docs.read', 'fabric:ontology.write',
            'fabric:orchestrate.read', 'fabric:orchestrate.write', 'fabric:agent.run',
            'fabric:skill.write', 'fabric:model.write']}

There is nothing to release. urlopen closes per request, so the client has no close() and needs none.

Call a tool

Calling a tool

Every tool has a method named after it, with underscores:

rows = fabric.query_context(
    query="MATCH (c:Customer) WHERE c.project_id = $project_id RETURN c.name AS name LIMIT 5"
)

The result is a dict subclass carrying the correlation ids as attributes, so the envelope does not get in your way:

print(rows)                 # {'columns': None, 'rows': None, 'row_count': 0, 'stats': {...}, 'truncated': False}
print(rows.lifecycle_id)    # qlc_000016
print(rows.trace_id)        # b9d8c07461dc4ae68539dae92e497a1e

$project_id is bound for you from the credential. Never pass it yourself — see below.

Or call by name

call() takes the tool name and accepts either spelling. These are the same call:

fabric.call("run_process_flow", goal="reconcile")
fabric.call("run-process-flow", goal="reconcile")

An unrecognised name is sent to the wire verbatim rather than rejected, so an older published version still reaches a tool it has no named method for, provided the name has hyphens: fabric.call("connect-whatsapp", ...). The underscore form ("connect_whatsapp") reaches /v1/connect_whatsapp, which does not exist, and the gateway answers 404.

Upgrading (pip install -U wexa) gives you the named method. A 404 on a hyphenated name means the tool is not registered in that deployment, not that the client failed to route it.

fabric.call("delete-context", label="Customer", key="id", value="c-1")

The same call on every surface

curl -s -X POST "$BASE/v1/query-context" \
  -H "Authorization: Bearer $WEXA_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"query":"MATCH (c:Customer) WHERE c.project_id = $project_id RETURN c.name AS name LIMIT 5"}'

All three produced the identical envelope against a live gateway.

A method only succeeds if the gateway registered that tool. Calling an unregistered one raises NotFound with status 404. Read the live list from GET /v1/connection-info.

Server-bound arguments

Four argument names are never yours to send: project_id, projectID, organization_id and executed_by. The platform fills them in from your credential's scope.

The SDK refuses them client-side, before the request is built:

fabric.query_context(query="RETURN 1", project_id="proj_other")
ValidationError: ['project_id'] are bound server-side from your token scope; sending them is rejected

The refusal exists because the gateway does not refuse it for you. Sending a body project_id of another project alongside a valid query-context request returned HTTP 200 and a normal envelope when it was tried against a live gateway: the handler reads only query and parameters, and binds $project_id from your credential's scope.

Your value was neither honoured nor rejected — it was ignored, and the call succeeded against the scope you were always pinned to. A silently ignored argument is worse than a refusal precisely because nothing tells you it happened.

The TypeScript SDK applies the same guard over the same four names. See server-bound arguments for the concept.

The non-tool methods

Five methods are not tools and are spelled out explicitly, because they map onto conventional REST routes rather than the POST /v1/<tool> convention.

MethodRouteReturns
whoami()GET /v1/whoamiYour user id, role, organization, department, project and grants.
approvals(status=None)GET /v1/approvalsPending or decided approvals, as a list.
approve(approval_id)POST /v1/approvals/{id}/approveThe decision result.
reject(approval_id, reason)POST /v1/approvals/{id}/rejectThe decision result. reason is required; without one the call raises ValidationError before any request.
lifecycle(lifecycle_id)GET /v1/lifecycles/{id}The stage-by-stage record of one governed call.
pending = fabric.approvals("pending")
for a in pending:
    print(a["id"], a["tool"], a["what"], a["why"], a["expires_at"])
fabric.approve(pending[0]["id"])

record = fabric.lifecycle(rows.lifecycle_id)
Retry, approvals and failures

Retry

rows = fabric.query_context(query=q, retry=True)     # safe: it is a read

retry is off by default because without an idempotency key a retried run_agent starts a second real run; pass idempotency_key and retries default on. retries=3 on the constructor means three attempts in total.

Only UpstreamError — and therefore TimeoutError_ — and QuotaExceeded retry:

RETRYABLE = (UpstreamError, QuotaExceeded)
RESUME_RETRYABLE = (QuotaExceeded,)

ConfigurationError does not retry despite carrying a 5xx status: it extends WexaError directly rather than UpstreamError, because a deployment fault is not load and retrying will never fix it. The full hierarchy, both predicates and the reasoning behind the resume boundary are on error handling — it is written against TypeScript, but the taxonomy and the retry policy are the same module ported.

Approvals

A call that gates raises ApprovalRequired, carrying a single-use resume_token.

Consequential writes wait by default

Breaking change. Every project now holds consequential writes made with an API key or an SDK session until a person approves them, admin API keys included. Table row inserts and updates (insert_table_rows, update_table_row), table create and rename (create_table, rename_table), trigger changes (set_triggers) and connector actions (run_connector_action) are consequential now, next to the tools that already were, such as delete_table_rows and save_context.

They raise ApprovalRequired unless the gate is off. A project admin turns the gate off, or back on, in the project's settings. Actions taken by a person in the console are not held.

from wexa import ApprovalRequired, WexaError

try:
    fabric.save_context(nodes=nodes)
except ApprovalRequired as pending:
    checkpoint(pending.approval_id, pending.resume_token)   # durable, before you wait

Later, possibly from another process:

try:
    fabric.call("save_context", resume_token=load_token(), retry=True)
except WexaError as e:
    if e.resume_spent:
        pass   # the approval is gone; request a fresh one rather than looping

The gateway redeems the token between S3 (quota) and S4 (validate), so a 429 leaves it intact and anything later has burned it. resume_spent tells the two apart, and it is why retry=True on a resume only ever helps a QuotaExceeded.

wait_for_approval=True blocks and polls instead, which is simpler and loses a granted approval if the process dies mid-wait:

fabric.save_context(nodes=nodes, wait_for_approval=True, approval_timeout=600, poll=5)

The wait ends on the first final status of the approval. approved resumes the call. rejected, expired, overdue and consumed raise ApprovalError with the status in the message (approval rejected) and the approval in raw. An overdue approval can still be approved in the console; to keep waiting for it, checkpoint the token and resume later.

Catching failures

from wexa import WexaError, PolicyDenied, ForbiddenError, QuotaExceeded

try:
    fabric.run_agent(agentflow_id="af_1", goal="summarize last week")
except PolicyDenied as e:
    ...                       # S5 — a rule refused it. Terminal.
except ForbiddenError as e:
    ...                       # S2 — mint a credential that holds the grant.
except QuotaExceeded as e:
    ...                       # S3 — back off.
except WexaError as e:
    print(e.stage, e.status, e.lifecycle_id, e.trace_id)
except OSError as e:
    ...                       # nothing reached the gateway

Order matters: PolicyDenied is a ForbiddenError, and ApprovalRequired is a WexaError.

The OSError arm is not optional. Python leaks urllib's URLError for a refused connection, and it is not a WexaError — verified live against an unreachable address. The TypeScript port wraps that case as TransportError precisely so one check covers everything.

Python and TypeScript, and what to read next

One SDK ported to the other

wexa and @wexa-fabric/sdk are deliberate ports of one another, not two independent libraries and not a stack. Neither wraps the other; both are thin HTTP clients for the same gateway. Python is the source of truth and TypeScript is the port, and they share one tool table, one set of server-bound arguments, one error taxonomy, one retry policy and one response envelope.

They are not byte-identical, and a handler moved between them needs four adjustments:

PythonTypeScript
Nothing reached the gatewayurllib's URLError, an OSError — not a WexaErrorTransportError, which is a WexaError
The 504 classTimeoutError_, underscored to dodge the builtinTimeoutError
Retry predicatesisinstance(e, RETRYABLE) / RESUME_RETRYABLEisRetryable(e) / isResumeRetryable(e)
DiscoveryIn __init__Deferred to the first call

Catch WexaError and OSError in Python. A lone except WexaError will not see a refused connection, and that is the failure most likely to happen in production.

What's next