run()

August 9, 2026 ยท View on GitHub

Starts the same Starlette application returned by app_of() with Uvicorn.

run(
    fns,
    *,
    title: str | None = None,
    capture_prints: bool | None = None,
    max_upload_bytes: int | None = None,
    pending_ttl: int | timedelta | None = 3600,
    returns_ttl: int | timedelta | None = 3600,
    uploads_dir: str | Path | None = None,
    returns_dir: str | Path | None = None,
    theme: Theme = "system",
    host: str = "127.0.0.1",
    port: int = 8000,
    uvicorn_kwargs: dict[str, Any] | None = None,
) -> None
from func_to_web import run


run([add, divide], title="Internal tools")

It accepts exactly the same input as app_of(), with the same errors, and capture_prints, max_upload_bytes, pending_ttl, returns_ttl, uploads_dir, returns_dir and theme are passed on unchanged to the application, which validates them all before Uvicorn starts. title, host and port are the three it handles itself: by default the server listens on 127.0.0.1:8000. The theme also applies to the index, which shares the space with the pages it frames.

FUNCTOWEB_UPLOADS_DIR and FUNCTOWEB_RETURNS_DIR say from outside the code what uploads_dir and returns_dir say in it, which is the usual place for a deployment to say it:

run(functions, uploads_dir="/srv/functoweb/uploads")

Which two directories were in force is not left to be guessed: run() announces them, with the version, on the line before the server starts.

FuncToWeb 2.5.0
UPLOADS DIR: /srv/functoweb/uploads
RETURNS DIR: /tmp/FuncToWeb/returns

The call blocks until the server stops, and returns nothing.

app_of() = integration
run()       = minimal standalone application

A real integration does not go through here: when you already have a host application with its own authentication, routes and deployment, what you mount is the application.

What it builds

A Starlette application with the space title and these routes:

/                       index of the registered functions
/{slug}/                the page of each function
/{slug}/invoke          its execution
/{slug}/invoke-stream   the same execution, streamed over SSE
/doc                    the space document
/static/{path}          the shared assets
/upload                 only if some function has a file field
/returns/{ref}          only if some function declares a Download

The last two are conditional: they appear only if some function needs them. app_of() builds exactly this same application.

Uvicorn options

uvicorn_kwargs is passed to uvicorn.run(...) and copied before use, so the caller's dictionary is never modified.

run(
    divide,
    uvicorn_kwargs={"log_level": "debug"},
)

Some keys are reserved because they are already explicit arguments; setting them raises TypeError instead of being silently ignored:

TypeError: uvicorn_kwargs cannot contain 'host'; use the host argument
TypeError: uvicorn_kwargs cannot contain 'port'; use the port argument
TypeError: uvicorn_kwargs cannot contain 'app'; run() builds it internally

The space index

The page at / is deliberately minimal: one link per function, in space order, with the name and the description its WebFunction already carries.

<a href="#add" data-slug="add"><strong>Add</strong><span>Add two numbers.</span></a>
<a href="#division" data-slug="division"><strong>Divide numbers</strong></a>

With no description there is no <span>: no text is invented and no gap is left. Both values are escaped, and the space title is the <title> of the page and the heading of the sidebar. The name is formatted the same way as on the function page (_ becomes spaces and the first letter is capitalized), so add in the example reads as Add. See web-function.md.

The selection lives in the URL hash, which is the function slug, and that is why each function is linkable:

/#add        opens /add/
/#division   opens /division/

An empty or unknown hash opens the first function instead of leaving the page blank, and the chosen one opens in an <iframe> that only navigates same-origin pages. Why it navigates with location.replace(), and why the index carries no form logic of its own, is in design/run.md.

/doc appears as another entry in the sidebar, with no data-slug and opening in a new tab.

The HTML is composed at startup, with the space already prepared, and every request to / returns the same text from memory.

Related: router.md, getting-started.md, web-function.md, sdk.md.