Prefill and hidden parameters
August 9, 2026 · View on GitHub
A prefill is a set of temporary initial values for one specific opening: it does not change the function, its schema, its plan or its base page.
It has two entry points: page_of() from Python and the prefill query
parameter of GET /{slug}/; the second builds on the first. Two more travel
alongside it, and all three describe one opening, never the function.
prefill → proposes initial values
hidden → decides which parameters are not shown
autorun → asks the page to submit itself once it is ready
They are independent: hidden does not need prefill and vice versa. A
function can also open another function in the space with its own return value
as the prefill, without building the URL by hand: OpenForm
uses this channel.
HTTP API
GET /{slug}/?prefill=<JSON object>&hidden=<JSON list of names>&autorun=1
The root JSON of the prefill must be an object. Its keys are parameter
names and its values travel in the browser transport format
http.md describes, the same one /invoke uses.
const url = new URL("/tools/edit_user/", location.origin);
url.searchParams.set("prefill", JSON.stringify({name: "Ana", age: 32}));
iframe.src = url;
The two entry points end at the same place, page_of(), and the walk from the
query parameter down to it is one path written twice →
design/prefill.md.
Any error gets a 400 before the page is served, never a half-filled
form:
| Query | Response |
|---|---|
prefill={not json | 400 prefill must be valid JSON |
prefill=[1, 2] | 400 prefill must be a JSON object |
prefill={"nope": 1} | 400 unknown prefill field: 'nope' |
prefill={"task_id": "two"} | 400 task_id: expected int, got str |
prefill={"title": "ab"} | 400 title: too short: 2 chars, minimum 3 |
prefill={"priority": "URGENT"} | 400 priority: expected Priority, got str |
prefill={"due": "30/07/2026"} | 400 due: expected date, got str |
hidden=[not json | 400 hidden must be valid JSON |
hidden={"a":1} | 400 hidden must be a JSON array |
hidden=["a",1] | 400 hidden must contain only strings |
hidden=["nope"] | 200: an unknown name hides nothing |
hidden=["a","a"] | 200: it is hidden once |
hidden=[] | 200: the same as not sending it |
From Python the same failures arrive as exceptions, with the default: prefix
that reveals how the prefill is applied:
ValueError: unknown prefill field: 'nope'
SchemaTypeError: age: default: expected int, got str
SchemaValueError: age: default: too large: 999, maximum 120
What is base and what is temporary
WebFunction.schema, WebFunction.plan and WebFunction.html are compiled
once when the WebFunction is created and never change.
Without prefill or hidden, that base HTML is returned directly. With either of
them, and only for that opening, a temporary Signature is created, the
values are applied as temporary defaults, plan_of() validates them and
generates a plan for that opening alone, and that plan is rendered. Nothing is
stored anywhere: two openings with different prefills share no state.
Partial prefill
A prefill does not have to cover every parameter. Parameters that do not appear keep their original state: a required parameter stays required, a declared default stays in place, and label, description and constraints do not change. A required parameter included in the prefill gets a temporary default for that opening.
File prefill
A file field can be prefilled too, and it follows the same path as any other value, with one extra step at each end: the reference is resolved against the storage before the core certifies it, and it is the reference again —never the resolved path— that is written into the plan.
reference → file_resolver → real path → the core certifies
→ the reference is the temporary default
→ the widget shows it as the current
file, with no pending upload
The local path certifies and is dropped there: it never reaches the browser, not in the URL, not in the body and not in the message of a rejection, which names the file and not where it is kept.
Like the rest of the prefill, it fails before the page is served:
| Query | Response |
|---|---|
prefill={"document": "report-<uuid>.pdf"} | 200, with the file in place |
prefill={"document": "nope.pdf"} | 400 File not found: nope.pdf |
prefill={"document": "../evil.pdf"} | 400 invalid file reference '../evil.pdf': is not a file of the storage directory |
prefill={"document": "/etc/passwd"} | 400 invalid file reference '/etc/passwd': is not a file of the storage directory |
prefill={"document": "notas.txt"} | 400 document: not an accepted file type: 'notas.txt', expected one of ('.pdf',) |
From Python the same rule applies to the value itself, and there the failure is
an exception, since page_of() takes a real path and there is no resolver to
turn one into a reference:
ValueError: document: default: file is not in the storage directory: 'x.pdf'
It works at any depth (list[File], dataclasses with a file, lists of
dataclasses), because decode() is what walks the structure.
A prefilled file is not uploaded again: it travels in the body of /invoke
like any other value and the resolver recognizes it. See files.md.
Hidden parameters
hidden takes the widget out of the page. What happens to the parameter is
exactly this:
- it stays compiled into the form, with its value (the prefill's, or its own default);
form.read()still includes it, so it travels in the body of/invokeand/invoke-streamlike any other parameter;- it still takes part in validation: hiding a parameter that has no value available makes the submission fail with the usual message, naming a field that is not visible.
The channel only checks the shape: valid JSON, a list at the root, and every
element exactly a str. Duplicates, ordering and unknown names are not its
business, because hiding is a visual matter and a misspelled name does not
break any contract.
It hides, it does not lock
GET /task/?prefill={"task_id":7}&hidden=["task_id"]
→ the form does not show task_id
POST /task/invoke {"task_id": 9, ...}
→ it runs with 9
hidden is not access control: the value travels in the body and a direct
call can send a different one. Authentication and permissions belong to the
host application. See security.md.
A CRUD on a single function
The host application builds the links:
def link_of(identifier: int, task: Task) -> str:
prefill = {
"task_id": identifier,
"title": task.title,
"due": task.due.isoformat(),
"priority": task.priority.name,
"done": task.done,
}
return "/task/?" + urlencode({"prefill": json.dumps(prefill)})
A link with no query opens the create form; each link with a prefill edits one record. An iframe needs no channel of its own: it opens that same URL.
Limits
The prefill travels in the URL, with everything that implies (history,
Referer, access log), and there is no other channel. A prefill proposes, it
does not impose. The full list is in limitations.md.
Running the opening on its own
autorun asks the page to press its own submit button as soon as it is
mounted. It is the third parameter of an opening and behaves like the other
two: it belongs to that opening, it changes nothing about the function, and it
travels either from Python or in the query.
GET /{slug}/?autorun=1
openModal(`${SPACE}/monthly_report`, {autorun: true});
It exists for a modal you open to see the answer, not to fill in a form: a
report, a chart, a generated file, a link. Those functions often take no
parameters, or take them all prefilled by the host, so the form has nothing to
ask and the button is a step with no decision in it. Calling /invoke from
your own code would skip the button too, but then you get JSON and have to
draw the table, the image or the download yourself — which is exactly the work
FuncToWeb has already done.
What it does is press the button, and nothing else. The click that follows is the ordinary one: the same validation, the same uploads, the same stream, the same result card, the same announcements to the host. Every rule about a run is the rule it was already.
form ready → it runs, once
form incomplete → nothing happens
An incomplete form is left untouched: no errors shown, no fields marked, nothing in red. Someone who has just opened a modal has not typed anything yet and has nothing to fix; the missing field is on screen, and their click is what the page was waiting for anyway. From that point on the page is an ordinary one, and clicking submit runs it.
Two things it is not:
- Not a loop. It presses once, at mount. A result that opens another form
with
OpenFormdoes not carryautoruninto it unless the href says so. - Not a permission. Whoever can open the page can already run the function by clicking; this only saves the click. What a page is allowed to run is decided by where the space is mounted and who reaches it → security.md.
Python API: page_of()
page_of(
web_function: WebFunction,
*,
prefill: Mapping[str, Any] | None = None,
hidden: Iterable[str] | None = None,
autorun: bool = False,
theme: Theme = "system",
) -> str
from func_to_web import WebFunction, page_of
web_function = WebFunction(edit_user)
html = page_of(web_function, prefill={"name": "Ana", "age": 32})
Returns the complete HTML of one opening. It is meant for integrations that are
more hands-on than app_of(): your own builders, custom HTML responses or
any flow that generates the page on its own.
web_functionmust be aWebFunction; a bare function, aWebFunctionsorNoneall raiseTypeError: web_function must be WebFunction.prefillcarries real Python values (date,time,Enummembers, dataclasses, lists), not their JSON transport form.hiddenholds parameter names. A barestris rejected, because it would iterate character by character:TypeError: hidden must be an iterable of str, not a single str.autorunasks the page to submit itself once mounted; anything other than aboolraisesTypeError: autorun must be bool. See Running the opening on its own.themeis the one fromapp_of(), with the same three values.- With no prefill, no hidden, no autorun and
theme="system"it returnsWebFunction.htmlunchanged.
The HTML expects the space assets at ../static/..., so it needs an application
that serves them. page_of() creates no routes and no application: that is the
job of app_of() and run().
Related: open-form.md, sdk.md, web-function.md, types.md.