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.
| Method | Route | Returns |
|---|---|---|
whoami() | GET /v1/whoami | Your user id, role, organization, department, project and grants. |
approvals(status=None) | GET /v1/approvals | Pending or decided approvals, as a list. |
approve(approval_id) | POST /v1/approvals/{id}/approve | The decision result. |
reject(approval_id, reason) | POST /v1/approvals/{id}/reject | The 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.