io.display.table

Rows from your database, as a table.

io.display.table shows records as a table, on a page or in a step of an action. Pass the rows as they come from the database and say which columns to show. A name can link to the action that handles it. Every row can have a menu of its own.

The simplest one

The whole action file, and roughly what your team sees when it runs.

src/actions/users/listUsersAction.ts
import { app } from '../../app';
import { prisma } from '../../db';
export const listUsersAction = app.defineAction({
name: 'List users',
handler: async ({ io }) => {
await io.display.table('All users', {
data: await prisma.user.findMany(),
columns: ['name', 'email', 'createdAt'],
});
},
});
The code. One call in an action, the whole file.
tools.example.com/actions/users/list_users

List users

All usersFilter
namecreatedAt
Ana RuizOct 16, 2026, 9:30 AM
Ben CarterOct 14, 2026, 4:12 PM
Mia ChenOct 9, 2026, 11:05 AM
Omar HaddadOct 2, 2026, 8:47 AM
Lena FischerSep 28, 2026, 2:20 PM
Continue
In the browser. Roughly what your team sees. Your theme decides the details.

What you can set

The settings you will reach for most, each with the line of code that turns it on.

data: await prisma.user.findMany()

Straight from the database. Pass the records as your query returns them. Relations and dates included, nothing to map first.

columns: ['name', 'email', 'createdAt']

Columns by key. Name the fields to show. A typo in a key is a type error, not an empty column.

renderCell: (row) => row.account.name

Cells you build. renderCell gets the whole record and returns what the cell shows, such as the name of a related account.

highlightColor: 'green'

Colored badges. Put a label in a badge of one of eight colors: online in green, failed in red.

{ label, route: storeAction.route() }

Links. A cell can open another action with its parameters filled in, or a page outside the app in a new tab.

rowMenuItems: (row) => [roleAction.route()]

A menu on every row. The … at the end of a row lists the actions for that record. Each row can get a different set.

pageSize: 50, filterable: true

Filter and paging. A filter above the table and a pager under it, both in the browser. Twenty rows a page unless you say otherwise.

'createdAt', 'credits', 'isActive'

Values people can read. Dates, numbers and yes or no come out formatted. An empty value reads N/A.

emptyText: 'No refunds waiting.'

Your own empty text. When there are no rows, the table says so in your words instead of a blank box.

Write your first tool today.

An async function, a few await calls, and the form is in the browser.

AwaitIO is free for small teams, made possible by companies and people who sponsor it.