Form
A form submits to your own endpoint using whatever request shape you declare — a pydantic body, Query/Form/Body parameters, or a mix.
class AddBookBody(BaseModel):
title: str
author_id: int
published_year: int | None = None
@page.form("Add Book", description="Add a new book to the catalog")
async def add_book(body: AddBookBody, session: AsyncSessionDep) -> spec.Form:
book = models.Book(**body.model_dump())
session.add(book)
await session.commit()
await session.refresh(book)
return {"message": f"Added book '{book.title}'"}Decorator
page.form(
name: str,
*,
method: spec.HttpMethod = "post",
fields: dict[str, spec.FieldConfig] | None = None,
description: str | None = None,
is_hidden: bool = False,
icon: spec.Icon | None = None,
color: spec.Color | None = None,
)The decorated function is registered at <method> /<page-id>/form/<form-id>, using whichever HTTP method you pass — get, post (the default), put, patch, delete, or head.
Return value
class FormResponse(TypedDict):
icon: NotRequired[Icon]
color: NotRequired[Color]
toast: NotRequired[str]
table: NotRequired[dict | object]
message: NotRequired[str]
type Form = FormResponse | None | strReturn None for a bare success with no message, a plain string as a shorthand for message, or a FormResponse dict for full control:
@page.form("Create Webhook", method="post")
async def create_webhook(body: WebhookBody) -> spec.Form:
return {
"icon": "webhook",
"color": "teal",
"toast": "Webhook created",
"message": f"Would create a webhook for '{body.event}' -> {body.url}",
"table": {"id": 1, "url": body.url, "event": body.event},
}toast is a brief transient notification; message is shown inline after submission; table is an arbitrary JSON payload echoed back to the user (typically the created or affected record) — it's for feedback, not for updating other components.
Reference fields
fields maps a request field name to a FieldConfig, turning a plain input into a picker sourced from another table:
class FieldConfig(TypedDict):
reference: NotRequired[str | None]
reference_field: NotRequired[str]
icon: NotRequired[Icon]
color: NotRequired[Color]from openadmin.fastapi import reference_table
@page.form(
"Add Author",
fields={
"friend": {
"reference": reference_table(get_all_authors),
"reference_field": "id",
"icon": "user",
"color": "blue",
}
},
)
async def add_author(body: AddAuthorBody, session: AsyncSessionDep) -> spec.Form: ...reference is the ID of a table (obtained with reference_table, so get_all_authors must already be decorated with @page.table(...)); reference_field is the column of that table's rows to submit as the field's value. See Implementing a Form with a Reference for the full walkthrough.
Other HTTP methods
@page.form("Rename Environment", method="patch")
async def rename_environment(
environment_id: int = Query(..., description="Environment to rename"),
new_name: str = Body(..., embed=True, description="New environment name"),
) -> str:
return f"Renamed environment #{environment_id} to '{new_name}'"Hiding a form
is_hidden=True keeps the endpoint and its spec entry but removes it from the visible page, the same as with tables and actions.