> For the complete documentation index, see [llms.txt](https://docs.kernel.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kernel.ai/developer/api-getting-started.md).

# API

<a href="https://dev.kernel.ai" class="button primary" data-icon="brackets-curly">Open API reference</a>

<a href="https://app.kernel.ai/" class="button primary" data-icon="key">Kernel customers - create API key</a>

<a href="https://kernel.ai/kernel-api" class="button primary" data-icon="key-skeleton">Non-Kernel customers - request API key</a>

## Prerequisities

* A Kernel API key
  * Copy the key value and the webhook signing secret
  * Store the key, secret and base URL in your .env file
    * `KERNEL_API_KEY="your-key-here"`
    * `KERNEL_WEBHOOK_SECRET="your-webhook-secret-here"`
    * `KERNEL_API_BASE_URL=`[`https://api.kernel.ai/rest`](https://api.kernel.ai/rest)
* A connection to your system of record (i.e., CRM or data warehouse)

## Before you start

* The Kernel APIs retrieve and reconcile company information from a range of public sources, and return it with a confidence level and the reasoning behind each value. They run asynchronously: a request returns a job you poll, so results are not immediate. Most jobs take a few minutes, with firmographics and combined jobs being the slowest

## Getting started prompts

Use these to run the three main Kernel APIs, step by step.

{% tabs %}
{% tab title="Entity resolution" %}
Turn a messy or partial account record into a verified, canonical identity you can trust.

<a href="https://dev.kernel.ai/api-reference/endpoint/create-entity-resolution" class="button primary" data-icon="brackets-curly">Entity resolution API reference</a>

{% prompt description=" Example entity resolution prompt" icon="screwdriver-wrench" %}

````markdown
# Resolve a record to its KERN ID

You have access to the user's system of record (for example their CRM) and to the Kernel API
(`KERNEL_API_KEY` is already configured). When the user points you at a record, resolve it to one
canonical **KERN ID** and return its core identity. The KERN ID is the foundation you build on for
enrichment, hierarchy, and other Kernel use cases.

## When to use it

The user references a record by whatever identifier they have (a CRM ID, for example) and wants it
resolved to a single verified company identity with a stable KERN ID.

## What to do

1. Look up the referenced record in the connected system of record and read its identifying fields:
   legal or company name, website, country, plus any city, state, postal code, address, email, or
   LinkedIn URL.
2. Call Kernel entity resolution with those fields. Set `external_id` to the record's own id (its CRM
   id) so the result maps straight back to it.
3. Return the KERN ID and the resolved identity, and store the KERN ID on the record.

## Inputs

Send whatever the record gives you; `website` is the strongest signal.

`legal_name`, `trading_name`, `website`, `country`, `city`, `state`, `postal_code`, `address`,
`email`, `linkedin_url`, `match_to_linkedin` (bool), `identity_bias` (`URL_BIAS` default, or
`NAME_BIAS`), `external_id` (the record's own id, echoed back).

## Call it (curl)

```bash
curl -s -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "legal_name": "Stripe, Inc.",
    "trading_name": "Stripe",
    "website": "https://stripe.com",
    "country": "US",
    "city": "South San Francisco",
    "state": "CA",
    "postal_code": "94070",
    "email": "info@stripe.com",
    "match_to_linkedin": true,
    "identity_bias": "NAME_BIAS",
    "external_id": "stripe-001"
  }'
# returns {"id":"<job_id>","status":"processing"}

curl -s https://api.kernel.ai/rest/v1/entity-resolution/<job_id> -H "x-api-key: $KERNEL_API_KEY"
# poll until status is "completed"
```

## Call it (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

def resolve(**signals):
    job_id = requests.post(f"{BASE}/v1/entity-resolution", headers=HEADERS, json=signals).json()["id"]
    delay = 2
    while True:                                    # resolution takes about 1 to 2 minutes
        time.sleep(delay)
        data = requests.get(f"{BASE}/v1/entity-resolution/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data
        delay = min(delay + 2, 30)

# 1) Read the record from your connected system of record by its id.
record = get_record("stripe-001")                  # your system's own lookup

# 2) Resolve it; pass the record's id as external_id so the result maps back.
result = resolve(
    legal_name=record["legal_name"],
    website=record["website"],
    country=record["country"],
    external_id=record["id"],
)
print(result["record"]["kernel_id"])               # the KERN ID, your foundation
```

## What you get back

In `record`:
- `kernel_id`: the canonical KERN ID. Store it on the record; it is the key for enrichment, hierarchy,
  and other use cases.
- `legal_info`: `legal_name`, `country`, `website`, `confidence`, `reasoning`.
- `trading_info`: `trading_name`.
- `entity_classification`: `type` and `subtype` (for example Company / Operating).

## Notes

- Pass as many of the record's fields as you have. `website` is the strongest signal; add `country` to
  separate same named companies in different regions.
- `identity_bias` defaults to `URL_BIAS` (trust the website most). Use `NAME_BIAS` when the name is
  more reliable than the URL.
- Resolution usually takes 1 to 2 minutes. For long jobs, pass `webhook_url` and Kernel calls you back
  instead of polling.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity. Use when you need a canonical ID.
- **Enrich firmographics**: KERN ID to revenue, headcount, location, status. Use when you need company attributes.
- **Enrich hierarchy**: KERN ID to parent and ultimate parent. Use when you need the corporate tree.

````

{% endprompt %}
{% endtab %}

{% tab title="Enrich with firmographics" %}
Get up-to-date revenue, headcount, and location for a company, each backed up by reasoning.

<a href="https://dev.kernel.ai/api-reference/endpoint/get-firmographics" class="button primary" data-icon="brackets-curly">Enrich with firmographics API reference</a>

{% prompt description="Example enrich with firmographics prompt" icon="dollar-sign" %}

````markdown
# Get a record's firmographics

You have access to the user's system of record and a record that already carries a Kernel `kernel_id`
(the Kernel API key is already configured). Return the firmographic data the user asks for.

## When to use it

The user wants firmographic facts about a company they have already resolved: revenue, headcount,
location, or operating status.

**Requires entity resolution first.** Kernel is keyed on the KERN ID: this accepts only a `kernel_id`,
never a company name or domain. The only way to get a `kernel_id` is to resolve the company (see the
Resolve prompt), so resolve first. Without a `kernel_id`, this does not run.

## First, ask what they want

Kernel returns four firmographic data points. Ask the user which they want, and offer the choice of
one, a combination, or all of them:

- **revenue** (entity and consolidated group figures)
- **headcount**
- **location** (operating and registered address)
- **operating status** (active, absorbed, and so on)

Then return only the data points they picked.

## Inputs

- `kernel_id` (required): the KERN ID stored on the record.
- `webhook_url` (optional): get a callback instead of polling.

## Call it (curl)

```bash
curl -s -X POST https://api.kernel.ai/rest/v1/firmographics \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" \
  -d '{"kernel_id":"<KERNEL_ID>"}'
# returns {"id":"<job_id>","status":"processing"}

curl -s https://api.kernel.ai/rest/v1/firmographics/<job_id> -H "x-api-key: $KERNEL_API_KEY"
# poll until status is "completed"
```

## Call it (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

# Maps the choices you offer the user to the fields in the response.
FIELDS = {"revenue": "revenue", "headcount": "headcount",
          "location": "location", "operating status": "op_status"}

def firmographics(kernel_id, wanted="all"):
    job_id = requests.post(f"{BASE}/v1/firmographics", headers=HEADERS,
                           json={"kernel_id": kernel_id}).json()["id"]
    delay = 2
    while True:                                     # firmographics can take 3 to 4 minutes
        time.sleep(delay)
        data = requests.get(f"{BASE}/v1/firmographics/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            break
        delay = min(delay + 5, 30)

    record = data.get("record", {})
    if wanted == "all":
        return record
    # wanted is a list of the user's choices, e.g. ["revenue", "headcount"]
    return {choice: record.get(FIELDS[choice]) for choice in wanted}

# Ask the user first, then pass their choice:
print(firmographics("<KERNEL_ID>", wanted=["revenue", "headcount"]))   # a combination
print(firmographics("<KERNEL_ID>", wanted="all"))                       # everything
```

## What you get back

The full firmographics record holds all four data points; return only the ones the user chose. Every
data point carries `reasoning`, and headcount and revenue carry a `confidence`.

- `op_status`: `operational_status` (for example Active or Absorbed), `reasoning`.
- `location`: `operating` and `registered`, each with `street`, `city`, `state`, `postcode`,
  `country`, `reasoning`.
- `headcount`: `count`, plus `count_entity` (this entity alone) and `count_consolidated` (whole group),
  `confidence`, `reasoning`.
- `revenue`: `usd` and `local` (with `local_currency`), each split into `*_entity` (this entity) and
  `consolidated_*` (whole group), plus `confidence`, `source`, `reasoning`.

### Example response

```json
{
  "kernel_id": "7944166432",
  "op_status": {
    "operational_status": "Active",
    "reasoning": "Entity is actively operating with no signs of cessation or absorption."
  },
  "location": {
    "operating": {
      "street": null, "city": "Singrauli", "state": "Madhya Pradesh",
      "country": "India", "postcode": "486889",
      "reasoning": "Address derived from company registration records."
    },
    "registered": {
      "street": "P.O. Singrauli Colliery", "city": "Singrauli", "state": "Madhya Pradesh",
      "country": "India", "postcode": "486889",
      "reasoning": "Address from official registry filing."
    }
  },
  "headcount": {
    "count": 13307, "count_entity": 13307, "count_consolidated": 13307,
    "confidence": "HIGH", "reasoning": "Headcount derived from annual report figures."
  },
  "revenue": {
    "usd": 2613846573, "usd_entity": 2613846573, "consolidated_usd": 2613846573,
    "local": 217820547750, "local_entity": 217820547750, "consolidated_local": 217820547750,
    "local_currency": "INR", "local_currency_entity": "INR", "consolidated_currency": "INR",
    "confidence": "HIGH", "source": "identified",
    "reasoning": "Revenue sourced from audited annual financial statements."
  }
}
```

## Notes

- Firmographics is slow (often 3 to 4 minutes). Use a longer poll interval, or pass `webhook_url` and
  skip polling.
- Revenue and headcount each come at two scopes: this entity alone (`usd_entity` / `count_entity`) and
  the whole group (`consolidated_usd` / `count_consolidated`). For "how big is this company," use the
  consolidated figure. For a single-entity company the two are equal.
- Each field carries `reasoning` that cites its sources. Surface it when the user wants provenance.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity. Use when you need a canonical ID.
- **Enrich hierarchy**: KERN ID to parent and ultimate parent. Use when you need the corporate tree.
- **Enrich (both)**: KERN ID to hierarchy and firmographics together. Use when you want to fill a whole record.

````

{% endprompt %}
{% endtab %}

{% tab title="Enrich with parent hierarchies" %}
See the current corporate ownership of an account, from its direct parent, all the way up the hierarchy.

<a href="https://dev.kernel.ai/api-reference/endpoint/resolve-parent" class="button primary" data-icon="brackets-curly">Enrich with parent hierarchy API reference</a>

{% prompt description="ExampleEnrich with parent hierarchies prompt" icon="family-dress" %}

````markdown
# Get a record's corporate hierarchy

You have access to the user's system of record and a record that already carries a Kernel `kernel_id`
(the Kernel API key is already configured). Return the company's parent and ultimate parent.

## When to use it

The user wants to know how a company they have already resolved sits in its corporate tree: who owns
it, what the ultimate (top) parent is, and whether it is a regional subsidiary.

**Requires entity resolution first.** Kernel is keyed on the KERN ID: this accepts only a `kernel_id`,
never a company name or domain. The only way to get a `kernel_id` is to resolve the company (see the
Resolve prompt), so resolve first. Without a `kernel_id`, this does not run.

## Inputs

- `kernel_id` (required): the KERN ID stored on the record.
- `webhook_url` (optional): get a callback instead of polling.

## Call it (curl)

```bash
curl -s -X POST https://api.kernel.ai/rest/v1/resolve-parent \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" \
  -d '{"kernel_id":"<KERNEL_ID>"}'
# returns {"id":"<job_id>","status":"processing"}

curl -s https://api.kernel.ai/rest/v1/resolve-parent/<job_id> -H "x-api-key: $KERNEL_API_KEY"
# poll until status is "completed"
```

## Call it (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

def hierarchy(kernel_id):
    job_id = requests.post(f"{BASE}/v1/resolve-parent", headers=HEADERS,
                           json={"kernel_id": kernel_id}).json()["id"]
    delay = 2
    while True:                                     # usually a minute or two
        time.sleep(delay)
        data = requests.get(f"{BASE}/v1/resolve-parent/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay + 2, 30)

rec = hierarchy("<KERNEL_ID>")
print(rec.get("parent"))                 # immediate parent, or None
print(rec.get("top_parent"))             # ultimate parent
print(rec.get("top_operating_parent"))   # highest operating company (skips holding companies)
print(rec.get("regional_subsidiary"))    # is_regional, regional_scope
```

## What you get back

In `record`:
- `parent`: the immediate parent, or `null` if it has none.
- `top_parent`: the ultimate parent at the top of the tree (its own `kernel_id`, legal name, country).
- `top_operating_parent`: the highest operating company in the tree, skipping pure holding companies
  and investment vehicles. Conditionally present, and usually the best entity to roll accounts up to.
- `regional_subsidiary`: `is_regional` and `regional_scope`.

These three parent fields can point to the same entity (as in the example below, where LinkedIn's
parent Microsoft is also the top and the top operating company) or differ: for a company under a
holding company, `top_parent` may be the holding company while `top_operating_parent` is the operating
company beneath it.

### Example response

Resolving LinkedIn, then `resolve-parent` on its `kernel_id`:

```json
{
  "kernel_id": "9331102168",
  "parent": {
    "kernel_id": "5034871910",
    "legal_name": "Microsoft Corporation",
    "trading_name": "Microsoft",
    "website": "microsoft.com",
    "country": "US",
    "entity_category": "Company",
    "entity_sub_category": "Operating",
    "confidence": "HIGH",
    "reasoning": "Microsoft Corporation is the acquirer and parent of LinkedIn Corporation (Microsoft acquisition announcement; Reuters)."
  },
  "top_parent": {
    "kernel_id": "5034871910",
    "legal_name": "Microsoft Corporation",
    "trading_name": "Microsoft",
    "website": "microsoft.com",
    "country": "US",
    "entity_category": "Company",
    "entity_sub_category": "Operating",
    "confidence": "HIGH"
  },
  "top_operating_parent": {
    "kernel_id": "5034871910",
    "legal_name": "Microsoft Corporation",
    "trading_name": "Microsoft",
    "website": "microsoft.com",
    "country": "US",
    "entity_category": "Company",
    "entity_sub_category": "Operating",
    "confidence": "HIGH"
  },
  "regional_subsidiary": {
    "is_regional": false,
    "regional_scope": null,
    "reasoning": "LinkedIn operates under its own global brand, not as a single-country arm of Microsoft."
  }
}
```

## Notes

- A company can be its own `top_parent`, in which case `parent` is `null`. That is expected.
- Hierarchy is async; poll until completed (usually a minute or two), or pass `webhook_url`.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity. Use when you need a canonical ID.
- **Enrich firmographics**: KERN ID to revenue, headcount, location, status. Use when you need company attributes.
- **Enrich (both)**: KERN ID to hierarchy and firmographics together. Use when you want to fill a whole record.

````

{% endprompt %}
{% endtab %}

{% tab title="Resolve & Enrich" %}
Resolve first, then enrich with firmographics or hierarchies (or both) in a single API call.

<a href="https://dev.kernel.ai/api-reference/endpoint/combined#resolve-then-enrich-firmographics" class="button primary" data-icon="brackets-curly">Resolve and enrich API reference</a>

{% prompt description="Resolve and enrich with firmographics" icon="1" %}

````markdown
# Resolve a record and get its firmographics in one call

You have access to the user's system of record (for example their CRM) and the Kernel API
(`KERNEL_API_KEY` is already configured). Given a company's details, Kernel's combined endpoint resolves
the company and returns its firmographics (revenue, headcount, location, operating status) in a single
call. Use it when you want a company's identity and its core facts, without chaining Resolve then
Firmographics.

This resolves the company first, then runs the requested job; everything is keyed on the KERN ID that
resolution produces.

## When to use it

You have a record (or just a company name and website) and want its canonical identity plus revenue,
headcount, location, and operating status. Resolution is included, so you do not need a `kernel_id`
first.

## Inputs

- `jobs` (required): `["firmographics"]` for this prompt. Without `jobs` the call is accepted but never
  processes.
- Company signals (same as Resolve): `legal_name`, `trading_name`, `website`, `country`, `city`,
  `state`, `postal_code`, `address`, `email`, `linkedin_url`, `match_to_linkedin`, `external_id`.
- `webhook_url` (optional, recommended here since firmographics is the slow step).

## Call it (curl)

```bash
curl -s -X POST https://api.kernel.ai/rest/v1/combined \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" \
  -d '{"jobs":["firmographics"],"legal_name":"Stripe","website":"stripe.com","country":"US"}'
# -> {"id":"<job_id>","status":"processing"}

curl -s https://api.kernel.ai/rest/v1/combined/<job_id> -H "x-api-key: $KERNEL_API_KEY"
# poll until status is "completed"
```

## Call it (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

def combined(jobs, **signals):
    job_id = requests.post(f"{BASE}/v1/combined", headers=HEADERS, json={"jobs": jobs, **signals}).json()["id"]
    delay = 5
    while True:                                    # exponential backoff; firmographics can take ~10 min
        time.sleep(delay)
        data = requests.get(f"{BASE}/v1/combined/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay * 2, 30)

rec = combined(["firmographics"], legal_name="Stripe", website="stripe.com", country="US")
print(rec["kernel_id"])
print(rec["firmographics"]["revenue"])        # firmographics, nested under "firmographics"
print(rec["firmographics"]["headcount"])
```

## What you get back

One `record`:
- **resolve identity:** `kernel_id`, `legal_info`, `trading_info`, `entity_classification`, `identity_*` fields
- **`firmographics`:** `revenue`, `headcount`, `location`, `op_status`

Firmographics is nested under `firmographics`. No hierarchy is returned (this prompt does not request it).

## Notes

- `jobs` is required. Omit it and the job is accepted but never leaves `pending`.
- This includes firmographics, the slow step (about 10 minutes in testing). Pass `webhook_url` rather
  than holding a long poll.
- Revenue and headcount come at two scopes: this entity alone (`usd_entity` / `count_entity`) and the
  whole group (`consolidated_usd` / `count_consolidated`). For "how big is this company," use the
  consolidated figure.
- Resolution is included, so no `kernel_id` is needed up front. If you already hold a `kernel_id`, use
  the single-purpose Firmographics prompt.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity.
- **Combined (full)** / **(resolve + hierarchy)**: the other one-call variants.
- **Enrich / Firmographics / Hierarchy**: the same data via separate calls when you already have a `kernel_id`.

````

{% endprompt %}

{% prompt description="Resolve and enrich with parent hierarchies" icon="2" %}

````markdown
# Resolve a record and get its hierarchy in one call

You have access to the user's system of record (for example their CRM) and the Kernel API
(`KERNEL_API_KEY` is already configured). Given a company's details, Kernel's combined endpoint resolves
the company and returns its corporate hierarchy (parent and ultimate parent) in a single call. Use it
when you want a company's identity and who owns it, without chaining Resolve then Hierarchy.

This resolves the company first, then runs the requested job; everything is keyed on the KERN ID that
resolution produces.

## When to use it

You have a record (or just a company name and website) and want its canonical identity plus its parent
and ultimate parent. Resolution is included, so you do not need a `kernel_id` first.

## Inputs

- `jobs` (required): `["resolve-parent"]` for this prompt. Without `jobs` the call is accepted but
  never processes.
- Company signals (same as Resolve): `legal_name`, `trading_name`, `website`, `country`, `city`,
  `state`, `postal_code`, `address`, `email`, `linkedin_url`, `match_to_linkedin`, `external_id`.
- `webhook_url` (optional).

## Call it (curl)

```bash
curl -s -X POST https://api.kernel.ai/rest/v1/combined \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" \
  -d '{"jobs":["resolve-parent"],"legal_name":"Stripe","website":"stripe.com","country":"US"}'
# -> {"id":"<job_id>","status":"processing"}

curl -s https://api.kernel.ai/rest/v1/combined/<job_id> -H "x-api-key: $KERNEL_API_KEY"
# poll until status is "completed"
```

## Call it (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

def combined(jobs, **signals):
    job_id = requests.post(f"{BASE}/v1/combined", headers=HEADERS, json={"jobs": jobs, **signals}).json()["id"]
    delay = 5
    while True:                                    # exponential backoff
        time.sleep(delay)
        data = requests.get(f"{BASE}/v1/combined/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay * 2, 30)

rec = combined(["resolve-parent"], legal_name="Stripe", website="stripe.com", country="US")
print(rec["kernel_id"])
print(rec["parentage"]["parent"])             # immediate parent, or None
print(rec["parentage"]["top_parent"])         # ultimate parent
```

## What you get back

One `record`:
- **resolve identity:** `kernel_id`, `legal_info`, `trading_info`, `entity_classification`, `identity_*` fields
- **`parentage`:** `parent`, `top_parent`, `top_operating_parent`, `regional_subsidiary`

Hierarchy is nested under `parentage`. No firmographics are returned (this prompt does not request them).

## Notes

- `jobs` is required. Omit it and the job is accepted but never leaves `pending`.
- This omits firmographics, so it is faster than the full combined call (no slow firmographics step).
  Poll with backoff, or pass `webhook_url`.
- Resolution is included, so no `kernel_id` is needed up front. If you already hold a `kernel_id`, use
  the single-purpose Hierarchy prompt.
- Sanity-check hierarchy: a confident `parent` / `top_parent` can still be debatable (in testing, Bose
  resolved to MIT, a majority but non-voting owner). Review before trusting.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity.
- **Combined (full)** / **(resolve + firmographics)**: the other one-call variants.
- **Enrich / Firmographics / Hierarchy**: the same data via separate calls when you already have a `kernel_id`.

````

{% endprompt %}

{% prompt description="Resolve and enrich with firmographics and hierarchies" icon="3" %}

````markdown
# Resolve and fully enrich a record in one call

You have access to the user's system of record (for example their CRM) and the Kernel API
(`KERNEL_API_KEY` is already configured). Given a company's details, Kernel's combined endpoint resolves
the company and enriches it with corporate hierarchy and firmographics in a single call, returning one
record. Use it when you want the whole picture and prefer one call over chaining Resolve, Hierarchy,
and Firmographics separately.

This resolves the company first, then runs the requested jobs; everything is keyed on the KERN ID that
resolution produces.

## When to use it

You have a record (or just a company name and website) and want its canonical identity, corporate
hierarchy, and firmographics together. Resolution is included, so you do not need a `kernel_id` first.

## Inputs

- `jobs` (required): `["resolve-parent", "firmographics"]` for this prompt. Without `jobs` the call is
  accepted but never processes.
- Company signals (same as Resolve): `legal_name`, `trading_name`, `website`, `country`, `city`,
  `state`, `postal_code`, `address`, `email`, `linkedin_url`, `match_to_linkedin`, `external_id`.
- `webhook_url` (optional, recommended here since this includes the slow firmographics step).

## Call it (curl)

```bash
curl -s -X POST https://api.kernel.ai/rest/v1/combined \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" \
  -d '{"jobs":["resolve-parent","firmographics"],"legal_name":"Stripe","website":"stripe.com","country":"US"}'
# -> {"id":"<job_id>","status":"processing"}

curl -s https://api.kernel.ai/rest/v1/combined/<job_id> -H "x-api-key: $KERNEL_API_KEY"
# poll until status is "completed"
```

## Call it (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

def combined(jobs, **signals):
    job_id = requests.post(f"{BASE}/v1/combined", headers=HEADERS, json={"jobs": jobs, **signals}).json()["id"]
    delay = 5
    while True:                                    # exponential backoff; this can take ~10 min
        time.sleep(delay)
        data = requests.get(f"{BASE}/v1/combined/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay * 2, 30)

rec = combined(["resolve-parent", "firmographics"],
               legal_name="Stripe", website="stripe.com", country="US")
print(rec["kernel_id"])
print(rec["parentage"]["top_parent"])         # hierarchy, nested under "parentage"
print(rec["firmographics"]["headcount"])      # firmographics, nested under "firmographics"
```

## What you get back

One `record`:
- **resolve identity:** `kernel_id`, `legal_info`, `trading_info`, `entity_classification`, `identity_*` fields
- **`parentage`:** `parent`, `top_parent`, `top_operating_parent`, `regional_subsidiary`
- **`firmographics`:** `revenue`, `headcount`, `location`, `op_status`

Hierarchy is nested under `parentage` and firmographics under `firmographics`.

## Notes

- `jobs` is required. Omit it and the job is accepted but never leaves `pending`.
- This includes firmographics, the slow step (about 10 minutes in testing). Pass `webhook_url` rather
  than holding a long poll.
- Resolution is included, so no `kernel_id` is needed up front. If you already hold a `kernel_id`, use
  the single-purpose Firmographics or Hierarchy prompts.
- Sanity-check hierarchy: a confident `parent` / `top_parent` can still be debatable (in testing, Bose
  resolved to MIT, a majority but non-voting owner). Review before trusting.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity.
- **Combined (resolve + hierarchy)** / **(resolve + firmographics)**: the lighter one-call variants.
- **Enrich / Firmographics / Hierarchy**: the same data via separate calls when you already have a `kernel_id`.
````

{% endprompt %}
{% endtab %}
{% endtabs %}

## Advanced use-case prompts

These prompts help you solve common problems with Kernel APIs.

* Resolve RevOps tickets
* Batch fix trouble accounts
* Boost system match rates
* Plug data gaps
* Collapse hierarchies
* Climb to the ultimate parent
* Find the top operating parent

{% tabs %}
{% tab title="Resolve RevOps tickets" %}
Clear your account data quality ticket backlog, with a ready-to-approve fix proposed for every flagged account.

{% prompt description="Resolve RevOps tickets" icon="exclamation" %}

````markdown
# Clear a RevOps data-quality ticket queue

You have access to the user's ticket queue and their system of record (CRM), plus the Kernel API
(`KERNEL_API_KEY` is already configured). Reps file tickets when a company Account record looks wrong:
bad firmographics (headcount, revenue, location, status), a wrong or missing parent, or the record
pointing at the wrong company. For each in-scope ticket, use Kernel to verify the Account and propose a
correction for a human to approve.

Resolution always runs first: it produces the KERN ID the enrichment is keyed on. For a firmographics
or hierarchy ticket, the resolve and that enrichment happen in one `/combined` call; identity tickets
only resolve.

## When to use it

You have a queue of data-quality tickets against company Account records and want Kernel to check each
one and propose fixes, instead of a rep researching and editing by hand.

## What this can and cannot do

Kernel is a company entity API. This workflow only handles company data-quality tickets on Account
records: the right company (identity), its firmographics (revenue, headcount, location, status), and
its hierarchy (parent, ultimate parent).

It cannot help with these, and you should route them to a human instead:
- Contact or Lead records, and any person-level data (names, emails, job titles, phone numbers)
- Deals or opportunities
- Lead or account routing and ownership assignment
- Field formatting, picklist values, or data hygiene unrelated to company facts
- Permissions or access
- Anything that is not company data on an Account record

Out-of-scope tickets are flagged and skipped, not guessed at.

## How the queue is supplied

Read the queue from wherever the user keeps it: CRM cases, a ticketing tool (Jira, Zendesk, Linear), or
an exported list. Kernel does not ingest tickets; you orchestrate, calling Kernel once per record. Each
ticket should identify its record by whatever id the source uses (ideally a CRM record id; it may
instead be a company name or URL).

## What to do, per ticket

1. Read the ticket and identify the record it refers to. Pull that record's current fields from the
   system of record (name, website, country, address, and the disputed values).
2. Resolve and run the flagged enrichment in one `/combined` call (set `jobs` from the ticket):
   - firmographics issue: `jobs: ["firmographics"]`
   - hierarchy issue: `jobs: ["resolve-parent"]`
   - wrong-company or identity issue: a plain resolve (no enrichment job; `/combined` needs at least one)
3. Use the resolved identity returned in the same response to confirm the company. If it does not match
   the record, that mismatch is the finding: a bad field is often a symptom of a mis-resolved record.
4. Build a field-by-field before/after: the record's current value vs Kernel's, with Kernel's
   confidence and reasoning.
5. Propose the change for review. Do not write to the record and do not close the ticket. Output a
   proposal a reviewer can accept or reject.

## Example (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

def run_job(path, body):
    job_id = requests.post(f"{BASE}/{path}", headers=HEADERS, json=body).json()["id"]
    delay = 2
    while True:
        time.sleep(delay)
        data = requests.get(f"{BASE}/{path}/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay + 5, 30)

def classify(ticket, record):
    # Account records only. Anything on a Contact, Lead, Opportunity, etc. is out of scope.
    obj = (record.get("object") or "Account").lower()
    if obj not in ("account", "company"):
        return "out_of_scope"
    t = (ticket.get("issue") or "").lower()
    # Out-of-scope topics even on an Account ticket: people, deals, routing, formatting, access.
    if any(w in t for w in ("contact", "person", "email", "phone", "job title",
                            "routing", "assignment", "deal", "opportunity",
                            "permission", "access", "format")):
        return "out_of_scope"
    if any(w in t for w in ("parent", "hierarchy", "ultimate", "subsidiary", "owns")):
        return "hierarchy"
    if any(w in t for w in ("revenue", "headcount", "employees", "size", "location",
                            "address", "hq", "industry", "status", "active", "defunct")):
        return "firmographics"
    # In-scope Account ticket with no clear enrichment: resolve confirms the right company.
    return "identity"

def process_ticket(ticket, record):
    issue = classify(ticket, record)
    if issue == "out_of_scope":
        return {"ticket": ticket["id"], "record": record["id"], "status": "out_of_scope",
                "reason": "Kernel handles company identity, firmographics, and hierarchy on Account "
                          "records only. Route this ticket to a human."}

    signals = {"legal_name": record.get("name"), "website": record.get("website"),
               "country": record.get("country"), "external_id": record["id"]}

    # Resolve and run the flagged enrichment in ONE /combined call. Identity tickets only need
    # resolution, and /combined needs at least one job, so those use plain entity-resolution.
    if issue == "firmographics":
        rec = run_job("v1/combined", {"jobs": ["firmographics"], **signals})
    elif issue == "hierarchy":
        rec = run_job("v1/combined", {"jobs": ["resolve-parent"], **signals})
    else:  # identity
        rec = run_job("v1/entity-resolution", signals)

    resolved_name = (rec.get("legal_info") or {}).get("legal_name")
    proposal = {"ticket": ticket["id"], "record": record["id"], "kernel_id": rec.get("kernel_id"),
                "resolved_name": resolved_name, "identity_ok": None, "changes": []}
    # Compare the resolved identity to the record (name / website) to flag a mis-resolved record.
    proposal["identity_ok"] = bool(resolved_name) and resolved_name.lower().startswith(
        (record.get("name") or "").lower()[:4])

    if issue == "identity":
        return proposal                                  # the resolve is the answer

    if issue == "firmographics":
        f = rec.get("firmographics") or {}
        hc, rev = (f.get("headcount") or {}), (f.get("revenue") or {})
        proposal["changes"] = [
            {"field": "headcount", "current": record.get("headcount"),
             "proposed": hc.get("count"), "confidence": hc.get("confidence"),
             "reasoning": hc.get("reasoning")},
            {"field": "revenue_usd", "current": record.get("revenue_usd"),
             "proposed": rev.get("consolidated_usd"), "confidence": rev.get("confidence"),
             "reasoning": rev.get("reasoning")},
        ]
    elif issue == "hierarchy":
        top = (rec.get("parentage") or {}).get("top_parent") or {}
        proposal["changes"] = [
            {"field": "ultimate_parent", "current": record.get("ultimate_parent"),
             "proposed": top.get("legal_name"), "confidence": top.get("confidence")},
        ]
    return proposal

# Read the queue from wherever it lives, then collect proposals for a human to review.
proposals = [process_ticket(t, get_record(t["record_id"])) for t in get_ticket_queue()]
for p in proposals:
    print(p)            # render as a review list; nothing is written back
```

## What you produce, per ticket

A proposal, never an edit:
- the ticket id and record id
- the resolved `kernel_id` and identity, and whether it matches the record
- a before/after for the disputed fields, with Kernel confidence and reasoning
- a recommendation: apply, reject, or needs a human look
- out-of-scope tickets, flagged and skipped with a reason, for a human to route

## Notes

- This only touches Account records. Contact, lead, deal, routing, formatting, and permission tickets
  are flagged out of scope and skipped, not attempted.
- Always resolve first, even for a firmographics ticket. If Kernel resolves the record to a different
  company or to a closed shell, fixing the number is pointless until the identity is right.
- For a hierarchy or firmographics ticket on a closed entity (`op_status` not Active), enrich the
  operating parent instead (`top_operating_parent`). See the Hierarchy prompt.
- Propose, do not auto-apply. Surface Kernel's reasoning so the reviewer can judge.
- Firmographics can take a few minutes; resolve and hierarchy are usually faster. Poll or use `webhook_url`.
- `/combined` requires at least one job. An empty `jobs` array is accepted but never processes, which
  is why the resolve-only (identity) branch uses plain entity resolution instead.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

This workflow also needs your system of record (CRM) connected to the agent. The API key alone is not
enough.

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity.
- **Enrich firmographics**: KERN ID to revenue, headcount, location, status.
- **Enrich hierarchy**: KERN ID to parent and ultimate parent.
- **Enrich (both)**: KERN ID to hierarchy and firmographics together.

````

{% endprompt %}
{% endtab %}

{% tab title="Batch fix trouble accounts" %}
Check and correct a whole list of your most important or messiest accounts in a single pass.

{% prompt description="Batch fix trouble accounts" icon="face-anguished" %}

````markdown
# Resolve a batch of problem accounts

You have access to the user's system of record (CRM) and the Kernel API (`KERNEL_API_KEY` is already
configured). The user hands you a batch of accounts they know have data issues, or that matter enough
to get exactly right. Run the Kernel checks they ask for across the whole batch and propose
corrections for review.

Each account is resolved and enriched in one `/combined` call (resolution is included). It still
resolves first inside that call; everything is keyed on the KERN ID it produces.

## When to use it

The user has a curated list of accounts (known-bad data, or strategically important) and wants Kernel
to verify and correct them in one pass, rather than fixing them one at a time.

## First, agree the scope

Ask the user which dimensions to refresh across the batch. They can pick one, some, or all:
- identity (the correct company and a KERN ID)
- firmographics (revenue, headcount, location, operating status)
- hierarchy (parent and ultimate parent)

Firmographics and hierarchy are bundled with resolution in one `/combined` call, which doubles as the
identity check. If you pick neither enrichment, it runs a plain resolve.

## How the list is supplied

Take the batch from wherever the user provides it: a pasted list, a CSV, a CRM segment, a saved view.
Each row should identify its account by whatever id the source uses (ideally a CRM record id; it may
instead be a company name or URL). For a large batch, pass `webhook_url` so Kernel calls you back
instead of holding open a long poll.

## What to do, per account

1. Pull the account's current fields from the system of record (name, website, country, address, and
   the values you will compare against).
2. Resolve the account and run the selected enrichment in one `/combined` call (`jobs` from the chosen
   dimensions; a plain resolve if none selected). Flag any account whose resolved company does not match
   the record: that is a likely root cause of its data issues.
3. Read the selected dimensions from the response (`firmographics`, `parentage`).
4. Build a before/after for each selected dimension: current value vs Kernel's, with confidence and
   reasoning. The accounts where Kernel disagrees are where the value is.
5. Collect a proposal per account. Do not write to any record.

## Example (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

# Dimensions the user chose for this batch. These become the /combined jobs; resolution is included.
DIMENSIONS = {"firmographics": True, "hierarchy": True}

def run_job(path, body):
    job_id = requests.post(f"{BASE}/{path}", headers=HEADERS, json=body).json()["id"]
    delay = 2
    while True:
        time.sleep(delay)
        data = requests.get(f"{BASE}/{path}/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay + 5, 30)

def review_account(record):
    signals = {"legal_name": record.get("name"), "website": record.get("website"),
               "country": record.get("country"), "external_id": record["id"]}
    jobs = (["resolve-parent"] if DIMENSIONS.get("hierarchy") else []) + \
           (["firmographics"] if DIMENSIONS.get("firmographics") else [])
    # One /combined call when enriching; plain resolve if only identity was chosen (/combined needs a job).
    rec = run_job("v1/combined", {"jobs": jobs, **signals}) if jobs else run_job("v1/entity-resolution", signals)

    resolved_name = (rec.get("legal_info") or {}).get("legal_name")
    out = {"record": record["id"], "kernel_id": rec.get("kernel_id"), "resolved_name": resolved_name,
           "identity_ok": bool(resolved_name) and resolved_name.lower().startswith(
               (record.get("name") or "").lower()[:4]),
           "changes": []}

    if DIMENSIONS.get("firmographics"):
        f = rec.get("firmographics") or {}
        hc, rev = (f.get("headcount") or {}), (f.get("revenue") or {})
        out["changes"] += [
            {"field": "headcount", "current": record.get("headcount"),
             "proposed": hc.get("count"), "confidence": hc.get("confidence")},
            {"field": "revenue_usd", "current": record.get("revenue_usd"),
             "proposed": rev.get("consolidated_usd"), "confidence": rev.get("confidence")},
        ]
    if DIMENSIONS.get("hierarchy"):
        top = (rec.get("parentage") or {}).get("top_parent") or {}
        out["changes"].append(
            {"field": "ultimate_parent", "current": record.get("ultimate_parent"),
             "proposed": top.get("legal_name"), "confidence": top.get("confidence")})
    return out

# Read the batch from wherever the user provides it.
proposals = [review_account(get_record(a)) for a in get_account_batch()]

# Surface the biggest problems first: identity mismatches, then most-changed records.
def changed(p): return sum(1 for c in p["changes"] if c["current"] != c["proposed"])
proposals.sort(key=lambda p: (p["identity_ok"], -changed(p)))
for p in proposals:
    print(p)            # render as a review list; nothing is written back
```

## What you produce

A review list, never edits:
- one row per account: record id, resolved KERN ID, and whether the identity matches the record
- for each selected dimension, a before/after with confidence and reasoning
- the batch ordered so identity mismatches and the biggest discrepancies come first

## Notes

- Resolve first whenever you enrich. A wrong number is often a symptom of a mis-resolved account, so
  the identity check is the first thing to surface.
- For closed entities (`op_status` not Active), enrich the operating parent instead
  (`top_operating_parent`). See the Hierarchy prompt.
- Firmographics is the slow step (a few minutes each). For large batches pass `webhook_url` and process
  in the background; log progress so the user can watch the queue drain.
- Propose, do not auto-apply. The user reviews the list and decides what to write back.
- `/combined` requires at least one job. An empty `jobs` array is accepted but never processes, which
  is why the identity-only selection uses plain entity resolution instead.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

This workflow also needs your system of record (CRM) connected to the agent. The API key alone is not
enough.

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity.
- **Enrich firmographics**: KERN ID to revenue, headcount, location, status.
- **Enrich hierarchy**: KERN ID to parent and ultimate parent.
- **Clear a RevOps ticket queue**: the same checks driven by reps' data-quality tickets.


````

{% endprompt %}
{% endtab %}

{% tab title="Boost system match rates" %}
Match more of your records to your other data vendors by sending them clean, current company details.

{% prompt description="Boost match rates between your systems" icon="link" %}

````markdown
# Lift match rates to your other data vendors

You have access to the user's system of record (CRM) and the Kernel API (`KERNEL_API_KEY` is already
configured). Poor match rates against other vendors (enrichment, intent, ad platforms, D&B) usually
come from messy or stale identifiers, above all a wrong or outdated domain. Resolve each record with
Kernel to get a clean canonical identity, then use those canonical keys as the inputs you send your
other vendors. This uses entity resolution only.

Entity resolution is the whole of this use case: the canonical match keys come from resolving each
record to its KERN ID.

## When to use it

You query or enrich against another vendor and too many records come back unmatched. Kernel
canonicalizes the company identity (verified legal name, the current canonical domain, LinkedIn URL,
KERN ID), so the keys you send the vendor are the ones most likely to match.

## What it produces, per record (one resolve call)

- `kernel_id`, `legal_name`, `trading_name`
- `canonical_domain` (the current verified website, often corrected vs the record: this is the big lever)
- `linkedin_url` (set `match_to_linkedin` so resolution also returns the LinkedIn company URL)
- `country` and the resolution confidence

## What to do

1. Pull the record's current signals from the system of record (name, website, country).
2. Resolve with Kernel, setting `match_to_linkedin` to true, to get the canonical identity.
3. Build the canonical match keys. Where Kernel's canonical domain differs from the record's, that
   correction is the main match-rate lever; flag it as a proposed source fix for review.
4. Output a match-ready table (record id to canonical keys) to feed your vendor.
5. Optional: pass the canonical keys to your vendor's match or enrich API and record whether it matched,
   so you can measure the lift.

## Example (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

def resolve(**signals):
    job_id = requests.post(f"{BASE}/v1/entity-resolution", headers=HEADERS, json=signals).json()["id"]
    delay = 2
    while True:
        time.sleep(delay)
        data = requests.get(f"{BASE}/v1/entity-resolution/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay + 2, 30)

def match_keys(record):
    r = resolve(legal_name=record.get("name"), website=record.get("website"),
                country=record.get("country"), external_id=record["id"],
                match_to_linkedin=True)
    legal, trading = (r.get("legal_info") or {}), (r.get("trading_info") or {})
    linkedin = r.get("linkedin") or {}
    canonical_domain = legal.get("website") or trading.get("website")
    keys = {
        "record_id": record["id"],
        "kernel_id": r.get("kernel_id"),
        "legal_name": legal.get("legal_name"),
        "trading_name": trading.get("trading_name"),
        "canonical_domain": canonical_domain,
        "linkedin_url": linkedin.get("url"),
        "country": legal.get("country") or trading.get("country"),
        "confidence": r.get("identity_resolution_confidence"),
    }
    # The main match-rate lever: a corrected domain. Flag it as a proposed source fix.
    if canonical_domain and canonical_domain != record.get("website"):
        keys["proposed_domain_fix"] = {"current": record.get("website"), "canonical": canonical_domain}
    return keys

def match_to_vendor(keys):
    # Optional: pass the canonical keys to your vendor's match or enrich API.
    # return vendor_client.match(domain=keys["canonical_domain"],
    #                            name=keys["legal_name"], linkedin=keys["linkedin_url"])
    ...

# Build canonical match keys for the batch, then export (and optionally match against the vendor).
rows = [match_keys(get_record(a)) for a in get_account_batch()]
for row in rows:
    print(row)            # match-ready: send canonical_domain / linkedin_url / legal_name to your vendor
```

## Output

- a match-ready row per record: record id, `kernel_id`, `legal_name`, `canonical_domain`, `linkedin_url`, `country`
- proposed source fixes: records where the canonical domain differs from the stored one (review before writing)
- if you ran the optional vendor step: matched yes or no per record, so you can measure the lift

## Notes

- The canonical domain is the biggest lever. Vendors key heavily on domain; sending the current,
  verified one (not a stale or wrong domain) is what moves match rates.
- `match_to_linkedin` adds `record.linkedin.url` (and a slug), a strong key for vendors that match on LinkedIn.
- Propose source-domain fixes, do not auto-apply. Fixing the domain at the source lifts every future
  vendor sync, not just this one.
- Resolution only; firmographics and hierarchy are not needed for matching.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

This workflow also needs your system of record (CRM) connected to the agent. The API key alone is not
enough.

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity.
- **Enrich firmographics**: KERN ID to revenue, headcount, location, status.
- **Enrich hierarchy**: KERN ID to parent and ultimate parent.
- **Clear a RevOps ticket queue / Resolve a batch of problem accounts**: full workflows over many records.

````

{% endprompt %}
{% endtab %}

{% tab title="Plug data gaps" %}
Fill the missing fields on your accounts automatically, leaving the data you already have untouched.

{% prompt description="Plug identity, firmographic or hierarchy gaps" icon="puzzle-piece" %}

````markdown
# Plug data gaps on your accounts

You have access to the user's system of record (CRM) and the Kernel API (`KERNEL_API_KEY` is already
configured). Many accounts are missing fields: no LinkedIn URL, no revenue, no headcount, no parent.
For each account, resolve it with Kernel, see which fields are blank, and fill only those. Never
overwrite a field that already has a value.

Each account is resolved and enriched in one `/combined` call (resolution is included). It still
resolves first inside that call; everything is keyed on the KERN ID it produces.

## When to use it

Your CRM has accounts with empty company fields and you want Kernel to complete them without touching
data that is already there.

## What it can fill

Kernel can fill these fields when they are blank on the account:
- From resolution (always included): LinkedIn URL, canonical domain (website), country.
- From firmographics: revenue, headcount, location, operating status.
- From hierarchy: parent and ultimate parent.

One `/combined` call resolves the account and runs whichever of firmographics and hierarchy a gap
needs, so the resolve-derived fields (LinkedIn URL, domain, country) come for free. If only those are
missing, it is a plain resolve with no enrichment.

## Which accounts

Either works: take a list or saved view the user supplies, or scan the system of record for accounts
missing target fields (revenue, headcount, or parent is blank).

## Fill or review (your call)

Pick how to handle each filled value:
- review: propose every fill for a human to accept (write nothing).
- fill: write the value automatically when Kernel's confidence meets your threshold (e.g. HIGH), and
  route anything below the threshold to review.

Either way, only blank fields are touched; populated fields are left alone.

## What to do, per account

1. Pull the account's current fields from the system of record.
2. Find which fields are blank: LinkedIn URL, canonical domain, country (from resolution), plus
   revenue, headcount, location, operating status (firmographics) and parent (hierarchy).
3. Resolve and enrich in one `/combined` call: set `jobs` to firmographics and/or resolve-parent based
   on which gaps exist (a plain resolve if only resolve-derived fields are missing). If the account
   resolves to a different company, the gaps may be a symptom of a mis-resolved record, so surface that.
4. For each blank field, take Kernel's value and its confidence, then fill or route to review per your mode.

## Example (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

MODE = "review"           # "review" proposes every fill; "fill" writes high-confidence ones
MIN_CONFIDENCE = "HIGH"   # in fill mode, auto-fill only at or above this; the rest go to review
RANK = {"LOW": 0, "MEDIUM": 1, "HIGH": 2}

def run_job(path, body):
    job_id = requests.post(f"{BASE}/{path}", headers=HEADERS, json=body).json()["id"]
    delay = 2
    while True:
        time.sleep(delay)
        data = requests.get(f"{BASE}/{path}/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay + 5, 30)

def is_blank(v):
    return v is None or v == ""        # adjust if your schema uses 0 or a placeholder for "missing"

def fill_gaps(record):
    # 1) Which fields are blank? (location maps to your address fields; here we use city.)
    gaps = []
    if is_blank(record.get("linkedin_url")):       gaps.append("linkedin_url")    # free, from resolution
    if is_blank(record.get("website")):            gaps.append("website")         # free, from resolution
    if is_blank(record.get("country")):            gaps.append("country")         # free, from resolution
    if is_blank(record.get("revenue_usd")):        gaps.append("revenue_usd")
    if is_blank(record.get("headcount")):          gaps.append("headcount")
    if is_blank(record.get("operational_status")): gaps.append("operational_status")
    if is_blank(record.get("city")):               gaps.append("location")
    if is_blank(record.get("ultimate_parent")):    gaps.append("ultimate_parent")

    # 2) Bundle the jobs the gaps need into one /combined call (resolution is always included).
    jobs = []
    if "ultimate_parent" in gaps:
        jobs.append("resolve-parent")
    if any(g in ("revenue_usd", "headcount", "operational_status", "location") for g in gaps):
        jobs.append("firmographics")
    signals = {"legal_name": record.get("name"), "website": record.get("website"),
               "country": record.get("country"), "external_id": record["id"],
               "match_to_linkedin": True}
    rec = run_job("v1/combined", {"jobs": jobs, **signals}) if jobs else run_job("v1/entity-resolution", signals)

    # 3) Read candidate values from the one response.
    legal    = rec.get("legal_info") or {}
    linkedin = rec.get("linkedin") or {}
    firmo    = rec.get("firmographics") or {}
    rev, hc  = (firmo.get("revenue") or {}), (firmo.get("headcount") or {})
    loc      = (firmo.get("location") or {}).get("operating") or {}
    top      = (rec.get("parentage") or {}).get("top_parent") or {}
    candidate = {   # field -> (value, confidence)
        "linkedin_url":       (linkedin.get("url"), None),
        "website":            (legal.get("website"), legal.get("confidence")),   # canonical domain
        "country":            (legal.get("country"), legal.get("confidence")),
        "revenue_usd":        (rev.get("consolidated_usd"), rev.get("confidence")),
        "headcount":          (hc.get("count"), hc.get("confidence")),
        "operational_status": ((firmo.get("op_status") or {}).get("operational_status"), None),
        "location":           (loc or None, None),   # {street, city, state, postcode, country}
        "ultimate_parent":    (top.get("legal_name"), top.get("confidence")),
    }

    fills = []
    for field in gaps:
        value, conf = candidate[field]
        if value is None:
            continue                                       # Kernel had nothing to offer
        auto = MODE == "fill" and RANK.get(conf, -1) >= RANK[MIN_CONFIDENCE]
        fills.append({"field": field, "value": value, "confidence": conf,
                      "action": "fill" if auto else "review"})
    return {"record": record["id"], "kernel_id": rec.get("kernel_id"), "fills": fills}

# Either: a supplied list, or scan the system of record for incomplete accounts.
records = SUPPLIED_LIST or get_incomplete_accounts()
results = [fill_gaps(get_record(a)) for a in records]
for res in results:
    print(res)            # write the "fill" items; route "review" items to a human
```

## What you produce

- per account: the resolved `kernel_id`, and a list of fills (field, value, confidence, action)
- only blank fields ever appear; populated fields are never proposed
- in fill mode, high-confidence values are written and the rest are queued for review

## Notes

- Resolve first. A record missing data is sometimes missing it because it points at the wrong company;
  the identity check catches that before you fill.
- For closed entities (`op_status` not Active), enrich the operating parent (`top_operating_parent`).
  See the Hierarchy prompt.
- Only blanks. Adjust `is_blank` if your schema uses 0 or a placeholder to mean missing.
- Operating status comes without a confidence score, so it routes to review by default in fill mode.
- LinkedIn URL comes from the resolve match and also has no confidence score, so it routes to review by
  default in fill mode.
- Location is multi-field: it fills your address fields (street, city, state, postal code, country) from
  `location.operating`, and likewise has no single confidence score, so it routes to review by default.
- Firmographics is the slow step; for large scans pass `webhook_url` and log progress.
- `/combined` requires at least one job. An empty `jobs` array is accepted but never processes, which
  is why a record with only resolve-derived gaps uses plain entity resolution instead.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

This workflow also needs your system of record (CRM) connected to the agent. The API key alone is not
enough.

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity.
- **Enrich firmographics**: KERN ID to revenue, headcount, location, status.
- **Enrich hierarchy**: KERN ID to parent and ultimate parent.
- **RevOps ticket queue / batch problem accounts / vendor match rates**: other practical workflows.


````

{% endprompt %}
{% endtab %}

{% tab title="Collapse hierarchies" %}
Use Kernel entity definitions to consolidate entities into a simple hierarchy, according to your preferences and logic.

{% prompt description="Collapse hierarchies" icon="list-tree" %}

````markdown
# Collapse account hierarchies

You have access to the user's system of record (CRM) and the Kernel API (`KERNEL_API_KEY` is already
configured). A CRM often holds several distinct entities that belong to one organization: the
operating company, its brands, its regional offices, a holding company. These are real, separate
entities, not duplicate records of one thing. Use Kernel to work out what each account really is and
how it rolls up, then group the entities your policy says should consolidate into one survivor.
Nothing is merged automatically.

Each account is resolved and its hierarchy fetched in one `/combined` call. Resolution still runs first
inside that call; everything is keyed on the KERN ID it produces.

## When to use it

Your CRM holds multiple entities of the same organization (operating company, brands, regional arms,
holding company) and you want to consolidate them down to the accounts you actually want to keep, on
your own terms.

## How Kernel informs the decision

For each account, one `/combined` call (`jobs: ["resolve-parent"]`) gives you what you need:
- the resolve identity: `entity_classification.subtype`, one of `Operating`, `HoldCo/Investment`,
  `Business Unit`, or `Establishment`, plus the `kernel_id` and resolution confidence. (A brand is a
  `Business Unit`; its brand name is in `trading_name`.)
- `parentage`, the targets to roll up into: `parent` (one level), `top_operating_parent` (the highest
  operating company, skipping holding companies), `top_parent` (the apex), and whether the account is a
  regional subsidiary.

## Define your policy (required, no default)

You declare, for each subtype, whether an account of that type survives as its own account or
collapses, and the level it collapses into. Nothing is assumed: any subtype you leave unset is flagged
for you, not guessed. Whether `Business Unit` (brands) and `Establishment` (regional offices) persist
is the heart of your definition. See `POLICY` in the code.

## What to do

1. Take the set of accounts to consolidate (a supplied list, or scan the system of record).
2. For each account: one `/combined` call (`jobs: ["resolve-parent"]`) returns its identity, subtype,
   `kernel_id`, and roll-up targets (under `parentage`).
3. Apply your policy to decide survive vs collapse, and compute the survivor `kernel_id` for collapses.
4. Group accounts by the survivor they collapse into.
5. Propose each group: the survivor, the members to merge in, and any flags. Merge nothing; the user
   reviews and executes merges in the CRM.

## Example (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}

# ---- YOUR POLICY (required; there is no default) -----------------------------
# For each subtype set "action" to "survive" or "collapse".
# For "collapse" set "into" to one of: "top_operating_parent" | "top_parent" | "parent".
# Any row you leave as None is flagged for you, not guessed.
POLICY = {
    "Operating":         {"action": None, "into": None},
    "HoldCo/Investment": {"action": None, "into": None},
    "Business Unit":     {"action": None, "into": None},   # this row answers "do brands persist?"
    "Establishment":     {"action": None, "into": None},
}
REGIONAL_RULE = None   # optional override for regional subsidiaries, same shape as a POLICY row
# ------------------------------------------------------------------------------

def run_job(path, body):
    job_id = requests.post(f"{BASE}/{path}", headers=HEADERS, json=body).json()["id"]
    delay = 2
    while True:
        time.sleep(delay)
        data = requests.get(f"{BASE}/{path}/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay + 5, 30)

def classify(record):
    """Resolve an account and read its roll-up targets, in one /combined call."""
    rec = run_job("v1/combined", {
        "jobs": ["resolve-parent"],
        "legal_name": record.get("name"), "website": record.get("website"),
        "country": record.get("country"), "external_id": record["id"]})
    parentage = rec.get("parentage") or {}
    return {
        "record_id": record["id"],
        "kernel_id": rec.get("kernel_id"),
        "resolved_name": (rec.get("legal_info") or {}).get("legal_name"),
        "subtype": (rec.get("entity_classification") or {}).get("subtype"),
        "confidence": rec.get("identity_resolution_confidence"),
        "is_regional": (parentage.get("regional_subsidiary") or {}).get("is_regional"),
        "targets": {
            "parent": parentage.get("parent") or {},
            "top_operating_parent": parentage.get("top_operating_parent") or {},
            "top_parent": parentage.get("top_parent") or {},
        },
    }

def decide(info):
    """Apply YOUR policy. Returns ('survive', None), ('collapse', survivor), or ('flag', reason)."""
    rule = REGIONAL_RULE if (info["is_regional"] and REGIONAL_RULE) else POLICY.get(info["subtype"])
    if not rule or rule.get("action") not in ("survive", "collapse"):
        return ("flag", f"set a policy for subtype {info['subtype']}")
    if rule["action"] == "survive":
        return ("survive", None)
    return ("collapse", info["targets"].get(rule["into"]) or {})

# Resolve the whole set, then group accounts by the survivor they collapse into.
infos = [classify(get_record(a)) for a in get_account_set()]
all_kids = {i["kernel_id"] for i in infos}
groups, flags = {}, []
for info in infos:
    action, detail = decide(info)
    if action == "survive":
        continue                                      # this account persists; not in any group
    if action == "flag":
        flags.append({"record_id": info["record_id"], "issue": detail})
        continue
    survivor = detail
    skid = survivor.get("kernel_id")
    if not skid:
        flags.append({"record_id": info["record_id"], "issue": "no roll-up target from Kernel"})
        continue
    groups.setdefault(skid, {"survivor": survivor, "members": []})["members"].append(info)

# Propose each merge group. Nothing is merged here.
for skid, g in groups.items():
    print({
        "survivor_kernel_id": skid,
        "survivor_name": g["survivor"].get("legal_name"),
        "survivor_is_an_account": skid in all_kids,    # if False, you decide: create it or promote a member
        "merge_in": [m["record_id"] for m in g["members"]],
        "low_confidence_members": [m["record_id"] for m in g["members"] if m["confidence"] != "HIGH"],
    })
print("flags:", flags)
# Review each group, then execute the merges in your CRM. Nothing was changed.
```

## What you produce

- one group per survivor: survivor `kernel_id` and name, plus the member accounts to merge in
- `survivor_is_an_account`: whether the survivor is already one of your accounts (if not, you decide
  whether to create it or promote a member to survivor)
- flags: members with no roll-up target, low-confidence resolutions, and subtypes with no policy set
- accounts your policy keeps simply do not appear in any group

## Notes

- Merging is destructive and irreversible. This proposes only; a human reviews every group and runs the
  merge in the CRM.
- Collapsing to `top_operating_parent` flattens a multi-level tree in one pass and skips holding
  companies. Collapsing to `parent` rolls up only one level.
- `top_parent` caveat: it is meant to be the apex but can stop short of the true ultimate parent (a data
  gap). If your policy rolls up to `top_parent`, verify the survivor, or climb: call resolve-parent on
  the survivor's `kernel_id` until `parent` is null.
- Subtypes you do not set in POLICY, and Government or Education entities that may have no subtype, are
  flagged rather than guessed.
- Review low-confidence resolutions and any identity mismatch before merging; a wrong resolve would
  merge the wrong accounts.
- `/combined` requires at least one job. An empty `jobs` array is accepted but never processes; this
  workflow always passes `resolve-parent`.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

This workflow also needs your system of record (CRM) connected to the agent. The API key alone is not
enough.

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity (the subtype lives here).
- **Enrich hierarchy**: KERN ID to parent, ultimate parent, and top operating parent.
- **Plug data gaps / batch problem accounts / vendor match rates**: other practical workflows.

````

{% endprompt %}
{% endtab %}

{% tab title="Climb to the ultimate parent" %}
Loop up the ownership chain hop by hop until there is no parent left — the verified apex of the tree, with every intermediate entity on the way.

{% prompt description="Climb to the ultimate parent" icon="sitemap" %}

````markdown
# Climb to the ultimate parent

You have access to the Kernel API (`KERNEL_API_KEY` is already configured). For any account, walk the
corporate ownership chain one hop at a time — company, parent, parent's parent — until there is no
parent left. The entity you stop at is the verified ultimate parent (the apex of the tree), and the
loop gives you every intermediate entity on the way up, each with its classification, confidence, and
reasoning.

A single `/resolve-parent` call already returns a `top_parent` shortcut, but it can stop short of the
true apex (a data gap), and it tells you nothing about the levels in between. Climbing hop by hop
fixes both: you land on the entity that genuinely has no parent, and you get the full chain.

## When to use it

- You need the complete ownership chain for an account, not just the endpoints — every holding
  company, intermediate parent, and regional arm between the account and the top.
- You are rolling up accounts by ultimate parent and want the apex verified, not shortcut.
- You are auditing hierarchy data and want to see exactly where a tree's levels sit.

## How Kernel informs it

Each `POST /v1/resolve-parent` call takes a `kernel_id` and returns:
- `parent` — the immediate parent (one hop), with its own `kernel_id`, names, website, country,
  `entity_category` / `entity_sub_category`, confidence, and reasoning
- `top_parent` — the apex shortcut (used as a cross-check here, not as the answer)
- `top_operating_parent` and `regional_subsidiary` — extra context, not needed for the climb

The loop is: call resolve-parent, take `parent.kernel_id`, call again, repeat until `parent` is null.

If you start from company details instead of a Kernel ID, run one `/v1/entity-resolution` call first
to get the `kernel_id`, then climb.

## What to do

1. Take the account (a Kernel ID, or company details to resolve first).
2. Climb: call `/v1/resolve-parent` on the current `kernel_id`, append the returned `parent` to the
   chain, move up. Stop when `parent` is null — the current entity is the ultimate parent.
3. Guard against cycles (fragmented data can loop) and cap the depth as a safety net.
4. Report the full chain bottom → top, and cross-check the climbed apex against the `top_parent`
   shortcut from the first call; disagreement means the shortcut stopped short.

## Example (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}
MAX_DEPTH = 25          # safety cap; real trees converge well before this

def run_job(path, body):
    """Kernel jobs are async: submit, then poll until the job completes."""
    job_id = requests.post(f"{BASE}/{path}", headers=HEADERS, json=body).json()["id"]
    delay = 2
    while True:
        time.sleep(delay)
        data = requests.get(f"{BASE}/{path}/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay + 5, 30)

def resolve_kernel_id(name=None, website=None, country=None):
    """Company details -> kernel_id. Skip this if you already have one."""
    rec = run_job("v1/entity-resolution",
                  {k: v for k, v in {"legal_name": name, "website": website,
                                     "country": country}.items() if v})
    return rec.get("kernel_id")

def climb(kernel_id):
    """Walk parent links up until there is no parent left."""
    chain, seen, cur = [], {str(kernel_id)}, str(kernel_id)
    shortcut = None
    for _ in range(MAX_DEPTH):
        rec = run_job("v1/resolve-parent", {"kernel_id": cur})
        if shortcut is None:
            shortcut = rec.get("top_parent")          # the one-call answer, kept as a cross-check
        parent = rec.get("parent")
        if not parent:                                # no parent -> cur is the ultimate parent
            return {"ultimate_parent_kernel_id": cur, "chain": chain,
                    "top_parent_shortcut": shortcut, "warning": None}
        pid = str(parent["kernel_id"])
        if pid in seen:                               # cycle guard (fragmented data)
            return {"ultimate_parent_kernel_id": cur, "chain": chain,
                    "top_parent_shortcut": shortcut, "warning": "cycle detected"}
        seen.add(pid)
        chain.append(parent)
        cur = pid
    return {"ultimate_parent_kernel_id": cur, "chain": chain,
            "top_parent_shortcut": shortcut, "warning": "max depth reached"}

def name_of(node):
    return node.get("trading_name") or node.get("legal_name") or node.get("kernel_id") if node else None

# --- run it -------------------------------------------------------------------
kid = resolve_kernel_id(name="Ben & Jerry's", country="United States")   # or a known Kernel ID
result = climb(kid)

print(f"Chain (bottom -> top), starting from {kid}:")
for node in result["chain"]:
    print(f"  ^ {name_of(node)}  ({node.get('entity_sub_category')}, "
          f"{node.get('website') or '-'}, kernel_id {node['kernel_id']})")
apex = result["chain"][-1] if result["chain"] else None
print(f"Ultimate parent: {name_of(apex) or kid}  (kernel_id {result['ultimate_parent_kernel_id']})")

shortcut = result["top_parent_shortcut"]
if shortcut and str(shortcut.get("kernel_id")) != result["ultimate_parent_kernel_id"]:
    print(f"NOTE: top_parent shortcut said {name_of(shortcut)} - the climb went higher; trust the climb.")
if result["warning"]:
    print(f"WARNING: {result['warning']}")
```

## What you produce

- the full ownership chain, bottom → top, one entity per hop, each with name, `kernel_id`, website,
  country, and classification
- the verified ultimate parent: the entity the climb ends on because it has no parent
- a cross-check against the single-call `top_parent` shortcut, flagged when they disagree
- warnings for cycles or depth-capped climbs, so nothing fails silently

## Notes

- Each hop is one async job that can take ~30–60 seconds; a deep tree takes a few minutes. For a batch
  of accounts, run the climbs concurrently and cache results per `kernel_id` — trees overlap heavily,
  so siblings share almost their whole chain.
- If the climbed apex disagrees with the `top_parent` shortcut, trust the climb: it is grounded hop by
  hop, while the shortcut can stop short.
- If you want the highest *operating* company instead of the absolute apex (which is often a holding
  company), use the "Find the top operating parent" use case instead.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity.
- **Find the top operating parent**: the same climb, stopped at the highest operating company.
- **Collapse hierarchies / plug data gaps / batch problem accounts**: other practical workflows.

````

{% endprompt %}

#### Just the script

The same logic as a ready-to-run CLI — copy it into `climb_to_ultimate_parent.py` and run it directly:

```bash
export KERNEL_API_KEY=your-key-here
pip install requests

python3 climb_to_ultimate_parent.py 7913528487                                   # by Kernel ID
python3 climb_to_ultimate_parent.py "Ben & Jerry's" --country "United States"   # by name
```

<details>

<summary>climb_to_ultimate_parent.py — expand for the full script</summary>

```python
#!/usr/bin/env python3
"""Climb the Kernel ownership tree from any account to its ultimate parent.

Walks /v1/resolve-parent hop by hop until there is no parent left. The entity
it stops at is the verified apex of the tree; the chain holds every entity in
between. Takes a Kernel ID, or a company name (resolved first).
"""
import argparse
import os
import re
import sys
import time

import requests

BASE = os.environ.get("KERNEL_API_BASE_URL", "https://api.kernel.ai/rest")
MAX_DEPTH = 25                        # safety cap; real trees converge well before this
KERN_RE = re.compile(r"^\d{6,12}$")   # Kernel IDs are numeric


def run_job(path, body, headers):
    """Kernel jobs are async: submit, then poll until the job completes."""
    job_id = requests.post(f"{BASE}/{path}", headers=headers, json=body).json()["id"]
    delay = 2
    while True:
        time.sleep(delay)
        data = requests.get(f"{BASE}/{path}/{job_id}", headers=headers).json()
        if data["status"] == "failed":
            sys.exit(f"{path} job failed: {data}")
        if data["status"] == "completed":
            return data.get("record", {})
        delay = min(delay + 5, 30)


def name_of(node):
    if not node:
        return None
    return node.get("trading_name") or node.get("legal_name") or node.get("kernel_id")


def climb(kernel_id, headers):
    """Walk parent links up until there is no parent left."""
    chain, seen, cur = [], {str(kernel_id)}, str(kernel_id)
    shortcut, warning = None, None
    for _ in range(MAX_DEPTH):
        rec = run_job("v1/resolve-parent", {"kernel_id": cur}, headers)
        if shortcut is None:
            shortcut = rec.get("top_parent")     # the one-call answer, kept as a cross-check
        parent = rec.get("parent")
        if not parent:                           # no parent -> cur is the ultimate parent
            break
        pid = str(parent["kernel_id"])
        if pid in seen:                          # cycle guard (fragmented data)
            warning = "cycle detected"
            break
        seen.add(pid)
        chain.append(parent)
        cur = pid
    else:
        warning = "max depth reached"
    return {"ultimate_parent_kernel_id": cur, "chain": chain,
            "top_parent_shortcut": shortcut, "warning": warning}


def main():
    ap = argparse.ArgumentParser(description="Climb the Kernel tree to the ultimate parent.")
    ap.add_argument("query", help="A Kernel ID, or a company name to resolve first")
    ap.add_argument("--website", help="Sharpens the match when resolving by name")
    ap.add_argument("--country", help="Sharpens the match when resolving by name")
    args = ap.parse_args()

    key = os.environ.get("KERNEL_API_KEY")
    if not key:
        sys.exit("Set KERNEL_API_KEY first (see 'Setting up your API key')")
    headers = {"x-api-key": key, "Content-Type": "application/json"}

    if KERN_RE.match(args.query.strip()):
        kid = args.query.strip()
    else:
        body = {k: v for k, v in {"legal_name": args.query, "website": args.website,
                                  "country": args.country}.items() if v}
        kid = run_job("v1/entity-resolution", body, headers).get("kernel_id")
        if not kid:
            sys.exit(f"Could not resolve '{args.query}' to a Kernel entity")
        print(f"Resolved to kernel_id {kid}")

    result = climb(kid, headers)
    print(f"Chain (bottom -> top), starting from {kid}:")
    for node in result["chain"]:
        print(f"  ^ {name_of(node)}  ({node.get('entity_sub_category')}, "
              f"{node.get('website') or '-'}, kernel_id {node['kernel_id']})")
    apex = result["chain"][-1] if result["chain"] else None
    print(f"Ultimate parent: {name_of(apex) or kid}  "
          f"(kernel_id {result['ultimate_parent_kernel_id']})")

    shortcut = result["top_parent_shortcut"]
    if shortcut and str(shortcut.get("kernel_id")) != result["ultimate_parent_kernel_id"]:
        print(f"NOTE: top_parent shortcut said {name_of(shortcut)} - "
              f"the climb went higher; trust the climb.")
    if result["warning"]:
        print(f"WARNING: {result['warning']}")


if __name__ == "__main__":
    main()
```

</details>
{% endtab %}

{% tab title="Find the top operating parent" %}
Get the account every subsidiary should roll up to: the highest operating company in the tree, skipping holding companies and investment vehicles.

{% prompt description="Find the top operating parent" icon="crown" %}

````markdown
# Find the top operating parent

You have access to the Kernel API (`KERNEL_API_KEY` is already configured). For any account, find its
top operating parent — the highest revenue-generating company in its ownership tree, skipping holding
companies, investment vehicles, and other non-operating entities — and build the operating-only chain
from the account up to it.

This is the account that subsidiaries roll up to for territory assignment, deduplication, and
reporting. The absolute apex of a tree is often a HoldCo or investment vehicle nobody sells to; the
top operating parent is the real business above it all.

## When to use it

- You are assigning accounts to owners or territories by "who is the real parent company".
- You are rolling up revenue, headcount, or opportunities and holding companies would distort the
  rollup.
- A rep asks "who actually owns this account?" and the answer needs to skip PE funds and shell
  entities.
- You have a CSV of accounts (Kernel IDs or names) and want the top operating parent for each.

## How Kernel informs it

Each `POST /v1/resolve-parent` call takes a `kernel_id` and returns:
- `top_operating_parent` — the answer: the highest operating company in the tree
- `parent` — the immediate parent, with `entity_category` / `entity_sub_category`, so you can tell
  operating companies from non-operating ones
- `top_parent` — the absolute apex (often a HoldCo above the top operating parent)

An entity is operating when `entity_category == "Company"` and `entity_sub_category` is not one of
`HoldCo/Investment`, `Business Unit`, `Academic Unit`, `Establishment`.

One call gives you the top operating parent. The loop in the example additionally walks the immediate
parents up to it, so you also get the chain of operating companies in between and a list of the
non-operating entities that were skipped — useful for showing *why* the rollup lands where it does.

If you start from company details instead of a Kernel ID, run one `/v1/entity-resolution` call first
to get the `kernel_id`.

## What to do

1. Take the account (a Kernel ID, or company details to resolve first).
2. Call `/v1/resolve-parent` on it and read `top_operating_parent`. If it is the account itself (or
   missing), the account is already the top of its operating tree — stop.
3. Otherwise walk up hop by hop: keep operating ancestors in the chain, set non-operating ones aside,
   and stop when you reach the top operating parent's `kernel_id`.
4. Report per account: the top operating parent, the operating chain (bottom → top), and the
   non-operating entities skipped along the way.

## Example (Python)

```python
import os, time, requests

BASE = "https://api.kernel.ai/rest"
HEADERS = {"x-api-key": os.environ["KERNEL_API_KEY"], "Content-Type": "application/json"}
MAX_DEPTH = 25
NON_OPERATING = {"HoldCo/Investment", "Business Unit", "Academic Unit", "Establishment"}

def run_job(path, body):
    """Kernel jobs are async: submit, then poll until the job completes."""
    job_id = requests.post(f"{BASE}/{path}", headers=HEADERS, json=body).json()["id"]
    delay = 2
    while True:
        time.sleep(delay)
        data = requests.get(f"{BASE}/{path}/{job_id}", headers=HEADERS).json()
        if data["status"] in ("completed", "failed"):
            return data.get("record", {})
        delay = min(delay + 5, 30)

def resolve_kernel_id(name=None, website=None, country=None):
    """Company details -> kernel_id. Skip this if you already have one."""
    rec = run_job("v1/entity-resolution",
                  {k: v for k, v in {"legal_name": name, "website": website,
                                     "country": country}.items() if v})
    return rec.get("kernel_id")

def is_operating(node):
    return bool(node) and node.get("entity_category") == "Company" \
        and node.get("entity_sub_category") not in NON_OPERATING

def name_of(node):
    return node.get("trading_name") or node.get("legal_name") or node.get("kernel_id") if node else None

def top_operating_parent(kernel_id):
    """One call answers it; the walk up fills in the operating chain."""
    kid = str(kernel_id)
    root = run_job("v1/resolve-parent", {"kernel_id": kid})
    top_op = root.get("top_operating_parent")
    top_op_id = str(top_op["kernel_id"]) if top_op else None

    if top_op_id in (None, kid):        # the account IS the top of its operating tree
        return {"kernel_id": kid, "is_own_top_operating": top_op_id == kid,
                "top_operating_parent": top_op, "operating_chain": [], "skipped": [],
                "ultimate_parent": root.get("top_parent")}

    chain, skipped, seen, cur = [], [], {kid}, kid
    for _ in range(MAX_DEPTH):
        rec = root if cur == kid else run_job("v1/resolve-parent", {"kernel_id": cur})
        parent = rec.get("parent")
        if not parent:
            break
        pid = str(parent["kernel_id"])
        if pid in seen:                 # cycle guard (fragmented data)
            break
        seen.add(pid)
        (chain if is_operating(parent) else skipped).append(parent)
        cur = pid
        if pid == top_op_id:            # reached the answer - stop climbing
            break
    if not chain or str(chain[-1]["kernel_id"]) != top_op_id:
        chain.append(top_op)            # make sure the apex is present and last

    return {"kernel_id": kid, "is_own_top_operating": False,
            "top_operating_parent": top_op, "operating_chain": chain,
            "skipped": skipped, "ultimate_parent": root.get("top_parent")}

# --- run it -------------------------------------------------------------------
kid = resolve_kernel_id(name="Ben & Jerry's", country="United States")   # or a known Kernel ID
r = top_operating_parent(kid)

if r["is_own_top_operating"]:
    print(f"{kid} is its own top operating parent - it IS the top of its operating tree")
else:
    top = r["top_operating_parent"]
    print(f"Top operating parent: {name_of(top)}  "
          f"(kernel_id {top['kernel_id']}, {top.get('website') or '-'})")
    print("Operating chain (bottom -> top):")
    for node in r["operating_chain"]:
        print(f"  ^ {name_of(node)}  ({node.get('entity_sub_category')}, kernel_id {node['kernel_id']})")
    if r["skipped"]:
        print("Skipped (non-operating): "
              + ", ".join(f"{name_of(n)} [{n.get('entity_sub_category')}]" for n in r["skipped"]))
if r["ultimate_parent"] and r["top_operating_parent"] and \
   str(r["ultimate_parent"].get("kernel_id")) != str(r["top_operating_parent"].get("kernel_id")):
    print(f"(Ultimate parent above it: {name_of(r['ultimate_parent'])} - non-operating apex)")
```

## What you produce

- the top operating parent per account: name, `kernel_id`, website — the rollup / assignment target
- `is_own_top_operating`: whether the account is already the top of its operating tree
- the operating chain, bottom → top: every operating company between the account and the answer
- the non-operating entities skipped (HoldCos, investment vehicles, business units), so the rollup is
  explainable
- the ultimate parent above it, when a non-operating apex sits on top of the tree

## Notes

- If you only need the answer and not the chain, the first `/resolve-parent` call is enough — read
  `top_operating_parent` and stop. The walk exists to make the result explainable.
- Batch over a CSV by running lookups concurrently and caching `/resolve-parent` responses per
  `kernel_id`: accounts in the same tree share most of their chain, so the cache saves most calls.
- Government and Education trees work differently: entities there may have no subtype, so treat the
  tree root itself as the rollup target rather than forcing an "operating" pick.
- Beware fragmentation: the same company occasionally appears as two adjacent Kernel entities in a
  chain. Collapse consecutive nodes with an identical name or `kernel_id`; never merge on shared
  website alone — regional subsidiaries legitimately share the parent's domain.
- Errors: 403 invalid or missing key, 429 rate limited, 500 server error.

## Setting up your API key (do this first, or nothing above will run)

Every call needs a Kernel API key in `KERNEL_API_KEY`. Until it is set, the prompt above fails with a
403 and returns nothing useful.

**Get a key:**
- Already a Kernel customer: create one in the app at app.kernel.ai (Settings > API keys).
- Not a customer yet: request one at https://kernel.ai/kernel-api.
- Kernel already sent you a key: you are ready for the next step.

**Set it up:** save the key to a `.env` file in your project (never commit `.env`):

```
KERNEL_API_KEY=your-key-here
```

Load it with `from dotenv import load_dotenv; load_dotenv()` in Python, or
`export KERNEL_API_KEY=your-key-here` in your shell for the curl examples.

**Test the connection before running the prompt.** A `202` means the key works; a `403` means it is
missing or wrong, so fix it before continuing:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.kernel.ai/rest/v1/entity-resolution \
  -H "x-api-key: $KERNEL_API_KEY" -H "Content-Type: application/json" -d '{"legal_name":"Stripe"}'
```

## Other Kernel use cases

- **Resolve**: company signals to a KERN ID and identity.
- **Climb to the ultimate parent**: the same climb, continued past operating companies to the absolute
  apex of the tree.
- **Collapse hierarchies / plug data gaps / batch problem accounts**: other practical workflows.

````

{% endprompt %}

#### Just the script

The same logic as a ready-to-run CLI — copy it into `top_operating_parent.py` and run it directly:

```bash
export KERNEL_API_KEY=your-key-here
pip install requests

python3 top_operating_parent.py 7913528487                                   # by Kernel ID
python3 top_operating_parent.py "Ben & Jerry's" --country "United States"   # by name
```

<details>

<summary>top_operating_parent.py — expand for the full script</summary>

```python
#!/usr/bin/env python3
"""Find the top operating parent for any account — the highest operating company
in its ownership tree, skipping holding companies and investment vehicles.

One /v1/resolve-parent call answers it; the walk up fills in the operating-only
chain and the non-operating entities skipped along the way. Takes a Kernel ID,
or a company name (resolved first).
"""
import argparse
import os
import re
import sys
import time

import requests

BASE = os.environ.get("KERNEL_API_BASE_URL", "https://api.kernel.ai/rest")
MAX_DEPTH = 25                        # safety cap; real trees converge well before this
KERN_RE = re.compile(r"^\d{6,12}$")   # Kernel IDs are numeric
NON_OPERATING = {"HoldCo/Investment", "Business Unit", "Academic Unit", "Establishment"}


def run_job(path, body, headers):
    """Kernel jobs are async: submit, then poll until the job completes."""
    job_id = requests.post(f"{BASE}/{path}", headers=headers, json=body).json()["id"]
    delay = 2
    while True:
        time.sleep(delay)
        data = requests.get(f"{BASE}/{path}/{job_id}", headers=headers).json()
        if data["status"] == "failed":
            sys.exit(f"{path} job failed: {data}")
        if data["status"] == "completed":
            return data.get("record", {})
        delay = min(delay + 5, 30)


def is_operating(node):
    return bool(node) and node.get("entity_category") == "Company" \
        and node.get("entity_sub_category") not in NON_OPERATING


def name_of(node):
    if not node:
        return None
    return node.get("trading_name") or node.get("legal_name") or node.get("kernel_id")


def top_operating_parent(kernel_id, headers):
    """One call answers it; the walk up fills in the operating chain."""
    kid = str(kernel_id)
    root = run_job("v1/resolve-parent", {"kernel_id": kid}, headers)
    top_op = root.get("top_operating_parent")
    top_op_id = str(top_op["kernel_id"]) if top_op else None

    if top_op_id in (None, kid):        # the account IS the top of its operating tree
        return {"kernel_id": kid, "is_own_top_operating": top_op_id == kid,
                "top_operating_parent": top_op, "operating_chain": [], "skipped": [],
                "ultimate_parent": root.get("top_parent")}

    chain, skipped, seen, cur = [], [], {kid}, kid
    for _ in range(MAX_DEPTH):
        rec = root if cur == kid else run_job("v1/resolve-parent", {"kernel_id": cur}, headers)
        parent = rec.get("parent")
        if not parent:
            break
        pid = str(parent["kernel_id"])
        if pid in seen:                 # cycle guard (fragmented data)
            break
        seen.add(pid)
        (chain if is_operating(parent) else skipped).append(parent)
        cur = pid
        if pid == top_op_id:            # reached the answer - stop climbing
            break
    if not chain or str(chain[-1]["kernel_id"]) != top_op_id:
        chain.append(top_op)            # make sure the apex is present and last

    return {"kernel_id": kid, "is_own_top_operating": False,
            "top_operating_parent": top_op, "operating_chain": chain,
            "skipped": skipped, "ultimate_parent": root.get("top_parent")}


def main():
    ap = argparse.ArgumentParser(description="Find the top operating parent for an account.")
    ap.add_argument("query", help="A Kernel ID, or a company name to resolve first")
    ap.add_argument("--website", help="Sharpens the match when resolving by name")
    ap.add_argument("--country", help="Sharpens the match when resolving by name")
    args = ap.parse_args()

    key = os.environ.get("KERNEL_API_KEY")
    if not key:
        sys.exit("Set KERNEL_API_KEY first (see 'Setting up your API key')")
    headers = {"x-api-key": key, "Content-Type": "application/json"}

    if KERN_RE.match(args.query.strip()):
        kid = args.query.strip()
    else:
        body = {k: v for k, v in {"legal_name": args.query, "website": args.website,
                                  "country": args.country}.items() if v}
        kid = run_job("v1/entity-resolution", body, headers).get("kernel_id")
        if not kid:
            sys.exit(f"Could not resolve '{args.query}' to a Kernel entity")
        print(f"Resolved to kernel_id {kid}")

    r = top_operating_parent(kid, headers)
    if r["is_own_top_operating"]:
        print(f"{kid} is its own top operating parent - it IS the top of its operating tree")
    else:
        top = r["top_operating_parent"]
        print(f"Top operating parent: {name_of(top)}  "
              f"(kernel_id {top['kernel_id']}, {top.get('website') or '-'})")
        print("Operating chain (bottom -> top):")
        for node in r["operating_chain"]:
            print(f"  ^ {name_of(node)}  "
                  f"({node.get('entity_sub_category')}, kernel_id {node['kernel_id']})")
        if r["skipped"]:
            print("Skipped (non-operating): "
                  + ", ".join(f"{name_of(n)} [{n.get('entity_sub_category')}]"
                              for n in r["skipped"]))
    up, top = r["ultimate_parent"], r["top_operating_parent"]
    if up and top and str(up.get("kernel_id")) != str(top.get("kernel_id")):
        print(f"(Ultimate parent above it: {name_of(up)} - non-operating apex)")


if __name__ == "__main__":
    main()
```

</details>
{% endtab %}
{% endtabs %}
