Custom Schemas
Extend stores with custom fields for your domain.
Learning stores use predefined schemas by default. Extend them with custom fields to capture domain-specific information.
Setup
pip install agno openai sqlalchemy "psycopg[binary]" pgvector
export OPENAI_API_KEY="your-api-key"The examples use a local PostgreSQL database on port 5532. With Docker running, start it using:
Run PgVector
docker run -d \
-e POSTGRES_DB=ai \
-e POSTGRES_USER=ai \
-e POSTGRES_PASSWORD=ai \
-e PGDATA=/var/lib/postgresql \
-v pgvolume:/var/lib/postgresql \
-p 5532:5432 \
--name pgvector \
agnohq/pgvector:18Later configuration fragments reuse the imports and db from the first complete example.
Create the database object used by the configuration snippets:
from agno.agent import Agent
from agno.db.postgres import PostgresDb
from agno.models.openai import OpenAIResponses
db = PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5532/ai")Extending User Profile
The default UserProfile includes name and preferred_name. Add fields for your domain:
from dataclasses import dataclass, field
from typing import Optional
from agno.learn.schemas import UserProfile
@dataclass
class CustomerProfile(UserProfile):
company: Optional[str] = field(
default=None,
metadata={"description": "Company or organization"}
)
plan_tier: Optional[str] = field(
default=None,
metadata={"description": "Subscription tier: free | pro | enterprise"}
)
role: Optional[str] = field(
default=None,
metadata={"description": "Job title or role"}
)
timezone: Optional[str] = field(
default=None,
metadata={"description": "User's timezone"}
)Use the custom schema:
from agno.learn import LearningMachine, UserProfileConfig
agent = Agent(
model=OpenAIResponses(id="gpt-5.2"),
db=db,
learning=LearningMachine(
user_profile=UserProfileConfig(schema=CustomerProfile),
),
)Field Guidelines
Use Metadata Descriptions
The metadata={"description": ...} tells the LLM what to extract:
# Good: Clear description guides extraction
role: Optional[str] = field(
default=None,
metadata={"description": "Job title like 'Data Scientist' or 'Engineering Manager'"}
)
# Less effective: No description
role: Optional[str] = NoneUse Optional Fields
All custom fields should be Optional with defaults:
# Good
company: Optional[str] = field(default=None, metadata={"description": "Company or organization"})
# Bad: no default, raises TypeError when the class is defined
# (a required field can't follow the optional fields on the base schema)
company: strDocument Constrained Values
For fields with known options, list them in the description:
plan_tier: Optional[str] = field(
default=None,
metadata={"description": "Subscription tier: free | pro | enterprise"}
)Dataclass annotations and field descriptions do not validate model-generated values. Profile update tools expose custom values as strings; normalize and validate values in application code when you need typed numbers or constrained choices.
For serialized agent configurations that must load in another process, define custom schemas in an importable Python module, rather than __main__.
Domain Examples
SaaS Support
@dataclass
class SupportProfile(UserProfile):
company: Optional[str] = field(
default=None,
metadata={"description": "Company name"}
)
plan: Optional[str] = field(
default=None,
metadata={"description": "Plan: starter | professional | enterprise"}
)
account_id: Optional[str] = field(
default=None,
metadata={"description": "Account or customer ID"}
)
primary_use_case: Optional[str] = field(
default=None,
metadata={"description": "Main use case or workflow"}
)Developer Tools
@dataclass
class DeveloperProfile(UserProfile):
primary_language: Optional[str] = field(
default=None,
metadata={"description": "Primary language: python | javascript | go | rust"}
)
framework: Optional[str] = field(
default=None,
metadata={"description": "Primary framework: react | django | fastapi"}
)
experience_years: Optional[str] = field(
default=None,
metadata={"description": "Years of programming experience"}
)
editor: Optional[str] = field(
default=None,
metadata={"description": "Editor: vscode | neovim | intellij"}
)Extending Other Schemas
Entity Memory
from agno.learn.schemas import EntityMemory
@dataclass
class CompanyEntity(EntityMemory):
industry: Optional[str] = field(
default=None,
metadata={"description": "Industry: fintech | healthcare | saas"}
)
funding_stage: Optional[str] = field(
default=None,
metadata={"description": "Stage: seed | series_a | series_b | public"}
)
employee_count: Optional[int] = field(
default=None,
metadata={"description": "Number of employees"}
)Learned Knowledge
from typing import List, Optional
from agno.learn.schemas import LearnedKnowledge
@dataclass
class TechnicalInsight(LearnedKnowledge):
applicable_languages: Optional[List[str]] = field(
default=None,
metadata={"description": "Languages this applies to"}
)
performance_impact: Optional[str] = field(
default=None,
metadata={"description": "Performance impact: high | medium | low"}
)
complexity: Optional[str] = field(
default=None,
metadata={"description": "Complexity: simple | moderate | complex"}
)These subclasses extend stored data, and can be supplied as EntityMemoryConfig(schema=CompanyEntity) or LearnedKnowledgeConfig(schema=TechnicalInsight). Their default tools have fixed parameters: they do not automatically capture these additional fields. Populate custom fields through an application-controlled write path; use facts or the base learning text with the default tools.
Full Example
from dataclasses import dataclass, field
from typing import Optional
from agno.agent import Agent
from agno.db.postgres import PostgresDb
from agno.learn import LearningMachine, UserProfileConfig
from agno.learn.schemas import UserProfile
from agno.models.openai import OpenAIResponses
@dataclass
class EnterpriseProfile(UserProfile):
company: Optional[str] = field(
default=None,
metadata={"description": "Company name"}
)
department: Optional[str] = field(
default=None,
metadata={"description": "Department: engineering | sales | marketing"}
)
role: Optional[str] = field(
default=None,
metadata={"description": "Job title"}
)
region: Optional[str] = field(
default=None,
metadata={"description": "Region: NA | EMEA | APAC"}
)
agent = Agent(
model=OpenAIResponses(id="gpt-5.2"),
db=PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5532/ai"),
learning=LearningMachine(
user_profile=UserProfileConfig(schema=EnterpriseProfile),
),
)
# Custom fields extracted automatically
agent.print_response(
"Hi, I'm Sarah Chen, VP of Engineering at Acme Corp. We're the EMEA team.",
user_id="sarah@acme.com",
)