Metadata-Version: 2.4
Name: enigma-api-client
Version: 0.12.2
Summary: Python SDK for the Enigma GraphQL API: search business data with GraphQL queries or questions in plain English
Author-email: Enigma Technologies <info@enigma.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://enigma.com
Project-URL: GraphQL API, https://documentation.enigma.com/guides/graphql
Keywords: enigma,graphql,business data,company data,kyb,sdk
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Framework :: AsyncIO
Classifier: Typing :: Typed
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx==0.28.1
Requires-Dist: pyyaml==6.0.3
Requires-Dist: pydantic<3.0,>=2.0
Requires-Dist: graphql-core==3.2.12
Provides-Extra: anthropic
Requires-Dist: claude_agent_sdk==0.2.121; extra == "anthropic"
Provides-Extra: bedrock
Requires-Dist: botocore>=1.34.0; extra == "bedrock"
Provides-Extra: all
Requires-Dist: claude_agent_sdk==0.2.121; extra == "all"
Requires-Dist: botocore>=1.34.0; extra == "all"
Dynamic: license-file

# Enigma API Client

The Enigma Python SDK, [`enigma-api-client`](https://pypi.org/project/enigma-api-client/), sends GraphQL queries to the [Enigma GraphQL API](https://documentation.enigma.com/guides/graphql) and turns questions in plain English into those queries. This page is the SDK reference.

## Prerequisites

- An Enigma API key. Find yours in the [Enigma Console](https://console.enigma.com/settings/api-keys) under **Settings > Account > API keys**.
- Python 3.11 or later.
- To generate queries on your own machine with a local strategy (see Choose a Strategy): an Anthropic or OpenRouter API key, or AWS credentials with access to Claude on Amazon Bedrock.

The examples read your Enigma API key from the `ENIGMA_API_KEY` environment variable:

```bash
export ENIGMA_API_KEY="your-api-key"
```

## Install

```bash
pip install enigma-api-client
```

Optional extras add dependencies for specific features:

| Extra | Installs | Needed for |
|---|---|---|
| `enigma-api-client[bedrock]` | `botocore` | `BedrockProvider`, Claude on Amazon Bedrock as the large language model (LLM) of `TemplateStrategy` or `GenerativeStrategy` |
| `enigma-api-client[anthropic]` | `claude_agent_sdk` | `AgentStrategy` |
| `enigma-api-client[all]` | `botocore` and `claude_agent_sdk` | `BedrockProvider` and `AgentStrategy` |

`AnthropicProvider` and `OpenRouterProvider` need no extra. The `anthropic` extra installs the Claude Agent SDK, which only `AgentStrategy` uses.

## Quick Start

### Ask a Question

```python
import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(prompt="Tell me about Chipotle")
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        brand = response.data["search"][0]
        print(brand["names"]["edges"][0]["node"]["name"])  # CHIPOTLE MEXICAN GRILL
        print(brand["__url"])  # the brand's profile in the Enigma Console


asyncio.run(main())
```

The SDK turns the question into a GraphQL query, runs it, and returns the result. By default, Enigma's hosted query service writes the query, so your Enigma API key is all you need. For the kinds of questions you can ask, see Generate Queries from Questions.

### Send a GraphQL Query

```python
import asyncio
import os

from enigma_api_client import Client, ClientConfig

QUERY = """
query Brand($name: String!) {
  search(searchInput: { name: $name, entityType: BRAND, conditions: { limit: 1 } }) {
    ... on Brand {
      names(first: 1) { edges { node { name } } }
      websites(first: 1) { edges { node { website } } }
    }
  }
}
"""


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(query=QUERY, variables={"name": "Chipotle"})
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        brand = response.data["search"][0]
        print(brand["names"]["edges"][0]["node"]["name"])  # CHIPOTLE MEXICAN GRILL
        print(brand["websites"]["edges"][0]["node"]["website"])  # https://www.chipotle.com


asyncio.run(main())
```

A name can match many entities, so `conditions: { limit: 1 }` keeps only the first. The [GraphQL guide](https://documentation.enigma.com/guides/graphql) and the [schema reference](https://documentation.enigma.com/reference/graphql_api) describe the queries you can write, and Write GraphQL by Hand lists the mistakes to avoid.

Later snippets show only what changes: usually a new `config = ...` line, sometimes with the imports and strategy it needs. To run a snippet, start from one of the preceding programs and put the snippet's lines in place of the program's `config = ...` line. If the snippet has no `config = ...` line, put its lines just before the program's `config = ...` line instead, and pass any strategy it builds, such as `TemplateStrategy(...)`, to `ClientConfig` as `query_strategy=`.

## Configure the Client

`ClientConfig` takes these fields:

| Field | Default | Meaning |
|---|---|---|
| `enigma_api_key` | Required | Your Enigma API key |
| `query_strategy` | `ServiceStrategy` | The strategy that turns questions into queries (see Choose a Strategy) |
| `tier` | `AccessTier.REDISTRIBUTION` | Your key's access tier, which local strategies write queries for (see Access Tiers) |
| `url` | `https://api.enigma.com/graphql` | The GraphQL endpoint |
| `auto_inject_id_and_typename` | `True` | Adds `id` and `__typename` to the inline fragments and connection nodes a query selects (see The Shape of `data`) |
| `auto_inject_page_info` | `False` | Adds `pageInfo { hasNextPage endCursor }` to every connection a query selects that doesn't already select `pageInfo` (see Paginate) |
| `customize_headers_callback` | `None` | A function that takes the default request headers and returns headers to add or replace. A header returned with an empty value is removed. It applies to searches only (see the note after this table). |

The client doesn't apply `customize_headers_callback` to its request for the API schema (see Schema Checks and Several Tiers) or to the hosted query service.

### Client Lifecycle

- Create one `Client` and reuse it for all your requests. It keeps connections open until you close it, either with `async with Client(config) as client:` or with `await client.aclose()`. Closing the client also closes its strategy.
- All methods are asynchronous. In a Jupyter notebook, which already runs an event loop, replace `asyncio.run(main())` with `await main()`. Start Jupyter from a shell where `ENIGMA_API_KEY` is set.
- Use a client from one event loop. Don't share it across threads that each run their own loop.

### Run Searches Concurrently

One client can run many searches at once:

```python
import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        prompts = ["Tell me about Chipotle", "Tell me about Sweetgreen", "Tell me about Shake Shack"]
        responses = await asyncio.gather(*(client.search(prompt=p) for p in prompts), return_exceptions=True)
        for prompt, response in zip(prompts, responses):
            if isinstance(response, Exception):
                print(prompt, "failed:", type(response).__name__)
            else:
                print(prompt, response.status_code)


asyncio.run(main())
```

`return_exceptions=True` keeps one failed search from hiding the others' results.

Your plan's [rate limits](https://documentation.enigma.com/resources/rate-limits) cap requests per second, bursts, and requests per day. To cap how many calls run at once, use an `asyncio.Semaphore`. That keeps bursts down but doesn't enforce a rate.

### Timeouts

The client waits up to 60 seconds for the API, and `ServiceStrategy` waits up to 30 seconds for the hosted query service. A timeout raises `httpx.TimeoutException`.

## Read the Response

`client.search()` returns a `SearchResponse`:

| Field | Holds |
|---|---|
| `status_code` | The HTTP status of the API's answer |
| `data` | The GraphQL `data` object, or `None` |
| `errors` | The GraphQL `errors` list, or `None` |
| `extensions` | The GraphQL `extensions` object, or `None` |
| `unavailable_data` | For a question: the parts Enigma can't answer (see Unavailable Data) |
| `raw_body` | The response body as text |
| `headers` | The HTTP response headers, looked up case-insensitively, such as `Retry-After` |

`response.is_success()` is true when the API answers with a 2xx status and no errors, and neither of these applies:

- **HTTP 206.** Some values the API computes in the background, such as some counts, weren't ready, so the API left them null. The rest of `data` is filled in, so you can still use a 206 answer. Before you treat a response as failed, check whether `response.status_code` is 206 and read `response.errors`.
- **A failed background task.** The answer's `extensions` report a background task that failed, was canceled, or ran out of credits.

To have the API wait up to 25 seconds for background values before it answers, the client adds the `max_background_task_wait` and `max_call_duration` query parameters to the URL. If `ClientConfig.url` already sets either one to a non-blank value, the client sends the URL as written and adds neither.

### The Shape of `data`

- `search` returns a list of entities. `__typename` says what each one is: `Brand`, `LegalEntity`, `OperatingLocation`, `Person`, or `Address`.
  - A `Brand` is a business as its customers know it, such as Chipotle.
  - A `LegalEntity` is a company as a government registers it, such as Chipotle Mexican Grill, Inc.
  - An `OperatingLocation` is one place where a brand does business, such as a store.
  - [Understanding Enigma Data](https://documentation.enigma.com/getting_started/enigma_data) describes them in full.
- Related data comes as connections: `{"edges": [{"node": {...}}, ...]}`. Each connection returns one page (see Paginate).
- A question that names several businesses, people, or street addresses can get several searches, under the keys `search`, `search2`, and `search3` of `data` (see Generate Queries from Questions).
- A question that counts the businesses of a category, overall or in a place, as in "How many coffee shops are in Denver?", gets `aggregate`, an object of counts such as `locationsCount` and `brandsCount`.
- Where a count of a brand's locations appears depends on the strategy that writes the query (see Choose a Strategy):
  - From a template (`ServiceStrategy`'s default, or `TemplateStrategy`): as `locationsCount` on the brand in `search`, with one search per brand (`search`, `search2`, and so on).
  - From `GenerativeStrategy`, or `ServiceStrategy(query_strategy="generative")`: as `locationsCount` in `aggregate`, with one `aggregate` per brand (`aggregate`, `aggregate2`, and so on).
- `GenerativeStrategy`, `ServiceStrategy(query_strategy="generative")`, and `AgentStrategy` have an LLM write the whole query, including its keys. With these strategies, read every key of `data`.

The client adds fields to what you asked for:

- `id` and `__typename` on the inline fragments (`... on Brand { ... }`) and connection nodes the query selects. The client skips:
  - An inline fragment that carries a directive, such as `@include` or `@skip`.
  - A connection node when the node or its `edges` carries a directive.
  - An entity selected only through a named fragment at the top of `search`, as in `search(...) { ...BrandFields }`.
- `__url`:
  - On each brand, legal entity, and operating location that has an `id` and a `__typename`: a link to its profile in the Enigma Console.
  - On a person: a link to the brand the person appears under, or to Enigma's website when the person appears under none.

To get `__url` for a selection the client skips, select `id` and `__typename` in it yourself.

To send your query exactly as written, set `auto_inject_id_and_typename=False`. A query that selects a connection node with no `id`, such as the `MediaContent` nodes of `isMentionedInMediaContents`, fails unless you do.

With `auto_inject_id_and_typename=False`, a brand, legal entity, or operating location gets `__url` only when your query selects its `id` and `__typename`, and a person only when your query selects its `__typename`.

The GraphQL query in the quick start returns:

```json
{
  "search": [
    {
      "id": "825ce859-b742-49cb-9787-b9b32f674f25",
      "__typename": "Brand",
      "names": {"edges": [{"node": {"id": "000000d0-0003-cd02-0000-0a8000000000", "__typename": "BrandName", "name": "CHIPOTLE MEXICAN GRILL"}}]},
      "websites": {"edges": [{"node": {"id": "0000002a-0010-60fa-0000-0a8000000000", "__typename": "Website", "website": "https://www.chipotle.com"}}]},
      "__url": "https://console.enigma.com/profile/brand/825ce859-b742-49cb-9787-b9b32f674f25"
    }
  ]
}
```

## Generate Queries from Questions

A question passed as `client.search(prompt=...)` must name what it is about. Every strategy answers these kinds of questions:

| Kind of question | Example |
|---|---|
| A business's profile, by name or website | "What does Sweetgreen do?", "Enrich company data for hubspot.com" |
| A business's locations in a city or state | "Find Costco locations in Spokane" |
| One location of a business, by street address | "Is the Costco at 12020 N Newport Hwy, Spokane, WA still open?" |
| What is at a street address | "What is at 350 Fifth Ave, New York, NY?" |
| A person and the businesses tied to them | "What businesses is Jane Doe associated with?" |
| A company's officers, owners, and related companies | "Who runs Airbnb?", "Who owns Shake Shack?" |
| A legal entity's registrations, taxpayer identification numbers (TINs), and bankruptcies | "Is Zoom Video Communications Inc registered in Delaware?" |
| Sanctions and watchlist screening | "Screen Sweetgreen for sanctions" |
| How many businesses of a category are in a place, or how many locations a brand has | "How many pharmacies are in Montana?", "How many Wendy's restaurants are there in Ohio?" |
| Government records of a business, person, or address | "Show business licenses held by Walgreens", "Show SAM.gov registrations for Lockheed Martin" (see Get Government Records) |
| Up to three businesses or people at once | "Compare Starbucks and Dunkin" |

- TINs and watchlist entries are left out for `AccessTier.REDISTRIBUTION` and listed in `unavailable_data`, so a screening question returns the business without them (see Access Tiers).
- A question about a street address alone needs address lookups, which are on by default when the API supports them for your tier (see Look Up an Address).
- A count by category needs a business type the SDK can map to an industry code. The default and `TemplateStrategy` map a fixed list of common types by name, such as pharmacies, dentists, and coffee shops, and refuse any other. `GenerativeStrategy`, `AgentStrategy`, and `ServiceStrategy(query_strategy="generative")` let the LLM pick the code, so check the code in the query.

The strategies differ in speed, in what they need, and in how they handle a question outside this table (see Choose a Strategy).

A question that names several businesses, people, or street addresses can get several searches, all run in one query:

- Each business or street address the question names gets a search. A person named with their company ("Jane Doe at Sweetgreen") or a single street address given for a business ("the Starbucks at 1 Main St") shares that business's search.
- Each person the question names with no company gets a search.

A refused question raises `EnigmaClientException` from `client.search`, and no query runs (see Generation Errors). Refused questions include these:

- A question that needs more than three searches or counts, such as one naming more than three businesses. A generated query runs at most three; the government-record searches the SDK adds don't count (see Get Government Records).
- A question in which any one of its searches would be refused on its own. The whole question is refused, not only that search.
- A request to list businesses by category ("List coffee shops in Denver").

The limit of three applies to searches and counts, not to names. In a template query (from `ServiceStrategy`'s default or `TemplateStrategy`), a lookup of one location of a business, or of an address, gets one search for each entry in the question's longest list. That list can be of businesses, people, streets, cities, or states, and in an address lookup also of units or ZIP codes (see How Template Queries Read Places). So "the Starbucks at 1 Main St in Boston and 2 Elm St in Cambridge" gets two searches. A question about "the Starbucks at 1 Main St, 2 Elm St, 3 Oak St, and 4 Pine St in Boston" names one business but needs four searches, so it is refused.

When a question asks for data Enigma doesn't serve, such as lawsuits, that part is left out and listed in `unavailable_data`.

### Choose a Strategy

A strategy turns each question into a GraphQL query. The default runs on Enigma's servers. The other three are local strategies: they run on your machine and call an LLM with your own account.

| Strategy | How it writes the query | What you need | Speed |
|---|---|---|---|
| `ServiceStrategy` (default) | On Enigma's hosted query service, an LLM picks one of Enigma's tested query templates and fills it in | Your Enigma API key | Fast |
| `TemplateStrategy` | An LLM picks one of Enigma's tested query templates and fills in the names and places | An LLM API key | Fast |
| `GenerativeStrategy` | An LLM writes the whole query; the SDK checks it against the API schema and repairs it | An LLM API key | Slower |
| `AgentStrategy` | A Claude agent on Amazon Bedrock reads Enigma's query reference and writes the query | AWS credentials with Bedrock access, and `enigma-api-client[anthropic]` | Slowest, often tens of seconds |

When to use each:

- **Start with the default.** Its queries come from tested templates, so the fields a kind of question returns are predictable. A question no template fits gets the closest one, usually the business's profile, which may answer it only in part.
- **When the default's answer misses part of what you asked, such as a state, and `unavailable_data` doesn't list it,** `ServiceStrategy(query_strategy="generative", enigma_api_key=...)` has the hosted service write the whole query instead of picking a template. For example, it can narrow a person's businesses to one state ("businesses registered to Jane Doe in Pennsylvania"), where a template lists that person's businesses in every state. Review the queries it writes.
- **To generate queries with your own LLM account,** use `TemplateStrategy` in place of the default and `GenerativeStrategy` in place of the hosted generative option. Set `tier` in `ClientConfig` to your key's tier (see Access Tiers).
- **When the other strategies still leave something out and you can wait tens of seconds,** try `AgentStrategy`. The agent reads Enigma's GraphQL reference and its examples as it works.
- **For card transactions,** your tier must be `AccessTier.EXTERNAL` (see Access Tiers). Use any strategy except `AgentStrategy`.
- **For a pipeline that asks the same kind of question many times,** generate the query once, review it, and run it with your own variables (see the section See and Reuse a Generated Query).

To use a strategy other than the default, pass it to `ClientConfig`:

```python
from enigma_api_client.strategies import TemplateStrategy

config = ClientConfig(
    enigma_api_key=os.environ["ENIGMA_API_KEY"],
    query_strategy=TemplateStrategy(),
)
```

### ServiceStrategy

`ServiceStrategy` sends your question to Enigma's hosted query service, authenticated with your Enigma API key, and returns the query the service writes. `client.search` then runs it. The service reads your access tier from the key.

The service picks a template by default, as `TemplateStrategy` does. To have it write the whole query instead, create the strategy yourself with `query_strategy="generative"`. A `ServiceStrategy` you create doesn't take the key from `ClientConfig`: pass `enigma_api_key=`, or it reads `ENIGMA_API_KEY`.

```python
from enigma_api_client.strategies import ServiceStrategy

strategy = ServiceStrategy(query_strategy="generative", enigma_api_key=os.environ["ENIGMA_API_KEY"])
config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"], query_strategy=strategy)
```

### TemplateStrategy

An LLM reads the question, picks one of Enigma's query templates, and extracts the names, places, and other values the template needs. The SDK then builds the GraphQL from the template, so the query's fields always come from tested templates.

The templates cover:

- Brand profiles
- Brand lookups by website
- A brand's locations
- A location's profile
- Person searches
- Corporate officers
- Legal entity verification
- Contacts
- Watchlist screening
- Risk assessments
- Counts by category and place
- Counts of a brand's locations
- Address lookups, when turned on (see Turn Address Lookups On or Off)
- Card transactions, when your tier is `AccessTier.EXTERNAL` (see Access Tiers)

A brand, location, legal entity, officer, or person lookup returns a core set of fields plus only the data the question asks for: industry, website, phone, officers, bankruptcies, watchlist entries, TINs, registration details, locations, affiliates, alternate names, address, reviews, or headcount. Contact, watchlist, and risk lookups return the same fields whatever the question asks, apart from headcount and government records.

- A compliance question adds whichever of officers, TINs, watchlist entries, bankruptcies, and registration details the lookup can return (a location's profile can return only registration details). A compliance question is one that mentions KYB, KYC, AML, compliance, due diligence, a background check, or vetting, or that asks to vet or screen the entity. A question that only asks to screen an entity or to run due diligence on it usually gets the watchlist or risk lookup instead.
- A question asking for everything about an entity adds all of this data that its lookup can return, except headcount. A question gets headcount only by asking for it, as in "how many employees".
- With several businesses or people, the data the question asks for is added to each one's search.

`TemplateStrategy` refuses a category listing with `UNSUPPORTED_DATA_REQUESTED`. It refuses with `INVALID_QUERY` a question that isn't about a business, location, person, or address. With `AccessTier.REDISTRIBUTION`, it usually refuses a request for card transaction data the same way. Any other question that fits no template gets the closest one, usually the business's profile.

| Argument | Default | Meaning |
|---|---|---|
| `anthropic_api_key` | `ANTHROPIC_API_KEY` | The Anthropic API key, when `llm_provider` is not set |
| `model` | `claude-haiku-4-5-20251001` | The Anthropic model, when `llm_provider` is not set |
| `llm_provider` | `None` | Another LLM provider (see LLM Providers) |
| `shape_cache` | On, unless `ENIGMA_SHAPE_CACHE_ENABLED` turns it off | Whether to answer a question that matches an earlier one's pattern without an LLM call (see Query Shape Cache) |
| `shape_cache_max_shapes` | 8192 | The most query shapes the cache keeps |
| `address_search` | `ENIGMA_ADDRESS_SEARCH`, else `"auto"` | Whether the strategy writes address lookups (see Look Up an Address) |

### How Template Queries Read Places

This section applies to template queries: those from `ServiceStrategy`'s default and from `TemplateStrategy`. In this section, a place is a city, a state, or, in an address lookup, a unit or a ZIP code.

Which lookups use places:

- A lookup of one location of a business, or of an address, searches by street, city, and state, and an address lookup also by unit and ZIP code. The next two lists say how a question's lists of these values pair.
- A brand's locations, counts of a brand's locations, and counts by category are filtered by city and state. A count by category over several places is one count, not one per place. Each search or count matches every city it is given in every state it is given, so "coffee shops in Springfield, IL and Columbus, OH" also counts those in Springfield, OH.
- A legal entity lookup isn't filtered by place. It lists each business's registrations in the states the question gives that business, under `registrationsInState` in each of its `registeredEntities`. The rules in the next list decide which states each business gets.
- Brand profiles, person searches, and officer, contact, watchlist, risk, and card transaction lookups don't filter by place.

How places are matched to businesses, people, and streets:

- **A lookup of one location of a business ("the Starbucks at 1 Main St"), or of an address, pairs places by position.** The first city or state in a list goes with the first business or street, the second with the second, and so on. In an address lookup, units and ZIP codes pair the same way. Each position gets its own search, so a list of four streets gets four searches.
- **Elsewhere, every place applies to each business or person.** "Starbucks locations in Boston and Dunkin locations in Chicago" gets one search per brand, each covering both cities.
- **To give each one its own place,** say "respectively" ("Starbucks and Dunkin locations in Boston and Chicago, respectively"), or ask about them separately.
- **When the question gives a place for some of them and none for another, each keeps its own.** "Starbucks in Boston, Dunkin, and Peet's in Chicago" searches Starbucks in Boston and Peet's in Chicago, and doesn't filter Dunkin by city.
- The rules apply to a question's cities and to its states separately, so in one question each business can keep its own city while every state applies to each business.

How pairing by position works:

- A single business or person pairs with every street and every place in a list, and a single street pairs with every place. So "the Starbucks at 1 Main St in Boston and Cambridge" gets two searches, and so does "the Starbucks at 1 Main St and 2 Elm St".
- Several businesses, people, or streets pair by position with a list of cities, states, units, or ZIP codes.
- A list of places can be longer than the list of businesses, people, or streets it pairs with. When the shorter list is of streets that all go with one business or person, that business or person is also searched in each extra place, without a street. In "the Starbucks at 1 Main St and 2 Elm St in Boston, Cambridge, and Somerville", Starbucks goes with both streets and is also searched in Somerville. Otherwise, the extra place has nothing to pair with, and the question is refused.
- Outside a count question, a street that pairs with no business or person gets an address lookup of its own (see Look Up an Address). While address lookups are off, such a question is refused.

### GenerativeStrategy

One LLM call writes the whole query. It handles questions the templates don't cover. `GenerativeStrategy` takes the same arguments as `TemplateStrategy`.

- The LLM works from an excerpt of Enigma's GraphQL reference, example query patterns, and rules. The rules tell it to write the question's values into the query text.
- The SDK checks the query against the API schema and the rules the API enforces at execution.
- The SDK fixes what it can without a model call. Otherwise, it makes one more LLM call to repair the query.
- The LLM can still choose fields that don't answer the question, so review the queries it writes before you rely on them.

`GenerativeStrategy` raises `UNSUPPORTED_DATA_REQUESTED` instead of returning a query in these cases:

- Any search in the query is one of these:
  - A person search by surname alone, when the question uses the word "surname", "last name", or "family name".
  - An employer identification number (EIN) written with its dash, such as `12-3456789`, searched as a name.
  - A number of five or more digits from the question searched as a name, when the question names an identifier, such as a filing number or license number, unless the question introduces the number as a business's name, as in "the store called 90210".
  - A wildcard name pattern, such as `Star%`.
  - A street address searched as a name, when the question uses wording such as "tenants", "occupants", "located at", "registered at", "operating out of", or "at this address".
- The query is a category count the question didn't ask for. That is, all of these hold:
  - The query only counts businesses: every top-level field in it is an `aggregate` count.
  - Each count writes its search input into the query text, not into variables.
  - No count names a business, website, person, phone number, id, or TIN.
  - At least one count is by category.
  - The question doesn't ask for a count.

### AgentStrategy

A Claude agent, built with the Claude Agent SDK, reads Enigma's GraphQL reference, examples, and notes on the schema's fields, and writes the query. The SDK checks the agent's answer against the API schema and applies the same checks as `GenerativeStrategy`, fixing what it can without a model call. `AgentStrategy` suits complex questions best and is the slowest strategy.

```bash
pip install "enigma-api-client[anthropic]"
```

```python
from enigma_api_client.strategies import AgentStrategy

config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"], query_strategy=AgentStrategy())
```

The agent runs on Amazon Bedrock only:

- Credentials come from the standard AWS credential chain.
- The identity needs `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream` on the inference profile and on the foundation model in every region the profile routes to.
- The default model is the US inference profile `us.anthropic.claude-haiku-4-5-20251001-v1:0`. Pass `model=` to change it.
- The region comes from `region=`, then `AWS_REGION`, then `AWS_DEFAULT_REGION`, and is `us-east-1` when none is set. Keep the region inside the profile's geography.
- `max_turns` (default 8) limits each call's agent turns, and `max_budget_usd` (default 0.25) limits each call's model spend in US dollars. A call that reaches either limit raises `INVALID_QUERY`.

The agent runs the Claude Code CLI bundled with `claude_agent_sdk` as a subprocess:

- Its nonessential network traffic is turned off.
- Its only tool reads files inside the query reference bundled with the SDK. It has no shell, file writes, web access, or file attachments.
- Every environment variable it inherits is blanked, except basic process settings (such as `PATH`, `HOME`, locale, and temporary directories), TLS and proxy settings, and the AWS credential chain.
- It keeps no session transcript on disk.

The agent writes values into the query itself, so the query it returns has no variables.

When its answer still fails the schema check after the SDK's fixes, `AgentStrategy` raises `UnrepairedQueryError`.

### See and Reuse a Generated Query

`strategy.generate_query(prompt)` returns the query without running it:

```python
import asyncio
import os

from enigma_api_client import Client, ClientConfig
from enigma_api_client.strategies import ServiceStrategy


async def main():
    key = os.environ["ENIGMA_API_KEY"]
    strategy = ServiceStrategy(enigma_api_key=key)
    async with Client(ClientConfig(enigma_api_key=key, query_strategy=strategy)) as client:
        generated = await strategy.generate_query("Tell me about Chipotle")
        print(generated.query)
        print(generated.variables)  # {'searchInput': {'name': 'Chipotle', 'entityType': 'BRAND', 'conditions': {'limit': 1}}}

        variables = {"searchInput": {**generated.variables["searchInput"], "name": "Sweetgreen"}}
        response = await client.search(query=generated.query, variables=variables)
        print(response.data["search"][0]["names"]["edges"][0]["node"]["name"])  # SWEETGREEN


asyncio.run(main())
```

- Check a query before you reuse it with other variables. Any strategy can write some values from the question directly into the query text, where your variables don't replace them:
  - When a question names a state, a legal entity lookup writes that state into the query.
  - A query that selects government records writes the searched name, person's name, and address into its archive searches.
  - `GenerativeStrategy` is told to write every value into the query text, and `AgentStrategy` always does.
- A template query numbers its variables like the keys of `data`: `searchInput`, `searchInput2`, and `searchInput3`. Other variables follow the same pattern, such as `locConditions2`.
- Call `generate_query` inside the `async with` block. A local strategy checks its query against the API schema, which the `Client` fetches with your key.
- Called directly, a local strategy's `generate_query` writes for `AccessTier.REDISTRIBUTION` unless you pass your tier, as in `generate_query(prompt, tier=AccessTier.EXTERNAL)`. `client.search` passes `ClientConfig.tier` for you. `ServiceStrategy` always writes for your key's tier.

`generate_query` returns a `GenerateQueryResponse`:

| Field | Holds |
|---|---|
| `query`, `variables` | The GraphQL query and its variables |
| `unavailable_data` | The parts of the question Enigma can't answer |
| `deterministic_repair` | The kinds of fixes, comma separated, when the SDK fixed the query without a model call |
| `cache_rule` | `"shape"` when the query shape cache answered without an LLM call; other values report what the cache learned from the call |
| `records` | For a question asking for government records, from a local strategy: whether the SDK added record fields (see the list after this table) |

`records` takes these values:

- `selected`: the SDK added record fields.
- `no_schema`: the SDK had no API schema yet (see Records and the API Schema).
- `no_entity`: the query has no entity search to add them to, such as a count.
- `already_selected`: the query already selects records, or already uses a response key the SDK would add, such as `archiveRecords`.
- `invalid`: the query doesn't parse, or adding record fields would break a rule the query passes.

### Access Tiers

The fields you can read depend on your API key's access tier:

- `AccessTier.REDISTRIBUTION` is the narrowest tier.
- `AccessTier.EXTERNAL` adds fields such as TINs, watchlist entries, and card transaction data.

The hosted service reads the tier from your key. A local strategy writes queries for the tier in `ClientConfig.tier`, which defaults to `AccessTier.REDISTRIBUTION`. Set it to your key's tier:

```python
from enigma_api_client import AccessTier
from enigma_api_client.strategies import TemplateStrategy

config = ClientConfig(
    enigma_api_key=os.environ["ENIGMA_API_KEY"],
    query_strategy=TemplateStrategy(),
    tier=AccessTier.EXTERNAL,
)
```

If you keep the default `tier`, a local strategy writes queries that leave out fields a broader key could read. If you don't know your key's tier, ask [Enigma support](mailto:support@enigma.com).

- A local strategy removes from the query each field the API doesn't serve your tier, and lists it in `unavailable_data` with the reason `contractual`. `enigma_api_client.restricted_fields_for(tier)` returns those fields.
- For `AccessTier.REDISTRIBUTION`, once the SDK has the API schema (see Records and the API Schema), a local strategy also leaves out the email addresses and phone numbers of the people in a company's roles (`emailAddresses` and `phoneNumbers` under a role), because the API answers a redistribution key with placeholder values for them. It lists a field it removed in `unavailable_data`, as `Role.emailAddresses` or `Role.phoneNumbers` with the reason `contractual`, only when the question asks for it and the query had selected it, as in "What is the email of Sweetgreen's CFO?" answered by the contacts lookup. A location's own phone numbers are real and stay.
- To filter data before you pass it on to others, use `enigma_api_client.gate_blocked_fields(tier)`. It returns those fields plus, for `AccessTier.REDISTRIBUTION`, fields the API serves but a redistribution contract forbids. A local strategy doesn't remove those extra fields, and the API can return them to your own queries too.

### Schema Checks and Several Tiers

- The SDK fetches the API schema for the `tier` of the first `Client` you create in a process, with that client's key, and checks generated queries against it. Set `tier` before you create your first `Client`. In a notebook, restart the kernel after you change it.
- A later `Client` with a different `tier` gets no schema. On that client, `AgentStrategy` raises `PROVIDER_ERROR`. `TemplateStrategy` and `GenerativeStrategy` still answer, with these differences:
  - Generated queries aren't checked against the schema.
  - Government records are left out.
  - Address lookups are off unless you turn them on with `address_search=True` or `ENIGMA_ADDRESS_SEARCH=on` (see Turn Address Lookups On or Off).
  - For `AccessTier.REDISTRIBUTION`, role email addresses and phone numbers stay in the query, so the API answers them with placeholder values (see Access Tiers).

To serve several tiers in one process, register a schema introspector with `validation.set_schema_introspector` before you create the first `Client`. An introspector is a function that takes an `AccessTier` and returns that tier's schema.

The SDK calls it either on your event loop's thread, blocking every search on that loop until it returns, or from a background thread with no context variables (`contextvars`) set. So the introspector must:

- Be synchronous and thread-safe.
- Return in bounded time.
- Not read context variables.
- Return `None` when a fetch fails, as the following example does, so a failed fetch doesn't raise from `client.search`.

```python
import os

from enigma_api_client import AccessTier, validation

KEYS = {
    AccessTier.EXTERNAL: os.environ["ENIGMA_EXTERNAL_KEY"],
    AccessTier.REDISTRIBUTION: os.environ["ENIGMA_API_KEY"],
}


def introspector(tier):
    key = KEYS.get(tier)
    if key is None:
        return None
    try:
        return validation.introspect_schema("https://api.enigma.com/graphql", key)
    except Exception:
        return None


validation.set_schema_introspector(introspector)  # before you create the first Client
```

Each `Client` then gets the schema for its own `tier`.

The first time a tier's schema is used after it is five minutes old, the SDK fetches it again in the background. Pass `refresh_seconds=` to `set_schema_introspector` to change that interval, or `refresh_seconds=None` to turn the refresh off.

### LLM Providers

`TemplateStrategy` and `GenerativeStrategy` call Anthropic's API by default, with `anthropic_api_key=` or the `ANTHROPIC_API_KEY` environment variable; their `model=` argument applies only to that default. Pass `llm_provider=` to use another provider:

```python
from enigma_api_client.strategies import (
    AnthropicProvider, BedrockProvider, GenerativeStrategy, OpenRouterProvider, TemplateStrategy,
)

# Anthropic's API (ANTHROPIC_API_KEY)
TemplateStrategy(llm_provider=AnthropicProvider(model="claude-haiku-4-5-20251001"))

# OpenRouter (OPENROUTER_API_KEY). provider_routing is sent as OpenRouter's "provider"
# object and reasoning as its "reasoning" object; omit either to keep OpenRouter's default.
GenerativeStrategy(llm_provider=OpenRouterProvider(
    model="anthropic/claude-haiku-4.5",
    provider_routing={"order": ["anthropic"], "allow_fallbacks": False},
    reasoning={"enabled": False},
))

# Claude on Amazon Bedrock (pip install "enigma-api-client[bedrock]"), signed with your AWS credentials.
# The default model is a US inference profile; pass model= outside US regions.
TemplateStrategy(llm_provider=BedrockProvider(region="us-east-1"))
```

### Write Your Own LLM Provider

To use another LLM, implement the `LlmProvider` protocol. Import `LlmProvider`, `Completion`, and `ProviderHTTPError` from `enigma_api_client.strategies`.

- Implement `async def complete(system, user, max_tokens) -> Completion`.
- Return `Completion(text, truncated)`, where `truncated` is true when the model hit its output limit.
- Pass the call's token counts to `Completion` as `input_tokens=`, `output_tokens=`, `cache_read_tokens=`, and `cache_write_tokens=`, so that Token Usage includes them. Leave cache reads and writes out of `input_tokens`; they have their own fields.
- If your provider reports which host served the call, pass it as `served_by=`.
- If your provider reports what it billed for the call, pass the amount in USD as a float, `cost_usd=`.
- Raise these exceptions for failures:

| Raise | Reported as |
|---|---|
| `ProviderHTTPError(status_code, message)` with 408, 429, or 5xx | `TRANSIENT_PROVIDER_ERROR` |
| `ProviderHTTPError` with any other HTTP status | `PROVIDER_ERROR` |
| `ProviderHTTPError` with a value that isn't an HTTP status | `TRANSIENT_PROVIDER_ERROR` |
| `ValueError`, for a missing key or bad configuration | `GENERATE_QUERY_CALLBACK_NOT_CONFIGURED` |
| `RuntimeError`, for any other provider failure | `TRANSIENT_PROVIDER_ERROR` |

Any other exception propagates unchanged.

### Query Shape Cache

`TemplateStrategy` and `GenerativeStrategy` keep an in-memory cache of query shapes learned from successful generations:

- A shape is a generated query with its names and places left as blanks.
- Once a shape has been confirmed across several different entities, a matching question is answered from the cache without an LLM call.
- The cache is on by default and is shared by every caller of the same strategy instance at the same tier.
- To turn it off, pass `shape_cache=False`, or set `ENIGMA_SHAPE_CACHE_ENABLED=0` before you create the strategy.

### Token Usage

After `client.search(prompt=...)` or `generate_query` returns or raises, `enigma_api_client.strategies.GENERATION_USAGE.get()` returns that call's LLM token counts as an `LlmUsage`:

| Field | Holds |
|---|---|
| `completions` | The number of LLM completions |
| `input_tokens`, `cache_read_tokens`, `cache_write_tokens`, `output_tokens` | The token counts |
| `unmetered_completions` | The number of completions whose provider left out the input or the output token count, so the sums miss their tokens |
| `served_by` | The host each completion named as having served it, in call order, for providers that report one, such as `OpenRouterProvider` |
| `cost_usd` | The amount in USD billed for the completions whose provider reported one, such as `OpenRouterProvider`; completions from other providers add nothing |

- Read it in the same task that awaited the call. `asyncio.gather` runs each call in a task of its own, so read the usage inside the coroutine you pass to it.
- `ServiceStrategy` reports zero usage, and `AgentStrategy` reports its run's token totals and no `cost_usd`.

### Write Your Own Strategy

To write your own strategy:

1. Subclass `QueryStrategy`.
2. Implement `generate_query` and `aclose`.
3. At the start of `generate_query`, set `GENERATION_USAGE` to a fresh `LlmUsage()`, so Token Usage reports this call, not an earlier one.

This strategy treats the whole question as a brand name:

```python
import asyncio
import os

from enigma_api_client import DEFAULT_TIER, Client, ClientConfig, QueryStrategy
from enigma_api_client.strategies import GENERATION_USAGE, LlmUsage
from enigma_api_client.strategies.base import GenerateQueryResponse

QUERY = """
query Brand($name: String!) {
  search(searchInput: { name: $name, entityType: BRAND, conditions: { limit: 1 } }) {
    ... on Brand { names(first: 1) { edges { node { name } } } }
  }
}
"""


class BrandNameStrategy(QueryStrategy):
    async def generate_query(self, prompt, *, tier=DEFAULT_TIER, prior_attempt=None):
        GENERATION_USAGE.set(LlmUsage())
        return GenerateQueryResponse(QUERY, {"name": prompt})

    async def aclose(self):
        pass


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"], query_strategy=BrandNameStrategy())
    async with Client(config) as client:
        response = await client.search(prompt="Chipotle")
        print(response.data["search"][0]["names"]["edges"][0]["node"]["name"])  # CHIPOTLE MEXICAN GRILL


asyncio.run(main())
```

## Handle Errors

Errors reach you in these ways:

- When the SDK can't generate a query, `client.search(prompt=...)` raises `EnigmaClientException` (see Generation Errors).
- An error status from the API doesn't raise. It comes back in the response, so check `response.is_success()`.
- A network failure reaching the API or the hosted query service raises an `httpx` exception (see API and Network Errors).
- An Enigma API key or custom header value that an HTTP header can't carry as written, such as one with a newline or a trailing space, raises `ValueError` (see API and Network Errors).

```python
import asyncio
import os

from enigma_api_client import Client, ClientConfig, EnigmaClientException


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        try:
            response = await client.search(prompt="List coffee shops in Denver")
        except EnigmaClientException as exc:
            print(exc.error_code, exc)
            return
        if not response.is_success():
            print("HTTP", response.status_code, response.errors)


asyncio.run(main())
```

The example prints the error message, which is useful while you develop. In production, log the exception type and `error_code` instead, because the message can quote values from the query.

### Generation Errors

When the SDK can't generate a query, `client.search(prompt=...)` raises `EnigmaClientException`, whose `error_code` is an `EnigmaClientErrorCode`.

With `ServiceStrategy`, the default, the code is always `INVALID_QUERY`, including when the hosted service fails with HTTP 401, 429, or 5xx. To tell these failures apart, read the exception:

- When the service answers with an error status, `exc.status_code` holds it and `exc.headers` holds its response headers. The message reads `Enigma API returned HTTP <status>:` followed by the start of its answer.
- When the service answers with a GraphQL error, the message starts `Enigma API GraphQL error:`.
- Retry when the status is 429 or 5xx. When `exc.headers.get("Retry-After")` gives a number of seconds, wait that long first.

For the preceding example, the message begins `Enigma API returned HTTP 400` and explains that listing businesses by category is not supported.

Local strategies use these codes:

| Code | Meaning |
|---|---|
| `INVALID_QUERY` | The question or the generated query is unusable |
| `UNSUPPORTED_DATA_REQUESTED` | Enigma doesn't serve what the question asks for, or the question needs a query the SDK refuses (see Generate Queries from Questions and GenerativeStrategy) |
| `GENERATE_QUERY_CALLBACK_NOT_CONFIGURED` | An LLM API key is missing, or the provider is misconfigured |
| `TRANSIENT_PROVIDER_ERROR` | The LLM provider timed out, rate limited the call, or failed with a 5xx error; retry with backoff |
| `PROVIDER_ERROR` | Any other LLM provider failure, such as HTTP 401 or 403. Retrying won't help. |

`AgentStrategy` also raises `PROVIDER_ERROR` when the SDK has no API schema for your tier (see Schema Checks and Several Tiers). In that case, a retry helps only if the schema fetch failure was temporary.

For a provider failure, `exc.status_code` holds the provider's HTTP status when it reported one.

The SDK never raises `RESTRICTED_DATA_REQUESTED` or `INVALID_VARIABLES`; they exist for applications that enforce their own policies.

`enigma_api_client` exports two subclasses of `EnigmaClientException`, both with the code `INVALID_QUERY`:

- `TruncatedOutputError`: the LLM hit its output limit. It happens again with the same model and question, so don't retry with the same model.
- `UnrepairedQueryError`: `GenerativeStrategy` or `AgentStrategy` wrote a query that failed the schema check even after repair. It carries `.draft`, `.errors`, and `.tier`, and you can pass it to another `GenerativeStrategy` as `generate_query(prompt, prior_attempt=exc)` to start from that draft. Its message quotes values from the generated query, so log the exception type and `error_code` rather than `str(exc)`.

### Unavailable Data

When a question asks for data Enigma doesn't serve, the rest of the question is still answered, and `response.unavailable_data` lists what was left out, one entry per item:

- `field`: what was left out.
- `reason`: `contractual` when your tier isn't served the data, and `out_of_scope` otherwise, for example when Enigma doesn't have it.
- `message`: why.

"What is Chipotle's annual revenue?" with a redistribution key returns the brand and this entry:

```python
[{"field": "revenue", "reason": "contractual",
  "message": "Revenue and other card-derived financials are not served due to contractual restrictions."}]
```

"Tell me about Chipotle, including any lawsuits against it" lists `lawsuits` as `out_of_scope`.

### API and Network Errors

- An error status from the API doesn't raise. Check `response.is_success()` and `response.status_code`, and see [Response status codes](https://documentation.enigma.com/guides/graphql/response-status-codes). A 402 means a billing problem, such as running out of credits.
- A 429 means you went over a [rate limit](https://documentation.enigma.com/resources/rate-limits). Wait at least the number of seconds in its `Retry-After` header, `response.headers.get("Retry-After")`, before you retry. Stop after a few retries, because a 429 can also mean you have used your daily limit.
- A response body that isn't a JSON object, such as a gateway's error page, comes back with `data` set to `None`, an error in `errors`, and the body in `raw_body`, so `is_success()` is false.
- An API key or header value an HTTP header can't carry as written, such as one with a newline or a trailing space, raises `ValueError` naming the header, before the API request is sent.
- Network failures, from the API or the hosted query service, raise `httpx` exceptions such as `httpx.TimeoutException` and `httpx.ConnectError`.

## Paginate

Each connection returns one page. To read every page, select `pageInfo { hasNextPage endCursor }` on the connection and send the same query again with `after:` set to the last `endCursor`:

```python
import asyncio
import os

from enigma_api_client import Client, ClientConfig

QUERY = """
query Locations($name: String!, $cursor: String) {
  search(searchInput: { name: $name, entityType: BRAND, conditions: { limit: 1 } }) {
    ... on Brand {
      operatingLocations(first: 100, after: $cursor) {
        edges { node { addresses(first: 1) { edges { node { fullAddress } } } } }
        pageInfo { hasNextPage endCursor }
      }
    }
  }
}
"""


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        cursor, addresses = None, []
        while True:
            response = await client.search(query=QUERY, variables={"name": "Sweetgreen", "cursor": cursor})
            if not response.is_success():
                raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
            locations = response.data["search"][0]["operatingLocations"]
            for edge in locations["edges"]:
                for address in edge["node"]["addresses"]["edges"]:
                    addresses.append(address["node"]["fullAddress"])
            if not locations["pageInfo"]["hasNextPage"]:
                break
            cursor = locations["pageInfo"]["endCursor"]
        print(len(addresses), "addresses")


asyncio.run(main())
```

- Start with `cursor` set to `None`.
- Use `hasNextPage` to decide whether more pages exist. A full page doesn't mean there are more.
- Keep the connection's `first` and `conditions` (with its `filter` and `orderBy`) the same on every page, because a cursor is only valid for the arguments it came from.
- `ClientConfig(auto_inject_page_info=True)` adds `pageInfo { hasNextPage endCursor }` to every connection that doesn't already select `pageInfo`, in every query the client sends, including generated ones.
- Generated queries already select page info on:
  - A brand's affiliated brands (`Brand.affiliatedBrands`) and a legal entity's parent and subsidiary legal entities (`LegalEntity.legalEntities`). `GenerativeStrategy` and `AgentStrategy` add this page info only once they have the API schema for your tier.
  - The government record pages, which a local strategy adds only once it has that schema (see Records and the API Schema).
  - `Brand.operatingLocations`, in the template that lists a brand's locations.
  - `registrationsInState`, in the legal entity lookup, when it lists registrations in several states. The registrations of those states share one page.
- To read more pages of a generated query:
  1. Get the query with `generate_query`.
  2. Add `after: $cursor` to the connection.
  3. Declare `$cursor: String` with the query's other variables, as the preceding example does. If the query declares no variables yet, add `($cursor: String)` after its name, or begin it with `query ($cursor: String) {` when it has no name.
- `roles` doesn't paginate (see Rules the API Enforces at Execution).

## Look Up an Address

A question about a street address with a house number and no business name searches addresses:

```python
import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(prompt="What is at 1600 Pennsylvania Ave NW, Washington DC?")
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        for address in response.data["search"]:
            locations = address["operatingLocations"]["edges"]
            print(f'{address["fullAddress"]}: {len(locations)} operating locations listed')


asyncio.run(main())
```

The query is an `entityType: ADDRESS` search that returns up to 10 addresses: the building and units in it. When the question names a unit, that unit usually comes first. Each address carries:

- Its address fields, such as `fullAddress`, `city`, `state`, and `zip`.
- The first page of the operating locations (with brand names), legal entities, and registrations at the address.

No strategy selects the `Address` counts `brandViaLegalEntityCount`, `brandViaPersonLegalEntityCount`, and `legalEntityViaRegistrationCount`, because they take too long to compute. Leave them out of your own queries too.

An address question that asks how many, other than a headcount, gets an `unavailable_data` entry with `field` set to `address counts` and `reason` set to `out_of_scope`, because the query doesn't count what is at the address. When the question also asks for government records and the SDK adds record fields, each address gets `recordCount` (see Records on Each Entity), and the response has no `address counts` entry.

How questions are read:

- A business name with an address ("the Starbucks at 123 Main St") is looked up as that business's location instead.
- A ZIP code alone, a street with no house number, or a business type on a street ("the barbershop on Elm Street") is refused.
- Before a local strategy returns the query, the SDK cleans the address the LLM extracted:
  - It moves a suite, floor, or apartment into `street2` in US Postal Service (USPS) short form (`STE 1A`, `FL 8`).
  - It drops a unit designator with no number.
  - It removes surrounding prose and a trailing country.
  - It converts state names to postal codes.

### Turn Address Lookups On or Off

Local strategies take an `address_search=` argument: `True`, `False`, or `"auto"`.

- Without the argument, a strategy reads the `ENIGMA_ADDRESS_SEARCH` environment variable (`on`, `off`, or `auto`).
- When neither is set, it uses `auto`.
- `auto` turns address lookups on when the API schema for your tier supports them. Each strategy instance decides at its first question for each access tier and keeps that answer. If the schema can't be fetched at that moment, address lookups stay off for that instance and tier.
- With address lookups off, a local strategy handles a question that gives only an address in one of two ways:
  - Usually, it refuses the question, raising `UNSUPPORTED_DATA_REQUESTED` or `INVALID_QUERY`.
  - Sometimes its LLM takes the address as a business name, and the strategy can return a query that searches for that name. `GenerativeStrategy` and `AgentStrategy` refuse such a query only when the question uses wording such as "tenants" or "registered at" (see GenerativeStrategy).
- While the API schema the SDK has cached for your tier shows no support for address lookups, a local strategy refuses an address search, whatever `address_search` says. It raises `UNSUPPORTED_DATA_REQUESTED`, and the message ends `(bare address lookup)`. Other address refusals, such as those with address lookups off, can end the same way, so the ending alone doesn't tell you why the question was refused.
- While no schema for your tier is cached yet, `TemplateStrategy` and `GenerativeStrategy` with address lookups turned on (`address_search=True` or `ENIGMA_ADDRESS_SEARCH=on`) still write address lookups.

## Get Government Records

To get an entity's government records, name the entity and ask for its records, as in "Show business licenses held by Walgreens".

The records come from Enigma's [Gov Archive](https://documentation.enigma.com/guides/gov-archive), which holds rows of government and public datasets, such as state Uniform Commercial Code (UCC) filings, business licenses, and city food inspections:

- Each row is a record.
- Enigma links most records to the businesses, locations, people, and addresses they name.
- Each dataset carries tags, such as `ucc_lien`, that name the types of records it holds.

You can ask for these record types:

- UCC and lien filings
- Business and occupational licenses, including liquor, contractor, tobacco and cannabis, insurance, money transmitter, firearms, and childcare licenses
- Permits and sales tax permits
- Inspections and health grades
- Violations and enforcement actions
- Government contracts, Paycheck Protection Program (PPP) loans, and SAM.gov registrations, including Unique Entity IDs (UEI) and CAGE codes
- Form 5500 benefit plans, H-1B and labor certifications, workers' compensation, and Worker Adjustment and Retraining Notification (WARN) layoff notices
- Securities and Exchange Commission (SEC) filings, 13F holdings, and Form 990 filings
- Property records, Environmental Protection Agency (EPA) records, and US Department of Transportation (USDOT) motor carrier records
- Campaign finance and lobbying
- Medicare and Medicaid providers, and minority-, women-, and veteran-owned business certifications

How a question is read:

- A question that asks for "public records", "government records", or the "gov archive" in general gets records of every type, unless it also names one of the preceding types.
- Words in the names a question searches by don't count, so a question about a business named "Permit Pros" asks for no permits.

The response holds records in two places:

- On each entity `search` returns (see Records on Each Entity).
- In archive searches: searches of the Gov Archive that the SDK adds to the query next to `search`, answered under keys that start with `archiveRecords` or `archiveBrandRecords` (see Records from Archive Searches).

This example prints both:

```python
import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(prompt="Show business licenses held by Walgreens")
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        for entity in response.data["search"]:
            print(entity["__typename"], "linked records:", entity.get("recordCount"))
        for key, items in response.data.items():
            if key.startswith(("archiveRecords", "archiveBrandRecords")):
                for item in items or []:
                    if item["__typename"] == "Record":
                        print(key, item["sourceTitle"], item["data"])


asyncio.run(main())
```

### Records on Each Entity

Each entity the search returns gains `recordCount`. Whether it also gets a `records` page depends on the question and on the API:

- For a question that names record types, the SDK adds the page and `matchingRecordCount` only when the API can filter an entity's linked records quickly, which the SDK tells from the API schema. Otherwise, the entities get neither, and those records come from the archive searches alone.
- A question asking for records in general gets the `records` page either way.

Read the page with `entity.get("records")`.

| Field | Where | Holds |
|---|---|---|
| `recordCount` | Every returned entity | The number of records linked to the entity, over every dataset |
| `recordSourceCounts`, `recordTagCounts` | A returned address | The record count per dataset title and per tag |
| `recordSources` | A returned legal entity | The first three of its `registeredEntities`, with page info. Each carries `recordSourceCounts` and `recordTagCounts`. |
| `matchingRecordCount` | An entity that gets a `records` page, for a question that names record types | The number of its linked records of those types |
| `records` | An entity that gets a `records` page | The linked records, with `pageInfo { hasNextPage endCursor }`: the first ten when the search is limited to one entity (`conditions: { limit: 1 }`), otherwise the first three. For a question that names record types, only records of those types. |

Legal entities the query selects under a returned brand or location also get:

- `recordCount`.
- Their first three `records`, when the returned entities get a `records` page.

### Records from Archive Searches

An archive search searches the Gov Archive by the same name or person (first and last name) as the `search` it sits beside, and by that search's address when the address has a street line. It takes the whole address: street line, unit, city, state, and ZIP code.

Which archive searches a query gets:

- A search by name, by a person's first and last name, or by a street address gets `archiveRecords`, with two exceptions:
  - A street address alone, with no name or person's first and last name, gets one only in an `entityType: ADDRESS` search.
  - In a `PERSON` search, `name` doesn't count; only the person's first and last name do.
- A name search with `entityType: BRAND`, `entityType: OPERATING_LOCATION`, or no `entityType` gets two archive searches:
  - `archiveRecords` searches legal entities.
  - `archiveBrandRecords` searches brands, which finds records that name a brand or trade name.
  - Neither one searches by a person's first and last name, even when the `search` has them.
  - The two can return the same record.
- A search with no name, person, or street address, such as one by website alone, gets no archive search.
- When several searches in the query get archive searches, the keys of the second and later ones end in a number, as in `archiveRecords2`. The number counts only the searches that get archive searches, so it need not match the number of the `search` key beside it.
- An archive search identical to an earlier one is sent once.

What each archive search returns:

- First, the entities it matched, with only `__typename` (and, for a person, `__url`).
- Then up to 50 records: the items whose `__typename` is `Record`. It returns a record when the record's data names the searched name or address, whether or not the record is linked to an entity.
- For a question that names record types, each record carries one of their tags. A question for records in general gets records with any tag.
- One page, with no cursor to read more.

Check what you get:

- The archive matches names loosely, so a record can belong to another business or person with a similar name. Unless the searched address has a street line, the archive search gets no city or state, so that namesake can be anywhere. Check the name in the record's `data`.
- A dataset can carry a record type's tag without belonging to it, so check `sourceTitle`.
- Records are not sorted by date.
- The archive searches miss many records of these types: UCC and lien filings, contractor licenses, PPP loans, government contracts, SAM.gov registrations, workers' compensation, lobbying, and Form 990 filings. When the entities get no `records` page, a question for these types often returns few records or none.

To leave the archive searches out of queries a local strategy writes, set `ENIGMA_ARCHIVE_SEARCH=off`.

For the example question, the query gains `recordCount` on each entity it returns, and archive searches next to `search`. The `archiveRecords` search, abbreviated:

```graphql
archiveRecords: search(searchInput: { name: "Walgreens", entityType: LEGAL_ENTITY, dataLayer: UNGROUNDED,
                                      conditions: { limit: 50, filter: { IN: ["tags", ["abc_license", "active_occupational_licenses", ...]] } } }) {
  __typename
  ... on Record { sourceTitle sourceUrl tags firstObservedDate lastObservedDate data }
}
```

`dataLayer: UNGROUNDED` makes the search read the archive's records directly, whether or not they are linked to an entity.

### What Each Record Holds

Each record, on an entity or in an archive search, carries:

- `sourceTitle`: the dataset's title.
- `sourceUrl`: a link to the dataset, not to the row. It can be null.
- `tags`: the dataset's tags, such as `ucc_lien` or `abc_license`.
- `firstObservedDate`, `lastObservedDate`: when Enigma first and last saw the row. These are usually not filing dates.
- `data`: the row itself, as column names and values. The filing dates are here.

### Read More of an Entity's Records

When an entity's `records` page has `pageInfo.hasNextPage` set to true, read the next page:

1. Search that entity alone by its id: `searchInput: { id: "<its id>", entityType: <its type>, conditions: { limit: 1 } }`. Set `entityType` to `BRAND`, `LEGAL_ENTITY`, `OPERATING_LOCATION`, `PERSON`, or `ADDRESS` to match its `__typename`.
2. Select `records` with the same `first` and `conditions` as before, plus `after: "<endCursor>"`. The cursor is only valid for those arguments.

To get a query to adapt, call `generate_query`.

### Record Types and Their Phrases

The installed package lists the record types a question can ask for, with the phrases that ask for each and the tags that select it:

```python
import json
from importlib.resources import files

path = files("enigma_api_client") / "plugins/enigma-api/skills/enigma-graphql/references/record_topics.json"
for topic in json.loads(path.read_text())["record_topics"]["topics"]:
    print(topic["id"], topic["phrases"][:3])
```

### Records and Unavailable Data

- An `unavailable_data` entry for something the selected records answer in full, such as "liquor licenses", is removed.
- When the query selects record counts but none of the records the question asks for, the SDK keeps each entry those records would answer in full and gives it the message "This query does not return these records." This happens, for example, on a search by website alone when the entities get no `records` page.
- When the query selects the records, an entry for something they answer only in part, such as "tax liens" or "lawsuits and violations", gets the message "The returned records cover this only in part."

### Records and the API Schema

A local strategy adds records only once it has the API schema for your tier.

`TemplateStrategy` fetches the schema as follows:

- With `address_search` set to `auto`, the default, it fetches the schema before it answers its first question, because it needs the schema to decide on address lookups.
- If you set address lookups yourself (`address_search=True` or `False`, or `ENIGMA_ADDRESS_SEARCH` set to `on` or `off`), it doesn't wait for the schema; it fetches it in the background. Until the schema arrives, its answers can come without records, and `generate_query` reports `records` as `no_schema`.
- After its first fetch fails, it starts another background fetch only when it finishes generating a query, so it starts none while no questions come in. These fetches start at most once every five minutes, or once every `refresh_seconds` seconds when you pass a number as `refresh_seconds` to `set_schema_introspector`. Until a fetch succeeds, its answers lack records.

`GenerativeStrategy` and `AgentStrategy` fetch the schema while they answer their first question, even when you set address lookups yourself. After a failed fetch, they try again on their next question.

- `GenerativeStrategy` fetches it before it writes its first query when `address_search` is `auto`, and otherwise when it checks that query. Until a fetch succeeds, its answers lack records.
- `AgentStrategy` fetches it before its agent starts. Until a fetch succeeds, it raises `PROVIDER_ERROR`.

## Environment Variables

| Variable | Used by | Meaning |
|---|---|---|
| `ENIGMA_API_KEY` | `ServiceStrategy` | The key of a `ServiceStrategy` you create without `enigma_api_key=` (the default strategy uses the `ClientConfig` key) |
| `ANTHROPIC_API_KEY` | `AnthropicProvider` | Anthropic API key |
| `OPENROUTER_API_KEY` | `OpenRouterProvider` | OpenRouter API key |
| `AWS_REGION`, `AWS_DEFAULT_REGION` | `BedrockProvider`, `AgentStrategy` | Region when `region=` is not passed: `AWS_REGION`, else `AWS_DEFAULT_REGION`, else `us-east-1` |
| `ENIGMA_SHAPE_CACHE_ENABLED` | `TemplateStrategy`, `GenerativeStrategy` | `0`, `false`, `no`, or `off` turns off the query shape cache. Read when the strategy is created. |
| `ENIGMA_ADDRESS_SEARCH` | `TemplateStrategy`, `GenerativeStrategy`, `AgentStrategy` | `on`, `off`, or `auto` (see Turn Address Lookups On or Off). `1`, `true`, and `yes` also mean `on`; `0`, `false`, and `no` mean `off`. Read when the strategy is created. |
| `ENIGMA_ARCHIVE_SEARCH` | `TemplateStrategy`, `GenerativeStrategy`, `AgentStrategy` | `off`, `0`, `false`, or `no` leaves the archive searches out of record queries (see Get Government Records). Read on each call. |

## Logging

The SDK logs through Python's `logging` module, on loggers named `enigma_api_client.*`:

- Lines at INFO and above are fixed messages with counts, statuses, names of the query rules the SDK checks, exception class names, schema field names, and file paths. They never contain your questions or the LLM's text.
- DEBUG lines can contain your questions, the LLM's output, and `AgentStrategy`'s transcript. Don't turn on DEBUG for these loggers where your logs must not hold that data.

## What Leaves Your Machine

- **Searches**: the query and its variables go to the Enigma API with your Enigma API key.
- **`ServiceStrategy`**: your question goes to Enigma's hosted query service with your Enigma API key. The service writes the query with an LLM. It has its own URL, which `ClientConfig.url` doesn't change.
- **Local strategies**: your question and Enigma's query instructions go to the LLM provider you configure (Amazon Bedrock for `AgentStrategy`). When the SDK asks the LLM to repair a query, it also sends the draft query and its errors. Search results never go to the LLM.
- **Schema checks**: the SDK fetches the API schema from the Enigma API with the first `Client`'s key, or with the keys used by the introspector you register (see Schema Checks and Several Tiers).

The SDK sends nothing else.

## Write GraphQL by Hand

The Enigma schema has hundreds of fields, several of them named differently from what you might guess. A query can also pass schema validation and still be rejected when it runs. This section lists what most often goes wrong.

### Common Mistakes

| Field you might guess | Actual field |
|---|---|
| `status` (of a location) | `operatingStatus` |
| `url` | `website` |
| `description` | `industryDesc` |
| `title` | `jobTitle` |
| `averageRating` | `reviewScoreAvg` |

- **A field that doesn't exist fails the whole request** with HTTP 400 and `Cannot query field`.
- **`search` returns a list.** Select through inline fragments, as in `search(...) { ... on Brand { ... } }`; it has no `edges`.
- **A category in `name` matches business names.** `name: "coffee shop"` returns businesses with names like "Coffee Shop". To count a category, use `aggregate`.
- **A company's officers are under its registrations.** `LegalEntity.persons` is almost always empty for a company. Follow `registeredEntities`, then `registrations`, then `roles`, then `legalEntities`.
- **Connections have no `totalCount`.** Count with an `aggregate` query, or with `count(field: ...)` on an entity.
- **Website content sits under each website.** Select `websiteContents` inside `websites`: `websites(first: 1) { edges { node { websiteContents(first: 1) { edges { node { httpStatusCode } } } } } }`.
- **A brand search for a local franchise returns the whole chain.** Searching `BRAND` for a single franchised hotel returns the parent brand with all its locations. Search `OPERATING_LOCATION` with an address to find one location.

### Rules the API Enforces at Execution

These queries pass schema validation and are rejected when they run. `GenerativeStrategy` and `AgentStrategy` check these rules before they return a query, and the templates follow them.

| Rule | Error |
|---|---|
| `search` needs `name`, `website`, `person`, `phoneNumber`, `id`, or `tin`, even when it has a filter. An address alone works only in an `entityType: ADDRESS` search with `address.street1` (a street line with a house number) or `address.id`. | `Search must include name, website, person, or phone_number` |
| `searchInput.prompt` can't be combined with a selection set. To have the API answer a question in plain English, send the question in `searchInput.prompt` with no selection set (see [API search](https://documentation.enigma.com/guides/graphql/api-search)). | `'prompt' cannot be combined with a selection set` |
| `roles` takes `first` and `conditions` only, although the schema lists `last`, `after`, and `before`. Fetch roles as one page. | `LegalEntity.roles does not currently support last, after or before` (the error names the parent type) |
| A `PERSON` search finds people by `person: { firstName, lastName }`, `id`, or `tin`, not by `name`. | `Person search must include an identifier or first, middle or last name` |
| A filter's `AND`, `OR`, and `NOT` take a list of clauses, and `NOT` takes exactly one. | Rejected |
| A filter clause has exactly one operator. Combine several clauses in one `AND` list. | Rejected |
| `IS_NULL` and `IS_NOT_NULL` test a column. To test for related records on a connection path such as `registrations`, use `HAS` or `NOT: [{ HAS: [...] }]`. | Rejected |
| An `aggregate` filter on `brands.industries.industryCode` needs `{ EQ: ["brands.industries.industryType", "naics_2022_code"] }` in the same `AND`. | `Joins aren't allowed for filter` |

## Upgrade Notes

Each section lists what changed after the version it names. Read every section that names your current version or a later one.

### Upgrading from 0.12.1

For `AccessTier.REDISTRIBUTION`, a local strategy no longer selects `emailAddresses` or `phoneNumbers` under a role (see Access Tiers), so code that reads them from a generated query's results raises `KeyError`. The API answers a redistribution key with placeholder values for both. A generated query that selects nothing else is refused with `UNSUPPORTED_DATA_REQUESTED`. On every tier, `TemplateStrategy`'s contacts lookup now also returns the address and phone number of the location whose roles it lists. With `Client`'s default strategy, this reaches you when the hosted service adopts this release.

### Upgrading from 0.11.0

With `Client`'s default strategy, the hosted service builds the query. The `TemplateStrategy` and `GenerativeStrategy` changes in this section reach you when the service adopts this release, whatever SDK version you run.

- `TemplateStrategy` answers a question that names several businesses, people, or street addresses with several searches in the same query, under `search`, `search2`, and `search3` (see Generate Queries from Questions). Before, it answered one of them or refused the question.
- `GenerativeStrategy` likewise searches each brand a question names, under `search`, `search2`, and `search3`. Before, it usually searched only the first brand, and otherwise keyed the searches with other aliases, such as `a` and `b`.
- `GenerativeStrategy` counts each brand a how-many question names, under `aggregate`, `aggregate2`, and so on. Before, it counted only the first brand.
- Code that reads only `data["search"]` gets the first search. Read the other keys for the rest.
- A generated query with more than three top-level fields (searches or counts) is refused. The government-record searches the SDK adds beside them don't count.
  - A local strategy raises `UNSUPPORTED_DATA_REQUESTED`.
  - `ServiceStrategy` raises `INVALID_QUERY` once the hosted service adopts this release.
- `GenerativeStrategy` and `AgentStrategy` refuse a query when any one of its searches is a kind they refuse (see GenerativeStrategy). Before, they refused it only when every top-level field in it was such a search.
- When the SDK repairs a `GenerativeStrategy` or `AgentStrategy` query that repeats a top-level field with different arguments, it keys the second one `search2` (or `aggregate2`). Before, it keyed it `search_2` (or `aggregate_2`).
- Templates return a brand's affiliated brands 50 to a page, up from 10. `GenerativeStrategy` asks its LLM for the same page size.
- A template legal entity lookup lists a business's registrations in each state the question gives that business (see How Template Queries Read Places). Before, it listed none when the question gave several states.
- In a lookup of one location of a business ("the Starbucks at 1 Main St") or of an address, `TemplateStrategy` searches each street, city, and state the question lists, and, in an address lookup, each unit and ZIP code. Before, it sent the list in a single field, dropped it, or refused the question. A question about a brand's locations in several cities still gets one search covering them all.
- How Template Queries Read Places describes how the places a question lists apply to several businesses or people, and when a list of places is refused.

### Upgrading from 0.10.3

- Address lookups no longer select the five `Address` count fields (`associatedBrandCount`, `operatingLocationOperatesAtCount`, `legalEntityReceivesMailAtCount`, `registrationRecordedCount`, and `associatedPersonCount`).
  - An address result no longer carries those keys, so code that reads `address["associatedBrandCount"]` raises `KeyError`. Read the operating locations, legal entities, and registrations each address lists instead.
  - With `Client`'s default strategy, the hosted service builds the query, so this change reaches you when the service adopts the release, whatever SDK version you run.
- Two kinds of schema now count as supporting address lookups, both for `address_search="auto"` and for the address-search refusal described in Turn Address Lookups On or Off:
  - One whose `Address` type carries `associatedBrandCount`, as before.
  - One that defines an `Address` type and the `Node` interface. This kind is new.
- When a question asks how many, other than a headcount, and the query is an address lookup that selects no count, `unavailable_data` has an `address counts` entry (`out_of_scope`).

### Upgrading from 0.10.2

- `SearchResponse.headers` holds the HTTP response headers, so you can read `Retry-After` on a 429. It takes no part in `==` or `repr`. `dataclasses.asdict(response)` and `dataclasses.astuple(response)` now include it as an `httpx.Headers`, which `json.dumps` can't serialize.
- A response body that isn't a JSON object, such as a gateway's error page, no longer raises `ValueError` (or `AttributeError` for JSON that isn't an object). `client.search` returns it with `data` set to `None`, an error in `errors`, and the body in `raw_body`, so `is_success()` is false. `ServiceStrategy` raises `EnigmaClientException` with `INVALID_QUERY` for one, and `introspect_schema` raises its "no schema data" `RuntimeError`.
- A query written with no space after an opening brace, such as `node{name}`, is sent correctly. The client used to join the `id` and `__typename` it adds to the first field, and the API rejected the query.
- `EnigmaClientException.status_code` holds the HTTP status of the failed LLM provider or hosted-service call behind an error, when there was one, so retry logic needs no message parsing.
- For the hosted service, `EnigmaClientException.headers` holds its response headers.
- When the SDK maps a provider's exception to `EnigmaClientException`, that exception is its `__cause__`.
- The error code for each HTTP status is unchanged.
- An API key or header value an HTTP header can't carry as written, such as one with a newline or a trailing space, raises `ValueError` naming the header, before the API request is sent. It replaces two errors, so the handlers that caught them no longer catch this case:
  - The httpx error that could quote the value, which `except httpx.HTTPError` caught.
  - For a character outside ASCII, the `UnicodeEncodeError`, which `except UnicodeEncodeError` caught.
- `introspect_schema`, which sent an API key outside ASCII encoded as UTF-8, now refuses it the same way.
- A custom `LlmProvider` can raise `ProviderHTTPError`, now exported from `enigma_api_client.strategies`, with the HTTP status of a failure. 408, 429, and 5xx are reported as `TRANSIENT_PROVIDER_ERROR`, any other HTTP status as `PROVIDER_ERROR`, and a value that is not an HTTP status as `TRANSIENT_PROVIDER_ERROR`.

### Upgrading from 0.10.1

- `Client.search` asks the API to wait up to 25 seconds for values it computes in the background, such as some counts, before it answers. The API answers a request with no background work at once. A `ClientConfig.url` that sets `max_background_task_wait` or `max_call_duration` to a non-blank value keeps its own wait.
- `SearchResponse.is_success()` is false for an HTTP 206 answer, whose background values are still null. It is also false for an answer that reports a background task that failed, was canceled, or ran out of credits.
- `TemplateStrategy`'s brand watchlist template selects each watchlist entry's `id` in place of `internalWatchlistEntryId`, which the API is retiring. The two identifiers differ, so ids you stored from `internalWatchlistEntryId` won't match.

### Upgrading from 0.10.0

- An `unavailable_data` entry for something the selected government records answer only in part, such as "tax liens", gets the message "The returned records cover this only in part." when the query selects those records.
- The phrases "work permit", "export permit", "licensing agreement", and "drivers' license" no longer ask for government records.

### Upgrading from 0.9.14

- With `TemplateStrategy`, `GenerativeStrategy`, or `AgentStrategy`, a question that asks for an entity's government or public records returns a query that selects them. Each returned entity carries the fields listed under Records on Each Entity. Beside a search by a name, person, or street address, the response gains `archiveRecords`. Beside a name search with `entityType: BRAND`, `entityType: OPERATING_LOCATION`, or no `entityType`, it also gains `archiveBrandRecords`. A question that asks for no records returns the same query as in 0.9.14.
- `unavailable_data` leaves out an entry the selected records answer in full, such as "liquor licenses" or "violations".
- When the SDK adds record counts but none of the records the question asks for, each entry those records would answer in full stays in `unavailable_data` and carries the message "This query does not return these records."
- When the SDK adds no record fields, a `violations` entry says that violation records are served as the Gov Archive records of a business, location, person, or address.

### Upgrading from 0.9.13

- The query reference `AgentStrategy` reads no longer lists `internalId`, which the API is retiring on every type.

### Upgrading from 0.9.12

- Generated queries select `pageInfo { hasNextPage endCursor }` on `Brand.affiliatedBrands` and `LegalEntity.legalEntities`, so a corporate family longer than one page can be paged with `after:`.

### Upgrading from 0.9.11

- `TemplateStrategy` lookups return a core set of fields plus only the data the question asks for (see TemplateStrategy). A plain "verify" question no longer returns TINs, watchlist entries, bankruptcies, or officers; ask for them, or ask a compliance question.
- `TemplateStrategy`'s legal entity lookup searches by name only, and reads a state in the question as the registrations to list in that state.
- `TemplateStrategy` can refuse a question about several businesses or people at once with `UNSUPPORTED_DATA_REQUESTED`. From 0.12.0 it answers up to three at once (see Upgrading from 0.11.0).
- With `TemplateStrategy`, a question with no usable name, website, or person name raises `UNSUPPORTED_DATA_REQUESTED` instead of being sent as an empty search.
- `TemplateStrategy`'s brand profiles select `operatingLocations` in place of the `hqAddress` alias, so the response key changes, and no longer select `isMarketables`.
- `TemplateStrategy` answers an officers question by searching legal entities when the name has more than one word, ends in Inc, Incorporated, Co, Company, LLC, LP, LLP, LLLP, or PLLC, and comes with no website. `search` then returns a `LegalEntity` whose officers are under `registeredEntities`, `registrations`, and `roles`. Other names, including names ending in Corp, Corporation, or Ltd, still search brands.

### Upgrading from 0.9.10

- `restricted_fields_for` and `gate_blocked_fields` also list `placeClasses`, `siteTypes`, and `tiers` for the `EXTERNAL` and `REDISTRIBUTION` tiers, and `legalEntitiesAppearsOn` and `legalEntitiesIsFlaggedBy` for `REDISTRIBUTION`; the API does not serve them to those tiers.
- `TemplateStrategy` answers a question about how many businesses of a category are in a place with a count instead of refusing it.

### Upgrading from 0.9.9

- `restricted_fields_for` and `gate_blocked_fields` no longer list `headcounts` for the `EXTERNAL` and `REDISTRIBUTION` tiers; the API serves it to every tier.
- A headcount question on a brand or legal entity now selects each legal entity's headcount band instead of declaring headcount in `unavailable_data`.
- For a headcount question whose query is not a company or legal entity lookup, `TemplateStrategy` and `GenerativeStrategy` still declare `employees` (`out_of_scope`). `TemplateStrategy`'s entry has a new message.

### Upgrading from 0.9.8

- The SDK also calls a schema introspector, the function registered with `enigma_api_client.validation.set_schema_introspector` (including the one `Client` registers), from a background thread with no context variables set. It does so:
  - When a strategy needs a tier's schema and none is cached yet.
  - On the first use of a cached schema that is more than five minutes old.
- The introspector must be thread-safe, return in bounded time, and not rely on context variables.
- `set_schema_introspector(introspector, refresh_seconds=None)` turns off the refresh of cached schemas. A missing schema is still fetched in the background. The introspector `Client` registers uses the five-minute interval.
- A local strategy refuses an address search, whatever `address_search` says, while the cached schema for the access tier shows no support for address lookups. The refusal raises `UNSUPPORTED_DATA_REQUESTED` with `bare address lookup` in its message. From 0.9.9 through 0.10.3, such a schema is one that lacks `Address.associatedBrandCount`. For 0.11.0 and later, see Upgrading from 0.10.3.
- While no schema for the access tier is cached yet, `TemplateStrategy` and `GenerativeStrategy` with address lookups turned on (`address_search=True` or `ENIGMA_ADDRESS_SEARCH=on`) still write address lookups.
- In addition to the `restricted_fields_for` fields, `TemplateStrategy` output and answers from the query shape cache drop any field your tier's schema doesn't define, and declare each in `unavailable_data` as `contractual`.

### Upgrading from 0.9.7

- `restricted_fields_for` and `gate_blocked_fields` list `tins`, `watchlistEntries`, `isFlaggedByWatchlistEntries`, and `appearsOnWatchlistEntries` for the `REDISTRIBUTION` tier; the API no longer serves them to it.
- `GenerativeStrategy`, `TemplateStrategy`, and `AgentStrategy` drop from the query they return each field the tier is not served, declare it in `unavailable_data` (`contractual`, with `field` set to the field's schema name), and add `restricted-field` to `deterministic_repair`. A query left with nothing else, or whose filter, `orderBy`, or `field` argument path still names such a field, raises `UNSUPPORTED_DATA_REQUESTED`.
- `ClientConfig.tier` (default `AccessTier.REDISTRIBUTION`) is the tier a local strategy generates for and validates against. Set it to your key's tier to keep TIN and watchlist selections with an external key. `ServiceStrategy` ignores it.
- Schema validation is process-wide and follows the first `Client`'s tier. A later `Client` at another tier validates without the schema, and its `AgentStrategy` raises `PROVIDER_ERROR`.
- `Client.search` passes `tier=` to `generate_query` when `ClientConfig.tier` is not the default, so a custom `QueryStrategy` must accept a `tier` keyword argument.

### Upgrading from 0.9.1

- `__url` links point at the Enigma Console's profile pages: `https://console.enigma.com/profile/brand/<id>`, `/profile/legal-entity/<id>`, and `/profile/operating-location/<id>`. Old links still redirect. A person's `__url` links to the brand it appears under, or to Enigma's website when it appears under none, instead of a person profile.

### Upgrading from 0.9.0

- `TemplateStrategy` counts a brand's locations ("How many Starbucks are in Texas?") with a brand search, so the count moves from `data.aggregate.locationsCount` to `data.search[0].locationsCount`. `brandsCount` is no longer returned, and `search` is empty when no brand matches. The count covers only that brand's open locations in the state and city the question names; before, it counted every open location, whatever the brand.

### Upgrading from 0.8.x

- `AgentStrategy` runs on Amazon Bedrock only and takes the keyword arguments `model`, `region`, `max_turns`, and `max_budget_usd`; `anthropic_api_key` is removed. The calling identity needs `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream` on the model.
- `AgentStrategy` raises instead of returning an empty query:
  - `UnrepairedQueryError` when its answer fails schema validation or does not parse.
  - `UNSUPPORTED_DATA_REQUESTED` from the refusal checks `GenerativeStrategy` applies.
  - `INVALID_QUERY` for:
    - An empty prompt or answer (the message starts `Query is not applicable to the Enigma API`).
    - A run that ends without a query.
    - An answer that doesn't match the agent's answer format.
    - A mutation or subscription.
    - An answer whose variables don't fit their declared types.
    - An answer nested too deeply.
    - An answer the SDK couldn't check.
    - An answer over 10,000 characters.
  - `TRANSIENT_PROVIDER_ERROR` or `PROVIDER_ERROR` when the model call or the Claude Code CLI the agent runs fails.
- `AgentStrategy` records its token usage in `GENERATION_USAGE`.
- The `anthropic` and `all` extras pin an exact `claude_agent_sdk` version (currently 0.2.121).
- `AgentStrategy` writes every value into the query text and returns `{}` as its variables. When the tier has no schema, it refuses the call with `PROVIDER_ERROR` before the agent runs. It takes `address_search=` like `GenerativeStrategy`.

### Upgrading from 0.7.x or Earlier

- Custom `LlmProvider`: `complete()` now returns `enigma_api_client.strategies.Completion(text, truncated, input_tokens=..., cache_read_tokens=..., cache_write_tokens=..., output_tokens=...)` instead of `str`.
- Custom `QueryStrategy`: `generate_query(prompt, *, tier=DEFAULT_TIER, prior_attempt=None)` gained `prior_attempt`. Implementations must set `GENERATION_USAGE` to a fresh `LlmUsage()` on entry.
- `GenerativeStrategy` raises `TruncatedOutputError` and `UnrepairedQueryError`. Both subclass `EnigmaClientException` with code `INVALID_QUERY`, so existing handlers still catch them.
- LLM provider transport failures now raise `EnigmaClientException`: `TRANSIENT_PROVIDER_ERROR` for timeouts and connection errors, `GENERATE_QUERY_CALLBACK_NOT_CONFIGURED` for configuration faults. They used to surface as raw `httpx` exceptions.
- `restricted_fields_for` and `gate_blocked_fields` no longer list `isMentionedInMediaContents`, and now list `headcounts`, `corporateFamilies`, and `customs` for the `EXTERNAL` and `REDISTRIBUTION` tiers.
- Address lookups are new and default to `auto` (see Look Up an Address).
- With address lookups off (for example, `ENIGMA_ADDRESS_SEARCH=off`), `TemplateStrategy` usually refuses a question that gives only an address, raising `UNSUPPORTED_DATA_REQUESTED` or `INVALID_QUERY` (see Turn Address Lookups On or Off). Before 0.8.0, it could send such a question as a nameless operating location search, which the API rejected with HTTP 400.
- `GenerativeStrategy` now refuses some queries its LLM writes with `UNSUPPORTED_DATA_REQUESTED` instead of serving them: a person search by surname alone, an EIN or filing number placed in `name`, a wildcard name pattern, a street address placed in `name`, and a category count written for a question that asks for a list.
- `AgentStrategy` logs its transcript at DEBUG on `enigma_api_client.strategies.agent`, not at INFO on the root logger.
- `TemplateStrategy` refuses a blank prompt with `INVALID_QUERY` before any LLM call.
