> ## 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.

# Search Basics

> Step-by-step guide to running your first Cognee search and understanding core parameters

A minimal guide to querying Cognee memory. The current top-level flow uses `cognee.recall()`, while the lower-level `cognee.search()` API remains available when you need direct retriever control.

**Before you start:**

* Complete [Quickstart](../getting-started/quickstart) to understand basic operations
* Ensure you have [LLM Providers](../setup-configuration/llm-providers) configured for LLM-backed retrieval types
* Run `cognee.remember(...)` or otherwise build the graph before querying it
* Keep at least one dataset with `read` permission for the user running the search

## Code in Action

```python theme={null}
await cognee.remember(
    [
        "Alice moved to Paris in 2010. She works as a software engineer.",
        "Bob lives in New York. He is a data scientist.",
        "Alice and Bob met at a conference in 2015.",
    ],
    self_improvement=False,
)

answers = await cognee.recall(query_text="What are the main themes in my data?")
for answer in answers:
    print(answer)
```

<Info>
  When you call `recall()` without an explicit `query_type`, Cognee uses its default auto-routing behavior to choose the best retrieval strategy for the query. To learn more about that routing behavior and the available lower-level search types, see [Recall](/core-concepts/main-operations/recall) and [Search Types](/core-concepts/main-operations/legacy-operations/search).
</Info>

## Parameters Reference

All examples below assume you are inside an async function. Import helpers when needed:

```python theme={null}
from cognee import SearchType
from cognee.modules.engine.models.node_set import NodeSet
```

<AccordionGroup>
  <Accordion title="Core and Prompt Parameters">
    * **`query_text`** (str, required): The question or phrase to search for. On graph-completion, hybrid, and agentic searches (including the default `GRAPH_COMPLETION`), it must be a string that still has content after trimming, so a whitespace-only query such as `"   "` or `"\t\n"` is rejected with `QueryValidationError` instead of being embedded and matched against nearest neighbours; other search types such as `CHUNKS` and `SUMMARIES` do not apply this check. Trimming is used only for that validity check — the string you pass is forwarded to retrieval unchanged.
      ```python theme={null}
      answers = await cognee.recall(query_text="Who owns the rollout plan?")
      ```
    * **`query_type`** (SearchType, optional): Sets the retrieval mode. With `recall()`, omitting it enables auto-routing by default; if you pass it explicitly, that strategy is used directly. See [Search Types](/core-concepts/main-operations/legacy-operations/search) for the full list and [Retrievers](/core-concepts/main-operations/legacy-operations/search#retrievers) for how each type maps to a retriever. To pick a type by latency and recall depth, see the [speed, cost, and recall depth comparison](/python-api/search-type#speed-cost-and-recall-depth).
      ```python theme={null}
      await cognee.recall(
          query_text="List coding guidelines",
          query_type=SearchType.CODING_RULES,
      )
      ```
    * **`top_k`** (int, optional, default: 15): Maximum number of results to return. It must be a positive integer, or `None` — which falls back to the retriever's own default (`5`) for graph-completion searches but removes the result cap entirely for vector searches such as `CHUNKS` and `SUMMARIES`. `top_k=0` or a negative value raises `QueryValidationError` for every search type, before any retriever is constructed.
      ```python theme={null}
      await cognee.recall(query_text="Summaries please", top_k=3)
      ```
    * **`system_prompt_path`** (str, optional, default: `"answer_simple_question.txt"`): Path to a prompt file packaged with your project.
      ```python theme={null}
      await cognee.recall(
          query_text="Explain the roadmap in bullet points",
          system_prompt_path="prompts/bullets.txt",
      )
      ```
    * **`system_prompt`** (Optional\[str]): Inline prompt string. Overrides `system_prompt_path` when set.
      ```python theme={null}
      await cognee.recall(
          query_text="Give me a confident answer",
          system_prompt="Answer succinctly and state confidence at the end.",
      )
      ```
    * **`only_context`** (bool, optional, default: False): Skip the LLM completion step and return the retrieved context directly. This avoids the final LLM call and is useful when you want to inspect or reuse the context yourself.

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

      async def main():
          results = await cognee.recall(
              query_text="What did we promise the client?",
              only_context=True,
          )

          if isinstance(results, str):
              # Single-dataset searches may unwrap to a plain string.
              print(results)
          elif results and isinstance(results[0], dict):
              # With access control enabled, each item is grouped by dataset.
              for dataset_result in results:
                  print("Dataset:", dataset_result["dataset_name"])
                  print("Context:", dataset_result["search_result"])
          else:
              # Otherwise, results is typically a list of context strings.
              for context_text in results:
                  print(context_text)

      asyncio.run(main())
      ```

          <Note>
            `only_context=True` works with any search type. For LLM-completion types (`GRAPH_COMPLETION`, `RAG_COMPLETION`, etc.) it returns the text that would have been sent to the LLM. For retrieval-only types (`CHUNKS`, `SUMMARIES`) the behavior is effectively unchanged because no final LLM call is made.
          </Note>
  </Accordion>

  <Accordion title="Advanced Parameters">
    * **`wide_search_top_k`** (int, optional, default: 100): Caps initial candidate retrieval for graph-completion retrievers before ranking. Increase for broader recall on large graphs.
    * **`triplet_distance_penalty`** (float, optional, default: 6.5): Penalty applied in graph retrieval ranking. Controls how triplet distance influences final result ordering.
    * **`retriever_specific_config`** (dict, optional): Per-retriever options. Examples: `response_model` for typed LLM output; `max_iter` for GRAPH\_COMPLETION\_COT; `context_extension_rounds` for GRAPH\_COMPLETION\_CONTEXT\_EXTENSION. For COT latency tuning, see [GRAPH\_COMPLETION\_COT](/core-concepts/main-operations/legacy-operations/search#graph_completion_cot) and the [SearchType speed comparison](/python-api/search-type#speed-cost-and-recall-depth).

      ```python theme={null}
      await cognee.recall(
          query_text="What is the current state of the project?",
          query_type=SearchType.GRAPH_COMPLETION,
          datasets=["project_memory"],
          retriever_specific_config={
              "include_global_context_index": True,
              "global_context_index_top_k": 3,
          },
      )
      ```

      Use `include_global_context_index` after building the index with `cognee.improve(..., build_global_context_index=True)`. See [Global Context Index](/core-concepts/further-concepts/global-context-index).

          <Note>
            For lower-level `search()` calls, a non-string `response_model` keeps the structured completion in the result payload as a model instance, plain `dict`, or list of those. Model instances serialize to dictionaries when the payload is dumped. With `recall()`, graph completions are normalized into `ResponseGraphEntry` objects: the validated payload lands in `result.structured` (mirrored in `result.raw`), and `result.text` stays renderable for display. `recall()` also accepts the model directly via its first-class [`response_model` parameter](/python-api/recall#structured-output-with-response_model).
          </Note>
    * **`verbose`** (bool, optional, default: False): When `true`, results include `text_result`, `context_result`, and `objects_result` fields alongside the answer.
    * **`include_references`** (bool, optional, default: True): When `true`, completion-style answers (`GRAPH_COMPLETION`, `RAG_COMPLETION`, etc.) get a deterministic `Evidence:` block appended to the answer text, citing the source chunks or graph context. Set to `False` to restore the exact prior answer text. See [Citation and Source Tracking](#citation-and-source-tracking) for details.
      ```python theme={null}
      # Disable the appended Evidence block
      await cognee.recall(
          query_text="What did we promise the client?",
          include_references=False,
      )
      ```
  </Accordion>

  <Accordion title="Node Sets & Filtering Parameters">
    These options scope retrieval to specific node sets. With `recall()`, pass `node_name` and optionally `node_name_filter_operator` — use the same names you passed to `cognee.add(..., node_set=[...])`. See [NodeSets](/core-concepts/further-concepts/node-sets) for background.

    **`node_name`** (Optional\[List\[str]]): Names of the node sets to include.

    <Accordion title="`node_name` example">
      ```python theme={null}
      await cognee.recall(
          query_text="What discounts did TechSupply offer?",
          node_name=["vendor_conversations"],
      )
      ```
    </Accordion>

    **`node_name_filter_operator`** (str, optional, default: `"OR"`): Controls how multiple node-set names are combined. `"OR"` returns results connected to **any** of the listed node sets; `"AND"` returns results connected to **all** of them.

    <Accordion title="`node_name_filter_operator` example">
      ```python theme={null}
      # OR (default) — results touching any of the listed node sets
      await cognee.recall(
          query_text="Summarize procurement rules",
          node_name=["procurement_policies", "purchase_history"],
          node_name_filter_operator="OR",
      )

      # AND — results that belong to every listed node set
      await cognee.recall(
          query_text="What topics span both domains?",
          node_name=["procurement_policies", "purchase_history"],
          node_name_filter_operator="AND",
      )
      ```
    </Accordion>

    <Note>
      Node-set filtering applies to graph-completion search types (`GRAPH_COMPLETION`, `GRAPH_COMPLETION_COT`, `GRAPH_COMPLETION_CONTEXT_EXTENSION`, `GRAPH_SUMMARY_COMPLETION`, `TEMPORAL`, `RAG_COMPLETION`, `TRIPLET_COMPLETION`, `CHUNKS`). It has no effect on `SUMMARIES`, `CYPHER`, or `NATURAL_LANGUAGE`.
    </Note>
  </Accordion>

  <Accordion title="Interaction & History Parameters">
    * **`session_id`** (Optional\[str]): Links this recall to a conversation session. With `recall()`, passing `session_id` by itself makes Cognee search session cache entries first; if nothing matches, it falls through to graph retrieval. When `session_id` is reused across completion-style recalls, previous Q\&A turns can also be included in the prompt context. If omitted while caching is enabled, Cognee writes to the dataset-scoped default session — `default_session_<dataset_id>` when the dataset is known, falling back to the global `default_session` when it is not — so recalls against different datasets never share one session. See [What Is a Session?](/core-concepts/sessions-and-caching#what-is-a-session).
      ```python theme={null}
      # Session-first recall: checks session cache before graph search
      await cognee.recall(
          query_text="Where does Alice live?",
          session_id="conversation_1"
      )

      # Add datasets when you want to force graph-backed recall in session-aware flows
      await cognee.recall(
          query_text="What does she do for work?",
          datasets=["people_demo"],
          session_id="conversation_1"
      )
      ```
      See [Sessions Guide](/guides/sessions) for complete examples. To record feedback on answers, see the [Feedback System](/guides/feedback-system).
  </Accordion>

  <Accordion title="Datasets & Users">
    * **`datasets`** (Optional\[Union\[list\[str], str]]): Limit search to specific dataset names.
      ```python theme={null}
      await cognee.recall(
          query_text="Key risks",
          datasets=["risk_register", "exec_summary"],
      )
      ```

    * **`dataset_ids`** (Optional\[Union\[list\[UUID], UUID]]): Same as `datasets`, using UUIDs instead of names.

      ```python theme={null}
      from uuid import UUID
      await cognee.recall(
          query_text="Customer feedback",
          dataset_ids=[UUID("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")],
      )
      ```

          <Warning>
            With backend access control enabled, `datasets=["name"]` resolves names only within datasets owned by the current user. If Bob is searching Alice's shared dataset `shared_dataset`, searching by name can fail even when Bob has read access. Use `dataset_ids=[shared_id]` for shared datasets the user did not create.
          </Warning>

    * **`user`** (Optional\[User]): The user to run the search as. Required for multi-tenant flows or background jobs.

      ```python theme={null}
      from cognee.modules.users.methods import get_user
      user = await get_user(user_id)
      await cognee.recall(query_text="Team OKRs", user=user)
      ```

      **When** `ENABLE_BACKEND_ACCESS_CONTROL=true`:

      * **Result shape**: Searches run only on datasets the user can access. Results are returned as a list of per-dataset objects (`dataset_name`, `dataset_id`, `search_result`). Use `verbose=True` to include `text_result`, `context_result`, and `objects_result` in each item.

      * **Parallel execution**: Multiple datasets are searched concurrently using `asyncio.gather()` — total time is roughly that of the slowest single-dataset search.

      * If no `user` is given, `get_default_user()` is used (created if missing); an error is raised only if this user lacks dataset permissions.

      * If `datasets` is not set, all datasets readable by the user are searched. An error is raised if none are accessible or if a requested dataset is forbidden.

          <Warning>
            `PermissionDeniedError` will be raised unless you search with the same user that added the data or grant access to the default user.
          </Warning>

      **When** `ENABLE_BACKEND_ACCESS_CONTROL=false`:

      * Dataset filters (`datasets`, `dataset_ids`) are ignored — all data is searched.
      * Results are returned as a plain list (e.g. `["answer1", "answer2"]`). If only one dataset is searched and the retriever returns a list, Cognee may unwrap one level for backwards compatibility.
  </Accordion>
</AccordionGroup>

## Citation and Source Tracking

Provenance is available at two levels:

1. **Dataset level** — when `ENABLE_BACKEND_ACCESS_CONTROL=true`, results are wrapped with `dataset_name` and `dataset_id`.
2. **Chunk/summary level** — `CHUNKS` and `SUMMARIES` results include an `id` you can use to look up the item in the graph.

### Evidence block (`include_references`)

For completion-style answers (`GRAPH_COMPLETION`, `RAG_COMPLETION`, and similar), Cognee appends a short `Evidence:` block to the answer text by default (`include_references=True`). It lists the chunks or graph context the answer was grounded in — for example, `- chunk 2 of document policy.pdf: "…"`.

* **Deterministic and in-process**: the block is assembled locally from the retrieved payloads or graph context. There is **no extra LLM call** and no prompt injection — it is appended after completion generation.
* **Response schema unchanged**: Evidence is added to the answer **text only**. Return types and result shapes are the same as before, and non-string `response_model` paths are unaffected.
* **Graceful degradation**: chunk-level evidence is used when the vector payload carries `document_name`/`document_id`. Older indexes that predate these fields fall back to entity → chunk → document graph traversal where the backend supports it, or omit the Evidence block silently (no errors) when neither is available.
* **Opting out**: set `include_references=False` to restore the exact previous answer text. This is useful when you compare against stored snapshots or evaluation baselines, which will otherwise diff against the new Evidence block.

```python theme={null}
# Default: answer text includes an appended "Evidence:" block
await cognee.recall(query_text="What is the refund policy?")

# Restore the exact prior output (no Evidence block)
await cognee.recall(
    query_text="What is the refund policy?",
    include_references=False,
)
```

<Note>
  **Search results do not include raw source file paths.** Evidence can show document names when reference metadata is available, but `raw_data_location` stays on the `Document` node. To trace a result back to the original stored file path, use dataset provenance or query the graph by `id`.
</Note>

<AccordionGroup>
  <Accordion title="Chunk-level fields (`CHUNKS`)">
    `SearchType.CHUNKS` returns a list of dicts with these fields:

    | Field         | Type  | Description                                                         |
    | ------------- | ----- | ------------------------------------------------------------------- |
    | `id`          | `str` | Chunk UUID                                                          |
    | `text`        | `str` | Raw chunk text                                                      |
    | `chunk_index` | `int` | Position of this chunk within the source document                   |
    | `chunk_size`  | `int` | Token count of this chunk                                           |
    | `cut_type`    | `str` | How the boundary was chosen (`sentence_end`, `paragraph_end`, etc.) |

    ```python theme={null}
    import asyncio
    import cognee
    from cognee import SearchType

    async def main():
        await cognee.add("path/to/policy.pdf", dataset_name="docs")
        await cognee.cognify(datasets=["docs"])

        results = await cognee.recall(
            query_text="What is the refund policy?",
            query_type=SearchType.CHUNKS,
        )

        for chunk in results:
            print("Text:        ", chunk["text"])
            print("Chunk index: ", chunk["chunk_index"])
            print("Chunk ID:    ", chunk["id"])  # UUID for graph lookups
            print()

    asyncio.run(main())
    ```
  </Accordion>

  <Accordion title="Summary-level fields (`SUMMARIES`)">
    `SearchType.SUMMARIES` returns a list of dicts with these fields:

    | Field  | Type  | Description  |
    | ------ | ----- | ------------ |
    | `id`   | `str` | Summary UUID |
    | `text` | `str` | Summary text |

    ```python theme={null}
    results = await cognee.recall(
        query_text="What is the refund policy?",
        query_type=SearchType.SUMMARIES,
    )

    for summary in results:
        print("Summary:", summary["text"])
        print("ID:     ", summary["id"])
    ```
  </Accordion>

  <Accordion title="Look up a chunk or node directly by ID">
    For `CHUNKS` and `SUMMARIES`, `recall()` (and the lower-level `cognee.search()`) use `query_text` as a retrieval query, not as an identifier lookup. Passing a chunk or summary `id` as the query searches for semantically similar content and can return unrelated results (or nothing) — not that specific item.

    For most workflows, keep the `id` as provenance metadata and use normal `recall()` queries to retrieve related content. If you need to inspect a specific chunk, summary, or graph node during debugging, query the databases directly:

    ```python theme={null}
    import asyncio
    from cognee.infrastructure.databases.graph import get_graph_engine
    from cognee.infrastructure.databases.vector import get_vector_engine_async

    async def main():
        item_id = "your-chunk-or-summary-uuid"  # the `id` returned by a CHUNKS or SUMMARIES search

        # From the graph: returns the node's properties, or None if not found
        graph_engine = await get_graph_engine()
        node = await graph_engine.get_node(item_id)
        print(node)

        # From the vector store: choose the collection for the search type
        vector_engine = await get_vector_engine_async()
        collection = "DocumentChunk_text"  # CHUNKS results
        # collection = "TextSummary_text"  # SUMMARIES results

        rows = await vector_engine.retrieve(collection, [item_id])
        if rows:
            print(rows[0].payload["text"])

    asyncio.run(main())
    ```

    Use `get_node(node_id)` / `get_nodes(node_ids)` for graph lookups and `retrieve(collection_name, data_point_ids)` for vector-store lookups. Use `DocumentChunk_text` for `CHUNKS` IDs and `TextSummary_text` for `SUMMARIES` IDs.

    <Note>
      `get_graph_engine()` and `get_vector_engine_async()` are async (use `await` for both); the vector engine's `retrieve` method is also async. The older synchronous `get_vector_engine()` still exists as a deprecated backward-compatibility shim — calling it emits a `DeprecationWarning`, so use `get_vector_engine_async()` in new code. With `ENABLE_BACKEND_ACCESS_CONTROL=true`, these getters return the default databases — direct lookups bypass the per-dataset isolation applied by `recall()`.
    </Note>
  </Accordion>

  <Accordion title="Traverse from a chunk to its source document">
    Each `DocumentChunk` has an `is_part_of` relationship to its parent document. The chunk also carries a flat `document_id` field, so use that field when you already have the chunk object and only need the parent document id.

    For debugging or graph inspection, get the graph engine directly with `get_graph_engine()` and traverse from the chunk node.

    ```python theme={null}
    from cognee.infrastructure.databases.graph import get_graph_engine

    graph_engine = await get_graph_engine()

    # get_connections returns (source, edge, target) triples for every edge on the node
    connections = await graph_engine.get_connections(str(chunk_id))
    parent_documents = [
        target
        for source, edge, target in connections
        if edge["relationship_name"] == "is_part_of"
    ]
    ```
  </Accordion>

  <Accordion title="Dataset-level provenance (access control enabled)">
    When `ENABLE_BACKEND_ACCESS_CONTROL=true`, every result is wrapped with dataset information:

    ```python theme={null}
    import asyncio
    import cognee
    from cognee import SearchType

    async def main():
        results = await cognee.recall(
            query_text="What is the refund policy?",
            query_type=SearchType.CHUNKS,
            datasets=["docs"],
        )

        for dataset_result in results:
            print("Dataset:   ", dataset_result["dataset_name"])
            print("Dataset ID:", dataset_result["dataset_id"])
            for chunk in dataset_result["search_result"]:
                print("  Text:", chunk["text"])
                print("  Chunk ID:", chunk["id"])

    asyncio.run(main())
    ```

    When `ENABLE_BACKEND_ACCESS_CONTROL=false`, results are a plain list with no `dataset_name` or `dataset_id` wrapper.
  </Accordion>

  <Accordion title="Raw source objects (LLM-completion modes)">
    For modes that return a generated answer (`GRAPH_COMPLETION`, `RAG_COMPLETION`, etc.), use `verbose=True` to receive the raw retrieved objects alongside the answer:

    ```python theme={null}
    results = await cognee.recall(
        query_text="Summarize the launch timeline",
        verbose=True,
    )

    for result in results:
        print("Answer:         ", result.get("text_result"))
        print("Context passed: ", result.get("context_result"))
        print("Source objects: ", result.get("objects_result"))
    ```
  </Accordion>
</AccordionGroup>

## Writing Effective Queries

A good query does two jobs at once: it tells Cognee **what** you want and, through its wording, hints at **how** to retrieve it. Follow these practices to get better answers.

* **Ask a full, natural-language question.** `recall()` and the graph-completion retrievers are LLM-backed, so `"What discounts did TechSupply offer in 2023?"` works better than a bare keyword like `"discounts"`. Reserve short keyword strings for `CHUNKS_LEXICAL` or `CHUNKS`.
* **Let phrasing steer auto-routing, or set `query_type` yourself.** When you omit `query_type`, a rule-based router reads cue words in your query and picks a strategy — summary words (`summary`, `overview`, `key takeaways`) lean toward summary completion, reasoning words (`why`, `explain`, `step by step`) toward chain-of-thought, relationship words (`how are X and Y connected`, `path between`) toward context extension, and time words (`when`, `before`, `after`, or a 4-digit year) toward temporal search. If no cue matches, it defaults to `GRAPH_COMPLETION`. When you know the mode you want, pass `query_type` explicitly to bypass the router. See [Auto-routing behavior](/core-concepts/main-operations/recall#examples-and-details) and [Choosing a Search Type](/python-api/search-type#choosing-a-search-type).
* **Phrase positively.** A negation (`not`, `no`, `never`, `without`) within \~20 characters before a cue word suppresses that cue, so `"summary without dates"` will not route to summary. Rephrase or set `query_type` when a query needs negated wording.
* **Scope the query to narrow results.** Pass `datasets` to limit which knowledge base is searched and `node_name` to restrict retrieval to specific [node sets](/core-concepts/further-concepts/node-sets). Tighter scope means less noise and faster answers.
* **Tune the call to the task.** Lower `top_k` for concise answers, raise it for broad recall. Use `only_context=True` when you want the retrieved context without an LLM answer (useful for inspecting retrieval quality or feeding your own prompt). Match the search type to your latency and cost budget using the [speed, cost, and recall depth comparison](/python-api/search-type#speed-cost-and-recall-depth).

## Full Example

<Accordion title="Latest guide">
  ```python theme={null}
  import asyncio
  import cognee


  async def main():
      # Start clean (optional in your app)
      await cognee.forget(everything=True)
      # Prepare knowledge base
      await cognee.remember(
          [
              "Alice moved to Paris in 2010. She works as a software engineer.",
              "Bob lives in New York. He is a data scientist.",
              "Alice and Bob met at a conference in 2015.",
          ],
          self_improvement=False,
      )

      # Make sure you've already run cognee.remember(...) so the graph has content
      answers = await cognee.recall(query_text="What are the main themes in my data?")
      for answer in answers:
          print(answer)

  if __name__ == "__main__":
      asyncio.run(main())
  ```
</Accordion>

<Accordion title="Legacy guide">
  ```python theme={null}
  import asyncio
  import cognee

  async def main():
      # Start clean (optional)
      await cognee.prune.prune_data()
      await cognee.prune.prune_system(metadata=True)

      await cognee.add(
          [
              "Alice moved to Paris in 2010. She works as a software engineer.",
              "Bob lives in New York. He is a data scientist.",
              "Alice and Bob met at a conference in 2015.",
          ]
      )
      await cognee.cognify()

      answers = await cognee.recall(query_text="What are the main themes in my data?")
      for answer in answers:
          print(answer)

  if __name__ == "__main__":
      asyncio.run(main())
  ```
</Accordion>

<Note>
  `recall()` is the current high-level retrieval entry point. It routes the query to the best available retrieval strategy and can still use graph-backed search under the hood.
</Note>

## Additional Examples

Additional examples are available on our [GitHub](https://github.com/topoteretes/cognee/tree/main/examples/guides).

* An advanced script running this same `forget()` → `remember()` → `recall()` flow over a full real document is also on our [GitHub](https://github.com/topoteretes/cognee/blob/dev/examples/advanced_guides/simple_document_qa/simple_document_qa_demo.py). Instead of the three inline sentences used above, it passes a file path to `remember()` — the bundled full text of *Alice in Wonderland* — and then asks three plain `recall()` questions whose answers depend on reading across the whole book.

<Columns cols={2}>
  <Card title="Custom Prompts" icon="text-wrap" href="/guides/custom-prompts">
    Learn about custom prompts for tailored answers
  </Card>

  <Card title="Permission Snippets" icon="shield" href="/guides/permission-snippets">
    Multi-tenant deployment patterns
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/introduction">
    Explore all search types and parameters
  </Card>

  <Card title="Sessions" icon="message-square" href="/guides/sessions">
    Enable conversational memory with sessions
  </Card>

  <Card title="Agent Memory Decorator" icon="bot" href="/core-concepts/further-concepts/agent-memory-decorator">
    Attach retrieval to an agent function boundary
  </Card>
</Columns>
