On this page

Using the Python SDK

The Python client has one calling convention: tool arguments are keyword arguments. That is the single biggest difference from the TypeScript port, where the payload is an object and the control options are a separate argument.

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, which is what lets the published 0.1.2 reach the tools it has no named method for.

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

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

retry is off by default because the gateway has no idempotency key — a retried run_agent starts a second real run. 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.