Building Custom Providers

Create your own context provider for any data source.

When the built-in providers don't fit, subclass ContextProvider. The base class handles tool wrapping, name derivation, and error shaping.

Minimal Example

Create a virtual environment using SDK setup, then install the model dependency and set your key:

uv pip install -U agno openai
export OPENAI_API_KEY="your-openai-api-key"

Save as faq_provider.py and run python faq_provider.py. The FAQ data is a local illustration.

import asyncio

from agno.agent import Agent
from agno.models.openai import OpenAIResponses
from agno.context import Answer, ContextProvider, Status

FAQ = {"pricing": "See agno.com/pricing", "support": "Email support@agno.com"}

class FAQContextProvider(ContextProvider):
    def status(self) -> Status:
        return Status(ok=True, detail=f"{len(FAQ)} entries")

    async def astatus(self) -> Status:
        return self.status()

    def query(self, question: str, *, run_context=None) -> Answer:
        key = next((k for k in FAQ if k in question.lower()), None)
        return Answer(text=FAQ[key] if key else "No FAQ entry matches that.")

    async def aquery(self, question: str, *, run_context=None) -> Answer:
        return self.query(question, run_context=run_context)

faq = FAQContextProvider(id="faq")
agent = Agent(model=OpenAIResponses(id="gpt-5.4-mini"), tools=faq.get_tools())

if __name__ == "__main__":
    asyncio.run(agent.aprint_response("How do I contact support?"))

The agent now has a query_faq tool. Same shape as every built-in provider.

Required Methods

You must implement these four abstract methods:

MethodPurpose
query(question, *, run_context=None) -> AnswerSync read
aquery(question, *, run_context=None) -> AnswerAsync read
status() -> StatusSync health check
astatus() -> StatusAsync health check

Answer

Answer is what query() returns. These fragments belong inside a query method:

from agno.context import Answer, Document

# Text-only answer
return Answer(text="The weather is sunny.")

# Answer with source documents
return Answer(
    text="Found 3 matching policies.",
    results=[
        Document(id="doc1", name="Refund Policy", uri="/policies/refund.md", snippet="..."),
        Document(id="doc2", name="Privacy Policy", uri="/policies/privacy.md", snippet="..."),
    ]
)

Status

Status reports provider health. These fragments belong inside a status method:

from agno.context import Status

# Healthy
return Status(ok=True, detail="Connected to database")

# Unhealthy
return Status(ok=False, detail="API key invalid")

Optional Methods

Override these to customize behavior:

MethodDefaultOverride when
update() / aupdate()Raises NotImplementedErrorProvider supports writes
asetup()No-opNeed async init (MCP sessions, cache priming)
aclose()No-opHold long-lived state (watches, connections)
instructions()Generic guidanceWant source-specific usage hints

Adding Write Support

Override update(), aupdate(), and _default_tools() for writable providers:

class NotesContextProvider(ContextProvider):
    def __init__(self, id: str, storage: dict):
        super().__init__(id)
        self.storage = storage

    def query(self, question: str, *, run_context=None) -> Answer:
        matches = [v for k, v in self.storage.items() if question.lower() in k.lower()]
        return Answer(text="\n".join(matches) if matches else "No matching notes.")

    async def aquery(self, question: str, *, run_context=None) -> Answer:
        return self.query(question, run_context=run_context)

    def update(self, instruction: str, *, run_context=None) -> Answer:
        if instruction.startswith("save note:"):
            parts = instruction[10:].split(" - ", 1)
            if len(parts) == 2:
                self.storage[parts[0].strip()] = parts[1].strip()
                return Answer(text=f"Saved note: {parts[0].strip()}")
        return Answer(text="Could not parse instruction. Use: save note: <title> - <content>")

    async def aupdate(self, instruction: str, *, run_context=None) -> Answer:
        return self.update(instruction, run_context=run_context)

    def _default_tools(self) -> list:
        return self._read_write_tools()  # Exposes both query and update tools

    def status(self) -> Status:
        return Status(ok=True, detail=f"{len(self.storage)} notes")

    async def astatus(self) -> Status:
        return self.status()

Continue the FAQ script with this class and replace its agent construction with:

notes = NotesContextProvider(id="notes", storage={})
agent = Agent(model=OpenAIResponses(id="gpt-5.4-mini"), tools=notes.get_tools())

The agent now has query_notes and update_notes. This dictionary is a process-local demonstration; choose durable storage for a deployed service.

Async Lifecycle

This partial class illustrates setup and teardown; implement the remaining abstract methods before instantiating it. Install aiohttp for this example. Call asetup() before queries and aclose() in finally:

class StreamingAPIContextProvider(ContextProvider):
    def __init__(self, id: str, api_url: str):
        super().__init__(id)
        self.api_url = api_url
        self.session = None

    async def asetup(self) -> None:
        import aiohttp
        self.session = aiohttp.ClientSession()

    async def aclose(self) -> None:
        if self.session:
            await self.session.close()

    async def aquery(self, question: str, *, run_context=None) -> Answer:
        async with self.session.get(f"{self.api_url}/search", params={"q": question}) as resp:
            data = await resp.json()
            return Answer(text=data.get("answer", "No answer found."))

    # ... implement query, status, astatus

Custom Instructions

Override instructions() to provide source-specific guidance:

def instructions(self) -> str:
    return """
    Use query_jira for:
    - Finding issues by key (e.g., "PROJ-123")
    - Searching by assignee, status, or labels
    - Getting sprint information

    Use update_jira for:
    - Changing issue status
    - Adding comments
    - Updating assignee
    """

Using RunContext

The run_context parameter carries caller state. This method fragment assumes your application has authenticated the caller and implemented get_docs_for_user and search. Choose an explicit missing-user policy; the illustrative fallback below exposes only public documents:

def query(self, question: str, *, run_context=None) -> Answer:
    user_id = run_context.user_id if run_context else None

    if user_id:
        # Fetch user-specific data
        user_docs = self.get_docs_for_user(user_id)
        return Answer(text=self.search(question, user_docs))

    # Fall back to global search
    return Answer(text=self.search(question, self.public_docs))

Available on run_context:

FieldDescription
user_idIdentifies the caller
session_idIdentifies the conversation
metadataArbitrary dict passed through the call chain
dependenciesValues injected via the agent's dependencies parameter

Wrapping External APIs

This illustrative contract uses a fictional endpoint, not a real weather API. Replace the base URL, authentication, request fields and response parsing with your service’s documented contract. Install httpx. Both HTTP clients are scoped to their calls, and async methods perform async I/O:

import httpx
from agno.context import Answer, ContextProvider, Status

class WeatherContextProvider(ContextProvider):
    def __init__(self, id: str, api_key: str, base_url: str = "https://example.invalid"):
        super().__init__(id, write=False)
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")

    def _params(self, question: str) -> dict:
        return {"city": question.removeprefix("weather in").strip(), "key": self.api_key}

    def _answer(self, response: httpx.Response) -> Answer:
        response.raise_for_status()
        data = response.json()
        return Answer(text=f"{data['temp']}F, {data['condition']}")

    def query(self, question: str, *, run_context=None) -> Answer:
        with httpx.Client(timeout=10) as client:
            return self._answer(client.get(f"{self.base_url}/current", params=self._params(question)))

    async def aquery(self, question: str, *, run_context=None) -> Answer:
        async with httpx.AsyncClient(timeout=10) as client:
            return self._answer(await client.get(f"{self.base_url}/current", params=self._params(question)))

    def status(self) -> Status:
        try:
            with httpx.Client(timeout=10) as client:
                client.get(f"{self.base_url}/health").raise_for_status()
            return Status(ok=True, detail="API reachable")
        except httpx.HTTPError as exc:
            return Status(ok=False, detail=str(exc))

    async def astatus(self) -> Status:
        try:
            async with httpx.AsyncClient(timeout=10) as client:
                (await client.get(f"{self.base_url}/health")).raise_for_status()
            return Status(ok=True, detail="API reachable")
        except httpx.HTTPError as exc:
            return Status(ok=False, detail=str(exc))

Next