> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognee.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom GLiNER Extraction

> Drive the GLiNER task list yourself to pass your own entity and relation labels and measure what extraction kept

A minimal guide to driving GLiNER extraction yourself instead of calling `cognify()`. You build the task list with `get_gliner_tasks()`, hand it the exact entity and relation labels you want, and read back a stats object reporting what the model proposed and what survived. A local GLiNER2 model does the extraction and writes the chunk summaries, so no `extract_content_graph` and no `extract_summary` calls are made — reach for this when the default labels are not the ones your graph needs, or when you want extraction loss measured rather than assumed.

## Before You Start

* Complete [Quickstart](/getting-started/quickstart) to understand basic operations
* Install the extra: `pip install "cognee[gliner]"`. The first run downloads `fastino/gliner2.5-base-v1` (\~800 MB) into the Hugging Face cache
* Configure an [embedding provider](/setup-configuration/embedding-providers) — extraction and summaries are LLM-free, but `add_data_points` still embeds what it stores, and the default provider is OpenAI. To keep the text on your machine too, pair this with the local fastembed setup in [Recall Without an LLM Key](/guides/no-llm-remember-recall)
* Read [Pipelines](/core-concepts/building-blocks/pipelines) and [Tasks](/core-concepts/building-blocks/tasks) — this guide runs a task list directly instead of calling `cognify()`
* No data is required up front — the script ingests its own passage, but it starts with `prune_data()` and `prune_system()`, which wipe all existing Cognee data; run it against a setup you can afford to reset
* The optional search at the end runs only when `LLM_API_KEY` is set; everything before it does not need one

## Code in Action

```python theme={null}
import asyncio
import os

import cognee
from cognee.context_global_variables import set_database_global_context_variables
from cognee.infrastructure.databases.graph import get_graph_engine
from cognee.modules.users.methods import get_default_user
from cognee.tasks.graph.gliner import GlinerRunStats, get_gliner_tasks

TEXT = """
Tim Cook is the chief executive officer of Apple Inc., headquartered in Cupertino,
California. Before joining Apple in 1998 he worked at Compaq and IBM. Apple was
founded by Steve Jobs, Steve Wozniak and Ronald Wayne in 1976 and today produces
the iPhone, the Mac and the Apple Watch. In 2014 Apple acquired Beats Electronics,
the headphone company co-founded by Dr. Dre and Jimmy Iovine, for three billion
dollars. Apple Park, the company's campus, opened in 2017.
"""

DATASET = "gliner_demo"


async def main():
    await cognee.prune.prune_data()
    await cognee.prune.prune_system(metadata=True)

    await cognee.add(TEXT, dataset_name=DATASET)
    user = await get_default_user()

    stats = GlinerRunStats()
    tasks = await get_gliner_tasks(
        entity_types={
            "person": "Full name of a human being",
            "organization": "Company or institution",
            "location": "City, region, campus or country",
            "product": "Commercial product",
            "date": "Year or calendar date",
            "money": "Monetary amount",
        },
        relation_types=["works_for", "headquartered_in", "founded_by", "produces", "acquired"],
        stats=stats,
    )
    # Every task in this list declares needs_llm=False, so the first-run
    # check probes only the embeddings it does use — no flag needed.
    await cognee.run_custom_pipeline(
        tasks=tasks,
        user=user,
        dataset=DATASET,
        pipeline_name="cognify_pipeline",
    )

    print("\nschemas by document:")
    for document_id, schema in stats.schemas_by_document.items():
        print(f"  {document_id} ({schema.source})")
        print(f"    entity types:   {sorted(schema.entity_types)}")
        print(f"    relation types: {sorted(schema.relation_types)}")
    print(f"\nchunks processed: {stats.chunks}")
    print(f"nodes mapped:     {stats.nodes}")
    print(
        f"relations: {stats.candidate_edges} proposed, "
        f"{stats.kept_edges} kept, {stats.dropped_edges} dropped (endpoint did not resolve)"
    )

    datasets = await cognee.datasets.list_datasets(user)
    dataset = next(d for d in datasets if d.name == DATASET)
    async with set_database_global_context_variables(dataset.id, dataset.owner_id):
        graph = await get_graph_engine()
        nodes, edges = await graph.get_graph_data()

    by_type: dict[str, int] = {}
    for _node_id, props in nodes:
        by_type[props.get("type", "?")] = by_type.get(props.get("type", "?"), 0) + 1
    print(f"\nstored graph: {len(nodes)} nodes, {len(edges)} edges")
    for type_name, count in sorted(by_type.items()):
        print(f"  {type_name}: {count}")

    summaries = [props for _id, props in nodes if props.get("type") == "TextSummary"]
    print(f"\nTextSummary nodes: {len(summaries)}")
    for props in summaries:
        print("  ---")
        print("  " + (props.get("text") or "<empty>").replace("\n", "\n  "))

    structural = {"contains", "is_a", "is_part_of", "made_from", "belongs_to_set"}
    entity_edges = sorted(
        {(str(edge[0]), edge[2], str(edge[1])) for edge in edges if edge[2] not in structural}
    )
    print(f"\nentity relations stored: {len(entity_edges)}")
    names = {str(node_id): props.get("name") for node_id, props in nodes}
    for source, relation, target in entity_edges:
        print(f"  {names.get(source, source)} --{relation}--> {names.get(target, target)}")

    if os.getenv("LLM_API_KEY"):
        from cognee import SearchType

        results = await cognee.search(
            query_text="Who leads Apple and where is it based?",
            query_type=SearchType.SUMMARIES,
            datasets=[DATASET],
            top_k=3,
        )
        print("\nSUMMARIES search (per dataset):")
        for per_dataset in results:
            for item in per_dataset.get("search_result", []):
                print("  -", (item.get("text") or "<empty>").replace("\n", " | "))


if __name__ == "__main__":
    asyncio.run(main())
```

## What Just Happened

### Step 1: Ingest the Text

```python theme={null}
    await cognee.add(TEXT, dataset_name=DATASET)
    user = await get_default_user()
```

`add()` stores the passage in the `gliner_demo` dataset exactly as it would on the LLM path — only the extraction step changes later. The pipeline is run explicitly rather than through `cognify()`, so it needs a user to run as; `get_default_user()` is the one `cognify()` would have used.

### Step 2: Declare the Extraction Schema

```python theme={null}
    stats = GlinerRunStats()
    tasks = await get_gliner_tasks(
        entity_types={
            "person": "Full name of a human being",
            "organization": "Company or institution",
            "location": "City, region, campus or country",
            "product": "Commercial product",
            "date": "Year or calendar date",
            "money": "Monetary amount",
        },
        relation_types=["works_for", "headquartered_in", "founded_by", "produces", "acquired"],
        stats=stats,
    )
```

GLiNER extracts against a closed schema: it only finds the types you name, so the labels here decide what the graph can contain. `entity_types` accepts either a list of names or a `name -> description` mapping, and the descriptions help the model tell similar labels apart. `get_gliner_tasks()` returns the full task list — classify, prepare schema, chunk, extract and summarize, store — and fills `stats` in as the run progresses.

### Step 3: Run the Pipeline Without an LLM

```python theme={null}
    # Every task in this list declares needs_llm=False, so the first-run
    # check probes only the embeddings it does use — no flag needed.
    await cognee.run_custom_pipeline(
        tasks=tasks,
        user=user,
        dataset=DATASET,
        pipeline_name="cognify_pipeline",
    )
```

`run_custom_pipeline()` loads the dataset's records itself, the same way `cognify()` does, and runs the GLiNER task list over them. Because every task declares `needs_llm=False`, the first-run readiness check probes only the embedding provider the pipeline actually uses — a missing LLM key is not an error on this path.

### Step 4: Read the Run Stats

```python theme={null}
    print(f"\nchunks processed: {stats.chunks}")
    print(f"nodes mapped:     {stats.nodes}")
    print(
        f"relations: {stats.candidate_edges} proposed, "
        f"{stats.kept_edges} kept, {stats.dropped_edges} dropped (endpoint did not resolve)"
    )
```

`GlinerRunStats` records what the model proposed against what survived mapping. A relation is dropped when one of its endpoints does not resolve to an extracted entity, so the kept-versus-dropped split measures extraction loss instead of leaving you to guess at it. `stats.schemas_by_document` additionally reports which schema each document was given and where it came from.

### Step 5: Inspect the Stored Graph

```python theme={null}
    datasets = await cognee.datasets.list_datasets(user)
    dataset = next(d for d in datasets if d.name == DATASET)
    async with set_database_global_context_variables(dataset.id, dataset.owner_id):
        graph = await get_graph_engine()
        nodes, edges = await graph.get_graph_data()
```

Each dataset has its own graph database, so reading it back means entering that dataset's context first. The prints that follow count nodes by type, dump the `TextSummary` nodes — the summaries GLiNER wrote, with no `extract_summary` call behind them — and list the non-structural edges as `source --relation--> target`.

## Advanced Usage

<AccordionGroup>
  <Accordion title="Let the Schema Resolve Itself">
    Dropping `entity_types` and `relation_types` does not disable the schema — it changes where the schema comes from. `get_gliner_tasks()` then falls through to the configured OWL ontology (`ontology_file_path` overrides the `ONTOLOGY_FILE_PATH` setting), and if there is none, to frozen label banks probed once per document against a sketch of its text.

    ```python theme={null}
    # Schema from the configured ontology, or the label banks if there is none
    tasks = await get_gliner_tasks(stats=stats)
    ```

    Read `stats.schemas_by_document` to see which of the three paths each document took — `schema.source` names the origin of the labels it was extracted with. This is the one place that answer is visible, so it is worth printing whenever you are not passing labels yourself.
  </Accordion>

  <Accordion title="Skip the Task List Entirely">
    The task list above is what you want when you need the stats object or your own labels. For LLM-free extraction on its own, `cognify(extractor="gliner")` selects the same list for you — there is nowhere to pass labels on that path, so the schema always resolves itself as described above. See [LLM-free extraction with GLiNER](/python-api/cognify#llm-free-extraction-with-gliner) for the `GRAPH_EXTRACTOR` setting and the arguments the extractor rejects.
  </Accordion>
</AccordionGroup>

<Columns cols={2}>
  <Card title="Recall Without an LLM Key" icon="unplug" href="/guides/no-llm-remember-recall">
    Run the whole loop — embeddings included — with no API key configured at all.
  </Card>

  <Card title="Custom Tasks and Pipelines" icon="workflow" href="/guides/custom-tasks-pipelines">
    Build and run your own task list, the mechanism this guide borrows.
  </Card>

  <Card title="Ontologies" icon="map-plus" href="/core-concepts/further-concepts/ontologies">
    Where the schema comes from when you do not pass labels yourself.
  </Card>

  <Card title="Embedding Providers" icon="layers" href="/setup-configuration/embedding-providers">
    Configure the one provider this pipeline still needs.
  </Card>
</Columns>
