app.py means to Pixeltable, and what pxt schema and pxt service do with it.
One file, two commands
app.py declares two kinds of things. TableModel classes describe tables. FastAPIRouter objects describe services: named sets of HTTP routes over those tables. Two commands turn the file into something running:
my_app, the catalog directory that holds the tables and services (my_app/docs, my_app/ingest).
Tables and columns
title, and an insert without one fails. body can be left out because its type includes None. Media types work the same way: give a pxt.Video, pxt.Image, pxt.Audio or pxt.Document column a local path or a URL, and Pixeltable keeps track of the file, downloading and caching it when it’s a URL.
A line with = is an assignment: a computed column. Pixeltable evaluates it for each row and stores the result. It can call a built-in function like pxtf.string.upper(), a model provider like openai.chat_completions(), or your own Python function marked with @pxt.udf (UDFs). Pixeltable records your function by its module path from the project root, app.excerpt here, so if you rename app.py, the columns that call it can’t compute until the name matches again. A cast sets a computed column’s type: .astype(pxt.String) fails the insert when the value is None, so use .astype(pxt.String | None) when it can be missing.
pxt.Column(...) covers what a plain annotation or assignment can’t say: a primary key, as with id, a column computed on read with stored=False, or a destination= bucket for the media a column produces.
When computed columns run
Each assignment refers to the columns it reads, so Pixeltable knows what depends on what. That decides what runs:- An insert computes the new rows. Rows already in the table aren’t touched, so loading 100 more videos costs 100 videos of work.
- An update recomputes what depends on the columns you changed. Change a row’s
titleand Pixeltable recomputes itstitle_upperandsummary. - A new computed column is computed for every row already in the table.
- A changed expression applies to new rows.
pxt schema diffsays that existing values will not be recomputed, andpxt schema updateprints the command that does it:pxt recompute my_app/docs summary. That rerunssummaryon every row, plus the columns, views and indexes built from it.--no-cascadeleaves the dependent columns alone.
insert(..., on_error='ignore') stores the row anyway and keeps the error on the failed column, in docs.summary.errortype and docs.summary.errormsg. pxt errors my_app/docs lists those rows (it needs a primary key to name them), and pxt recompute my_app/docs summary --errors-only reruns only them.
Every insert, update, recompute and schema change makes a new version of the table. pxt history my_app/docs lists the versions and pxt revert my_app/docs undoes the last one, data and schema together (Version control).
Views and iterators
ATableModel class with base= is a view of another table. It has its base’s columns plus its own, and it updates when rows arrive in the base. Add an iterator and each base row becomes many view rows:
videos and frames gets one row per second of it, each with a thumbnail. pxtf.document.document_splitter() does the same for the chunks of a document, which is how a RAG app gets one row per chunk. Without an iterator, a view can filter its base: base=Docs.where(Docs.title != '') keeps the rows that have a title. More in Views and Iterators.
Indexes
Indexes go on theTableModel class, in __indexes__, so pxt schema update creates and drops them along with the table:
.similarity() ranks rows against a query. A B-tree index speeds up filters and lookups on a column, and Pixeltable creates one only where you list it. Details: Embedding indexes.
Services
FastAPIRouter is one service, named by name=. Each route turns a POST into a table operation:
pxt service update app.py my_app runs each service as its own background process, on a port it picks, and pxt service list shows the URLs. A service reads its table definitions when it starts. After pxt schema update changes a table, the service’s insert, update, delete and compute routes answer HTTP 409 until pxt service update restarts it, on the same port. Adding a route is additive. Changing or removing one needs --allow-destructive, because something may still be calling it.
pxt service run app.py my_app serves in your terminal instead, in the foreground, which is what a container entrypoint wants. File uploads, background jobs, mounting the routes on your own FastAPI app and calling them from TypeScript are in HTTP serving. To skip HTTP entirely and insert from Python, see Self-hosting.
The catalog
The catalog is where your tables live. Locally, that’s a Postgres database and a media folder that Pixeltable manages in its home directory,~/.pixeltable unless you set PIXELTABLE_HOME (Infrastructure).
Tables sit in directories. pxt schema update app.py my_app creates my_app and puts every table in app.py under it: my_app/docs, my_app/videos. Run pxt schema update app.py my_app_dev and you get a second set of the same tables, empty, which is a good place to try a change first.
pxt ls my_app, pxt describe my_app/docs and pxt rows my_app/docs show what’s there, and pxt dashboard opens a local UI. From Python, pxt.get_table('my_app.docs') returns the table.
On Pixeltable Cloud, pxt db update pxt://org:db uploads the project to a hosted database, and then the same two commands take that URI in place of my_app. Pixeltable Cloud goes from pxt login to a running service.
diff, update and prune
pxt schema compares app.py with the tables in the catalog, and pxt service compares it with the running services. Both have the same three verbs:
pxt schema update applies safe changes, like a new column, right away. A change that loses data, like dropping a column, needs --allow-destructive, and without it update exits 3 and changes nothing. A change Pixeltable can’t make in place, like a new type for an existing column, shows up as UNSUPPORTED in the plan, and update refuses it until you change the file or the table. update only touches the tables app.py declares. Tables you’ve removed from the file stay until prune drops them, and prune asks first.
Because diff reports through its exit code, pxt schema diff app.py my_app doubles as a CI check that the tables under my_app match the file. Every flag is in the CLI reference.
Next
HTTP serving
Uploads, background jobs, query routes and your own FastAPI app.
Pixeltable Cloud
The same
app.py on a hosted database, from pxt login to a running service.