Forms and intake
Extract nested fields from resumes, applications, and KYC documents.
Forms and intake documents often combine identity fields with repeated sections such as employment, education, skills, and references. A nested Pydantic schema captures this structure in one run.
Place the resume to extract at resume.pdf in the directory where you run this code.
output_schema requests structured output and Agno attempts to parse it. A failed or unparseable run can leave content as text. Check RunStatus.completed and the expected Pydantic type before reading fields, indexing, or writing downstream. These examples stop on failure; an application can instead send the original input to a review queue. Schema validation checks the shape and constraints, so verify extracted facts against the source separately.
Before running these examples, create and activate a Python environment, then install the provider and set its key:
uv venv .venv --python 3.12
source .venv/bin/activate
uv pip install "agno[openai]"
export OPENAI_API_KEY="..."Replace every https://example.com/... PDF URL with a real PDF URL accessible to the model provider, or use File(filepath="...") for a local PDF. Create the referenced local files before running the example. Extraction quality depends on the document and model; the output comments are illustrative.
from agno.run.base import RunStatus
from typing import List, Optional
from agno.agent import Agent
from agno.media import File
from agno.models.openai import OpenAIResponses
from pydantic import BaseModel, Field
class Employment(BaseModel):
company: str
title: Optional[str] = None
start_date: Optional[str] = None
end_date: Optional[str] = Field(None, description="Null if current")
summary: Optional[str] = Field(None, description="Bullet points joined into one string")
class Education(BaseModel):
institution: str
degree: Optional[str] = None
field_of_study: Optional[str] = None
graduation_year: Optional[int] = None
class Resume(BaseModel):
full_name: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
location: Optional[str] = None
headline: Optional[str] = Field(None, description="Top-of-page summary line")
employment: List[Employment] = Field(default_factory=list)
education: List[Education] = Field(default_factory=list)
skills: List[str] = Field(default_factory=list)
agent = Agent(
model=OpenAIResponses(id="gpt-5.5"),
instructions=(
"Extract every field from the attached resume PDF. Preserve the "
"candidate's wording for titles and summaries. Use null when a "
"field is missing. Do not infer skills that are not on the page."
),
output_schema=Resume,
)
resume_run = agent.run(
"Extract this resume.",
files=[File(filepath="resume.pdf")],
)
if resume_run.status != RunStatus.completed or not isinstance(resume_run.content, Resume):
raise RuntimeError("No validated Resume; send the input to review before continuing")
resume = resume_run.content
# Illustrative output. Your values depend on resume.pdf:
# Resume(full_name='Sarah Johnson', email='sarah@example.com',
# headline='Senior Platform Engineer',
# employment=[Employment(company='Acme Corp', title='Staff Engineer',
# start_date='2023-02', end_date=None, ...),
# Employment(company='Beta Labs', title='Senior Engineer',
# start_date='2019-06', end_date='2023-01', ...)],
# education=[Education(institution='University of Texas',
# degree='B.S.', field_of_study='Computer Science',
# graduation_year=2018)],
# skills=['Python', 'PostgreSQL', 'Kubernetes', 'Terraform'])Adapt the outer model and instructions for job applications and KYC intake. Reuse the File input and Agent configuration.
KYC intake
Identity verification forms add typed fields the downstream system has to accept verbatim (passport numbers, dates of birth, addresses). The schema should be conservative about types: keep IDs as strings to preserve leading zeros and country-specific formats.
class KYCSubmission(BaseModel):
full_name: str
date_of_birth: Optional[str] = Field(None, description="ISO 8601")
country_of_residence: Optional[str] = Field(None, description="ISO 3166-1 alpha-2")
national_id_type: Optional[str] = Field(None, description="passport, driver_license, national_id")
national_id_number: Optional[str] = Field(None, description="As printed, including any leading zeros")
address: Optional[str] = None
declared_source_of_funds: Optional[str] = NoneFor KYC, every field is review-worthy. Combine this schema with the confidence pattern so the downstream queue knows what to send to a compliance reviewer.
Multi-page applications
Job applications arrive as one combined PDF or as a set of loose attachments. files takes a sequence, so the whole bundle goes into a single run against one schema. Create the three PDFs under applications/sjohnson/ before running this continuation of the first example.
from agno.run.base import RunStatus
class Application(BaseModel):
candidate: Resume
cover_letter: Optional[str] = None
references: List[str] = Field(default_factory=list)
application_agent = Agent(
model=OpenAIResponses(id="gpt-5.5"),
instructions=(
"Extract the application from the attached documents: a resume, a cover "
"letter, and a reference list. Use null when a field is missing."
),
output_schema=Application,
)
application_run = application_agent.run(
"Extract this application.",
files=[
File(filepath="applications/sjohnson/cover-letter.pdf"),
File(filepath="applications/sjohnson/resume.pdf"),
File(filepath="applications/sjohnson/references.pdf"),
],
)
if application_run.status != RunStatus.completed or not isinstance(application_run.content, Application):
raise RuntimeError("No validated Application; send the input to review before continuing")
application = application_run.content
Run once per attachment when you want an isolated schema per document, then compose in plain Python. Every run needs its own schema, because output_schema takes a Pydantic model: Resume for the resume, and a wrapper for the reference list. A bare List[str] is not a schema.
class References(BaseModel):
items: List[str] = Field(default_factory=list)Schema-shape comparison
| Workload | Header | Repeated structure | Notes |
|---|---|---|---|
| Invoice | Vendor, totals, dates | List[LineItem] | Numbers stay numeric |
| Contract | Parties, dates, governing law | List[Clause] with category Literal | Verbatim clause text |
| Resume | Identity, headline | Parallel lists: employment, education, skills | Preserve candidate wording |
| KYC | Identity | Few sub-lists; conservative typing | Keep IDs as strings |
Reuse the Agent setup across these workloads and change output_schema for the document structure.
Next steps
| Task | Guide |
|---|---|
| Process every PDF in a Drive folder | Batch and durability |
| Flag low-confidence KYC fields for review | Human routing and eval |
| Validate extraction against a labeled set | Human routing and eval |