# Forms and intake (/use-cases/document-processing/forms-and-intake)



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.

<Note>
  `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.
</Note>

Before running these examples, create and activate a Python environment, then install the provider and set its key:

```bash
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.

```python
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 [#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.

```python
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] = None
```

For KYC, every field is review-worthy. Combine this schema with the [confidence pattern](/use-cases/data-labeling/structured-extraction#per-field-confidence) so the downstream queue knows what to send to a compliance reviewer.

## Multi-page applications [#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.

```python
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.

```python
class References(BaseModel):
    items: List[str] = Field(default_factory=list)
```

## Schema-shape comparison [#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 [#next-steps]

| Task                                      | Guide                                                                           |
| ----------------------------------------- | ------------------------------------------------------------------------------- |
| Process every PDF in a Drive folder       | [Batch and durability](/use-cases/document-processing/batch-and-durability)     |
| Flag low-confidence KYC fields for review | [Human routing and eval](/use-cases/document-processing/human-routing-and-eval) |
| Validate extraction against a labeled set | [Human routing and eval](/use-cases/document-processing/human-routing-and-eval) |

## Developer Resources [#developer-resources]

* [Document extraction cookbook](https://github.com/agno-agi/agno/tree/main/cookbook/data_labeling/_16_document_extraction)
* [Structured output](/input-output/structured-output/agent)
