Table Layout
The table layout is the default display for list actions. It presents data in a familiar tabular format with sortable columns, pagination, and row-click navigation to detail views.
Default behaviour
When you create a list action in your schema, Keel automatically generates a table tool. Without any configuration, the table:
- Displays all response fields as columns in schema order
- Enables sorting on fields marked with
@sortable - Provides cursor-based pagination with a default page size of 50
- Links row clicks to the model's get action (if one exists)
model Order {
fields {
reference Text @unique
status OrderStatus
customer Customer
totalAmount Decimal
createdAt Timestamp
}
actions {
get getOrder(id)
list listOrders(status?, customer.id?) {
@sortable(reference, createdAt, totalAmount)
}
}
}This schema produces a table tool with sortable columns for reference, createdAt, and totalAmount. Clicking any row navigates to the getOrder tool with the order's ID.
Column configuration
Columns are configured through response field settings in your tool's JSON file. Each field under $.results becomes a column.
{
"action_name": "listOrders",
"response": {
"$.results.reference": {
"display_name": "Order Ref",
"display_order": 1
},
"$.results.status": {
"display_name": "Status",
"display_order": 2
},
"$.results.customer.name": {
"display_name": "Customer",
"display_order": 3
},
"$.results.totalAmount": {
"display_name": "Total",
"display_order": 4
},
"$.results.createdAt": {
"display_name": "Created",
"display_order": 5
},
"$.results.id": {
"visible": false
}
}
}Column options
| Option | Type | Description |
|---|---|---|
display_name | string | Column header label |
display_order | number | Position in the table (lower numbers appear first) |
visible | boolean | Whether the column is shown (default: true) |
visible_condition | string | CEL expression for conditional visibility |
help_text | string | Tooltip shown on column header hover |
image_preview | boolean | Display file fields as inline thumbnails |
link | object | Make the cell a clickable link to another tool |
Sorting
Table sorting is controlled at the schema level using the @sortable attribute on your list action:
actions {
list listOrders() {
@sortable(reference, createdAt, totalAmount)
}
}Only fields declared in @sortable will have clickable column headers. Users can click a column header to sort ascending, click again for descending, and click a third time to clear the sort.
Default sort order
Without user interaction, list actions sort by createdAt descending (newest first). To change the default, use @orderBy:
actions {
list listOrders() {
@orderBy(reference: asc)
@sortable(reference, createdAt, totalAmount)
}
}When a user clicks a sortable column, their choice overrides the @orderBy default until they navigate away.
Filtering
Configure quick search to let users filter results by typing:
{
"action_name": "listOrders",
"filter_config": {
"quick_search_field": "$.where.reference.contains"
}
}The quick search field maps to a contains filter on your list action's inputs. As users type, the table filters to show matching records.
For more advanced filtering, define input fields on your list action:
actions {
list listOrders(status?, customer.id?, reference?) {
@sortable(reference, createdAt)
}
}Each input appears as a filter control above the table.
Pagination
Tables use cursor-based pagination by default. Configure the page size in your tool's JSON file:
{
"action_name": "listOrders",
"pagination": {
"page_size": {
"default_value": 25
}
}
}Pagination options
| Option | Type | Description |
|---|---|---|
page_size.default_value | number | Number of rows per page (default: 50) |
Users can navigate between pages using the pagination controls at the bottom of the table. The controls show the current page position and total record count when available.
Row click navigation
When a user clicks a table row, the table navigates to the model's get action. Keel automatically configures this when:
- The list tool exists for a model
- A get action exists for the same model that accepts an
idinput
The row's id is passed to the get action, which loads and displays the full record.
Customising row navigation
Override the default navigation with get_entry_action:
{
"action_name": "listOrders",
"get_entry_action": {
"tool_id": "get-order",
"data_mapping": [
{
"key": "id",
"path": { "path": "$.id" }
}
]
}
}To disable row click navigation entirely, set get_entry_action to null or mark it as deleted.
Cell links
Make individual cells clickable by adding a link to the response field:
{
"action_name": "listOrders",
"response": {
"$.results.reference": {
"display_name": "Order Ref",
"link": {
"tool_id": "get-order",
"data_mapping": [
{
"key": "id",
"path": { "path": "$.id" }
}
]
}
},
"$.results.customer.name": {
"display_name": "Customer",
"link": {
"tool_id": "get-customer",
"data_mapping": [
{
"key": "id",
"path": { "path": "$.customer.id" }
}
]
}
}
}
}Cell links are useful for relationship fields, allowing users to navigate directly to related records.
Toolbar actions
Add action buttons to the table toolbar with create_entry_action and entry_activity_actions:
{
"action_name": "listOrders",
"create_entry_action": {
"tool_id": "create-order",
"title": "New Order",
"as_dialog": true
}
}The create action button appears in the table toolbar, giving users quick access to add new records.
Complete example
Here's a full configuration for an order management table:
enum OrderStatus {
Pending
Processing
Shipped
Delivered
Cancelled
}
model Customer {
fields {
name Text
email Text @unique
}
actions {
get getCustomer(id)
list listCustomers()
}
}
model Order {
fields {
reference Text @unique
status OrderStatus @default(OrderStatus.Pending)
customer Customer
totalAmount Decimal
createdAt Timestamp @default(now())
}
actions {
get getOrder(id)
list listOrders(status?, customer.id?) {
@sortable(reference, createdAt, totalAmount)
@orderBy(createdAt: desc)
}
create createOrder() with (reference, status, customer.id, totalAmount)
update updateOrder(id) with (status?)
}
}{
"id": "list-orders",
"action_name": "listOrders",
"name": "Orders",
"help_text": "View and manage customer orders",
"filter_config": {
"quick_search_field": "$.where.reference.contains"
},
"pagination": {
"page_size": {
"default_value": 25
}
},
"response": {
"$.results.reference": {
"display_name": "Order Ref",
"display_order": 1,
"link": {
"tool_id": "get-order",
"data_mapping": [
{ "key": "id", "path": { "path": "$.id" } }
]
}
},
"$.results.status": {
"display_name": "Status",
"display_order": 2
},
"$.results.customer.name": {
"display_name": "Customer",
"display_order": 3,
"link": {
"tool_id": "get-customer",
"data_mapping": [
{ "key": "id", "path": { "path": "$.customer.id" } }
]
}
},
"$.results.totalAmount": {
"display_name": "Total",
"display_order": 4
},
"$.results.createdAt": {
"display_name": "Created",
"display_order": 5
},
"$.results.id": {
"visible": false
},
"$.results.customer.id": {
"visible": false
}
},
"create_entry_action": {
"tool_id": "create-order",
"title": "New Order",
"as_dialog": true
},
"get_entry_action": {
"tool_id": "get-order"
}
}This configuration creates a table that:
- Shows orders with sortable columns for reference, total, and date
- Filters by order status and customer
- Provides quick search on order reference
- Displays 25 records per page
- Links the reference column and customer name to their respective detail views
- Includes a "New Order" button that opens the create form in a dialog
- Navigates to order details when clicking any row