<- Back to Complex Components

Purpose

DataTable renders model-driven tabular data with pagination, filters, sorting, inline edits, row actions, selection actions, and remote loading.

Use it for data that has a stable column schema and may grow beyond a small inline list. For non-trivial datasets, prefer remote_service so the table can request only the current page and keep filter/sort state in the client.

Constructor

DataTable(
    id: str,
    model: list[dict] | None = None,
    rows: list[dict] | None = None,
    page: int = 0,
    page_size: int = 25,
    total_rows: int = 0,
    remote_service: str | dict | None = None,
    pagination: bool | None = None,
    on_page_change: str | None = None,
    on_cell_edit: str | None = None,
    on_row_add: str | None = None,
    on_row_delete: str | None = None,
    on_filter_change: str | None = None,
    show_row_numbers: bool = False,
    selectable: bool = False,
    row_actions: list[dict] | None = None,
    selection_actions: list[dict] | None = None,
    paginated: bool = True,
    virtual: bool = False,
    auto_refresh: int | None = None,
    sort_field: str | None = None,
    sort_direction: str = "asc",
)

Properties

Property Type Default Description
model list[ColumnDef] [] Column contract. Each column reads a field from each row.
rows list[dict] [] Current rows rendered by the table.
page int 0 Zero-based current page.
page_size int 25 Rows requested or displayed per page.
total_rows int 0 Total result count across all pages.
remote_service str | dict none Action spec used for initial load, remote pagination, remote filters, remote sorting, and auto-refresh.
paginated / pagination bool true Shows pagination controls. pagination is accepted as an alias.
show_row_numbers bool false Adds a row-number column.
selectable bool false Adds row checkboxes and enables selection_actions.
row_actions list[ActionDef] [] Per-row menu actions.
selection_actions list[ActionDef] [] Bulk action buttons shown when at least one row is selected.
on_cell_edit str | dict none Action called after an editable cell changes.
on_row_add str | dict none Action called by the built-in Add row button.
on_row_delete str | dict none Legacy per-row delete action. Prefer row_actions for new tables.
on_page_change str | dict none Action called by pagination when remote_service is not used.
on_filter_change str | dict none Action called by filters when remote_service is not used.
sort dict none Serialized sort state, for example {"field": "name", "direction": "asc"}.
auto_refresh int none Seconds between remote reloads.
virtual bool false Renderer hint for large tables.

Python uses sort_field and sort_direction in the constructor; the serialized property is sort. YAML should use sort.

Action Confirmation

Use confirm inside the ActionSpec for user-triggered table actions such as on_row_add, row_actions, selection_actions, and the legacy on_row_delete. Confirmation text should use translation keys.

remote_service, pagination, filter, sort, auto-refresh, and on_cell_edit dispatch without confirmation because they are data loading or cell-edit synchronization flows.

table = sdk.ui.DataTable(
    "datatable_confirm",
    model=model,
    rows=rows,
    paginated=False,
    selectable=True,
    on_row_add={
        "name": "components.datatable_confirm_action",
        "context": {"source": "python_datatable_add", "operation": "add"},
        "confirm": {
            "text": sdk.i18n.t("components.datatable.confirm.add_prompt"),
            "confirm_text": sdk.i18n.t("components.datatable.confirm.accept"),
            "cancel_text": sdk.i18n.t("components.datatable.confirm.cancel"),
        },
    },
    row_actions=[
        {
            "label": sdk.i18n.t("components.datatable.confirm.row_action"),
            "action": {
                "name": "components.datatable_confirm_action",
                "context": {"source": "python_datatable_row", "operation": "inspect"},
                "confirm": {
                    "text": sdk.i18n.t("components.datatable.confirm.row_prompt"),
                    "confirm_text": sdk.i18n.t("components.datatable.confirm.accept"),
                    "cancel_text": sdk.i18n.t("components.datatable.confirm.cancel"),
                },
            },
        }
    ],
    selection_actions=[
        {
            "label": sdk.i18n.t("components.datatable.confirm.selection_action"),
            "variant": "danger",
            "action": {
                "name": "components.datatable_confirm_action",
                "context": {"source": "python_datatable_selection", "operation": "archive"},
                "confirm": {
                    "text": sdk.i18n.t("components.datatable.confirm.selection_prompt"),
                    "confirm_text": sdk.i18n.t("components.datatable.confirm.accept"),
                    "cancel_text": sdk.i18n.t("components.datatable.confirm.cancel"),
                },
            },
        }
    ],
)

Column Model

Each column maps one row field to one rendered cell. field is also the key used in filter, sort, edit, row-action, and remote-service payloads, so keep field names stable.

model = [
    {"field": "id", "header": "ID", "type": "int", "width": 70, "editable": False, "sortable": True},
    {"field": "name", "header": "Name", "type": "str", "editable": True, "filterable": True, "filter_type": "text"},
    {"field": "role", "header": "Role", "type": "enum", "editable": True, "filterable": True, "filter_type": "select", "options": ["Admin", "Editor", "Viewer"]},
    {"field": "active", "header": "Active", "type": "bool", "editable": True, "filterable": True, "filter_type": "boolean"},
    {"field": "joined", "header": "Joined", "type": "str", "transform": "date|%Y-%m-%d"},
    {"field": "tags", "header": "Tags", "type": "str", "transform": "join_list|, |upper"},
]

ColumnDef schema:

Key Type Required Meaning
field str yes Row key read by the cell. Every rendered row should contain this key.
header str yes Column label shown in the table header.
type str no Cell type. Supported values are str, int, float, bool, and enum. Omitted values behave like str.
editable bool | rule no Enables inline editing. It can be a boolean or a visibility/condition rule evaluated against the current row as $item.
filterable bool no Shows a filter control for the column.
filter_type str no Filter control type. Supported values are text, select, boolean, date, and number.
options list[str] no Values for enum editors and select filters.
sortable bool no Marks the column as sortable for remote sort interactions.
width int no Initial column width hint in pixels.
transform str no Display formatter applied to read-only display text.

Field Types

type Display Editable control Edit payload
str Text, optionally passed through transform. Text input. String from the input.
int Text, optionally passed through transform. Text input. String from the input; validate/cast in the action if needed.
float Text, optionally passed through transform. Text input. String from the input; validate/cast in the action if needed.
bool Disabled checkbox. Checkbox. Boolean.
enum Text, optionally passed through transform. Select using options. Selected option value.

Filter Types

filter_type Control Filter payload
text Text input. String, or absent/null when cleared.
select Select with All plus options. Selected option value, or absent/null when All is selected.
boolean Select with All, Yes, No. Boolean, or absent/null when All is selected.
date Text input. String. The renderer does not parse dates before dispatch.
number Text input. String. The renderer does not parse numbers before dispatch.

For client-side filtering, non-boolean filters compare by case-insensitive substring. For remote tables, filter values are sent to remote_service and the backend action owns parsing and matching.

Formatters

transform supports the pipe syntax:

formatter|arg1|arg2|...

The old colon syntax is still accepted for date, truncate, and get_stub for compatibility.

Formatter Syntax Input Output
upper upper Any value Uppercase string.
lower lower Any value Lowercase string.
title title Any value Title-cased string.
truncate truncate|N Any value String shortened to N characters with ellipsis. Default N is 50. Legacy: truncate:N.
date date|FORMAT ISO-like date/datetime string Formatted date string. Default format is %Y-%m-%d. Legacy: date:FORMAT.
join_list join_list|SEP|INNER List of primitive values Values joined with SEP. Optional INNER applies another formatter to each value.
join_objects join_objects|KEY|SEP|INNER List of objects Extracts KEY from each object, optionally applies INNER, and joins with SEP.
get_stub get_stub|STUB|FIELD Lookup key Reads /stubs/{STUB} from client state, finds a row whose key matches the cell value, and returns FIELD or value. Legacy: get_stub:STUB:FIELD.

Examples:

model:
  - {field: name, header: Name, type: str, transform: title}
  - {field: description, header: Description, type: str, transform: "truncate|80"}
  - {field: joined, header: Joined, type: str, transform: "date|%Y-%m-%d"}
  - {field: tags, header: Tags, type: str, transform: "join_list|, |upper"}
  - {field: groups, header: Groups, type: str, transform: "join_objects|name|, |title"}
  - {field: owner_id, header: Owner, type: str, transform: "get_stub|users|label"}

Row Data Format

rows is always a list of dictionaries. Each row should have a stable id, one key for each visible column field, and optional metadata such as selectable.

rows = [
    {
        "id": 101,
        "name": "Ada Lovelace",
        "role": "Admin",
        "status": "active",
        "team": "Platform",
        "spend": 1840,
        "selectable": True,
    },
    {
        "id": 104,
        "name": "Margaret Hamilton",
        "role": "Admin",
        "status": "locked",
        "team": "Platform",
        "spend": 2110,
        "selectable": False,
    },
]

The renderer reads row[column["field"]] for each visible cell. Extra row keys are not displayed unless the model declares a matching column, but they are still available to row actions and row visibility rules through $item.

For inline tables, total_rows normally equals len(rows). For remote paginated tables, rows is only the current page and total_rows is the total number of matching records.

Row And Selection Actions

row_actions render a per-row menu. The current row is resolved as the standard action item, so explicit action.context can bind row fields with $item.*.

{"item": row, "item_index": row_index, "tableId": table_id}

selection_actions render in a bulk action bar after the user selects at least one row. Each action receives:

{"selected_ids": [...], "selected_rows": [...], "tableId": table_id}

Rows should have a stable id. A row can disable selection with selectable: false or with a rule object.

Action visibility uses the same nested-action contract as other components: show_if, hide_if, and required_permissions. Row actions can inspect the current row through $item.<field>. Selection actions do not have a single row item, so use store/data bindings or permissions for their visibility.

Use non-mutating row actions on inline, store-bound, data-model-bound, and direct-update tables unless the action also updates that specific table source. Use mutating row actions such as promote/delete on the remote table that owns the backing data.

Action specs in row, selection, pagination, filter, edit, and remote loading flows must use fully qualified module action names, for example components.test_datatable_remote. See Action Dispatch.

row_actions = [
    {
        "label": "Inspect",
        "icon": "ric.search-eye-line",
        "action": {
            "name": "components.test_datatable_action_event",
            "context": {"op": "inspect", "source": "users_table"},
        },
    },
    {
        "label": "Pending only",
        "icon": "ric.time-line",
        "action": {
            "name": "components.test_datatable_action_event",
            "context": {"op": "pending_only", "source": "users_table"},
        },
        "show_if": {"conditions": [{"left": "$item.status", "op": "==", "right": "pending"}]},
    },
]

selection_actions = [
    {
        "label": "Export selected",
        "icon": "ric.download-2-line",
        "action": {
            "name": "components.test_datatable_action_event",
            "context": {"op": "export_selected", "source": "users_table"},
        },
    },
    {
        "label": "Archive selected",
        "icon": "ric.archive-line",
        "variant": "danger",
        "action": {
            "name": "components.test_datatable_remote_archive_selected",
            "context": {"op": "archive_selected", "source": "users_table"},
        },
        "show_if": {
            "conditions": [
                {
                    "left": bound.store("/components_test/datatable/show_bulk_archive", scope="page", default=True),
                    "op": "==",
                    "right": True,
                }
            ]
        },
        "required_permissions": ["components.datatable.bulk_archive"],
    },
]

table = sdk.ui.DataTable(
    "users_table",
    model=model,
    rows=rows,
    paginated=False,
    show_row_numbers=True,
    selectable=True,
    row_actions=row_actions,
    selection_actions=selection_actions,
    on_cell_edit="components.test_datatable_action_event",
)

Remote tables can add mutating row actions. These actions should update the backing data and then reload the same table using the tableId received in the payload.

row_actions:
  - label: Promote
    icon: ric.arrow-up-circle-line
    action:
      name: components.test_datatable_remote_promote
      context: {op: promote, source: users_remote}
    show_if:
      conditions:
        - left: "$item.status"
          op: "!="
          right: locked
  - label: Delete
    icon: ric.delete-bin-2-line
    variant: danger
    action:
      name: components.test_datatable_remote_delete
      context: {op: delete, source: users_remote}
    hide_if:
      conditions:
        - left: "$item.status"
          op: "=="
          right: locked
    required_permissions: [components.datatable.delete]

Action payload rules:

Trigger Payload added by the table
row_actions item, item_index, tableId
selection_actions selected_ids, selected_rows, tableId
on_cell_edit rowId, rowIndex, field, value, row, tableId
remote_service page, pageSize, filters, sort, sortField, sortDirection, tableId, and optionally autoRefresh

The action's explicit context is merged before the table payload. Use context for command metadata such as op and source; use $item.* in row action context when a route or command needs values from the current row.

Bindings And Direct Updates

rows, page, page_size, total_rows, sort, and filters are mutable table state. rows also supports collection patches.

store_table = sdk.ui.DataTable(
    "store_table",
    model=model,
    rows=bound.store("/components_test/datatable/store_rows", scope="page", default=rows),
    total_rows=bound.store("/components_test/datatable/store_total", scope="page", default=len(rows)),
    paginated=False,
)

data_table = sdk.ui.DataTable(
    "data_table",
    model=model,
    rows=bound.data("/components_test/datatable_model/rows", default=[]),
    total_rows=bound.data("/components_test/datatable_model/total", default=0),
    paginated=False,
)

live_table = sdk.ui.DataTable("live_table", model=model, rows=rows, total_rows=len(rows), paginated=False)
live_table.allow("rows.set", "rows.append", "rows.remove", "rows.replace", "page.set", "page_size.set", "total_rows.set", "sort.set", "filters.set")

surface_id = ctx["_surface_id"]
sdk.effects.ui_property_update("live_table", "rows", new_rows, action="set", surface_id=surface_id)
sdk.effects.ui_collection_append("live_table", "rows", new_row, surface_id=surface_id)
sdk.effects.ui_collection_replace("live_table", "rows", {"id": row_id, "item": updated_row}, surface_id=surface_id)
sdk.effects.ui_collection_remove("live_table", "rows", {"id": row_id}, surface_id=surface_id)

Remote Service

When remote_service is present, the table calls it on initial render and when remote filter, sort, pagination, or auto-refresh state changes.

The action receives:

{
    "page": 0,
    "pageSize": 25,
    "filters": {"status": "active"},
    "sort": {"field": "name", "direction": "asc"},
    "sortField": "name",
    "sortDirection": "asc",
    "tableId": "users_table",
    "autoRefresh": True,  # only on auto-refresh
}

The action should return targeted property updates for rows, page, page_size, total_rows, and optionally sort.

remote_row_actions = [
    *row_actions,
    {
        "label": "Promote",
        "icon": "ric.arrow-up-circle-line",
        "action": {
            "name": "components.test_datatable_remote_promote",
            "context": {"op": "promote", "source": "users_remote"},
        },
        "show_if": {"conditions": [{"left": "$item.status", "op": "!=", "right": "locked"}]},
    },
    {
        "label": "Delete",
        "icon": "ric.delete-bin-2-line",
        "variant": "danger",
        "action": {
            "name": "components.test_datatable_remote_delete",
            "context": {"op": "delete", "source": "users_remote"},
        },
        "hide_if": {"conditions": [{"left": "$item.status", "op": "==", "right": "locked"}]},
        "required_permissions": ["components.datatable.delete"],
    },
]

remote_table = sdk.ui.DataTable(
    "users_remote",
    model=model,
    rows=rows,
    page=0,
    page_size=25,
    total_rows=len(rows),
    remote_service="components.test_datatable_remote_list",
    paginated=True,
    selectable=True,
    row_actions=remote_row_actions,
    selection_actions=selection_actions,
    sort_field="name",
    sort_direction="asc",
)

Initial rows are a fallback for the first paint. When remote_service runs, its response should replace rows, page, page_size, total_rows, and optionally sort with authoritative values.

Model CRUD Action

The remote service pattern used by the monitor module is the same pattern to use with model-driven CRUD: read page, pageSize, filters, sort, and tableId from the action context, call module_sdk.models.<model>, then update only the table properties.

This example is intentionally complete. Replace objective_mappings with the writable model owned by the module or platform area you are exposing.

from __future__ import annotations

from typing import Any

from democrai.sdk.decorators import action


def _table_id(ctx: dict[str, Any], default: str = "objective_mappings_table") -> str:
    return str(ctx.get("tableId") or ctx.get("table_id") or default).strip()


def _page(ctx: dict[str, Any]) -> int:
    return max(0, int(ctx.get("page", 0) or 0))


def _page_size(ctx: dict[str, Any]) -> int:
    return max(1, min(int(ctx.get("pageSize", 25) or 25), 200))


def _filters(ctx: dict[str, Any]) -> dict[str, Any]:
    raw = ctx.get("filters") or {}
    if not isinstance(raw, dict):
        return {}
    return {
        str(key): value
        for key, value in raw.items()
        if value is not None and value != ""
    }


def _sort(ctx: dict[str, Any], default_field: str = "name") -> dict[str, str]:
    raw = ctx.get("sort") or {}
    if not isinstance(raw, dict):
        raw = {}
    field = str(raw.get("field") or raw.get("sortField") or default_field).strip()
    direction = str(raw.get("direction") or raw.get("sortDirection") or "asc").lower()
    return {"field": field or default_field, "direction": "desc" if direction == "desc" else "asc"}


def _surface_id(ctx: dict[str, Any]) -> str:
    return str(ctx.get("_surface_id") or "main").strip() or "main"


def _refresh_table(module_sdk, ctx: dict[str, Any]):
    table_id = _table_id(ctx)
    page = _page(ctx)
    page_size = _page_size(ctx)
    filters = _filters(ctx)
    sort = _sort(ctx)

    listing = module_sdk.models.objective_mappings.list(
        page=page,
        page_size=page_size,
        filters=filters,
        sort=sort,
    )
    rows = list(listing.get("rows") or [])
    total_rows = int(listing.get("total_rows") or 0)
    resolved_page = int(listing.get("page") or page)
    resolved_page_size = int(listing.get("page_size") or page_size)
    resolved_sort = dict(listing.get("sort") or sort)
    surface_id = _surface_id(ctx)

    return module_sdk.effects.respond(
        module_sdk.effects.ui_property_update(table_id, "rows", rows, action="set", surface_id=surface_id),
        module_sdk.effects.ui_property_update(table_id, "page", resolved_page, surface_id=surface_id),
        module_sdk.effects.ui_property_update(table_id, "page_size", resolved_page_size, surface_id=surface_id),
        module_sdk.effects.ui_property_update(table_id, "total_rows", total_rows, surface_id=surface_id),
        module_sdk.effects.ui_property_update(table_id, "sort", resolved_sort, surface_id=surface_id),
    )


@action("list_objective_mappings")
async def list_objective_mappings(ctx: dict, session: dict, module_sdk):
    del session
    return _refresh_table(module_sdk, ctx)


@action("create_objective_mapping")
async def create_objective_mapping(ctx: dict, session: dict, module_sdk):
    del session
    payload = {
        "objective": str(ctx.get("objective") or "support_triage"),
        "capability": str(ctx.get("capability") or "chat"),
        "priority": int(ctx.get("priority", 100) or 100),
    }
    module_sdk.models.objective_mappings.create(payload)
    return _refresh_table(module_sdk, {**ctx, "page": 0})


@action("update_objective_mapping")
async def update_objective_mapping(ctx: dict, session: dict, module_sdk):
    del session
    item = dict(ctx.get("item") or {})
    row_id = int(item.get("id") or 0)
    payload = {
        "objective": str(item.get("objective") or ctx.get("objective") or ""),
        "capability": str(item.get("capability") or ctx.get("capability") or ""),
        "priority": int(ctx.get("priority", item.get("priority", 100)) or 100),
    }
    module_sdk.models.objective_mappings.update(row_id, payload)
    return _refresh_table(module_sdk, ctx)


@action("delete_objective_mapping")
async def delete_objective_mapping(ctx: dict, session: dict, module_sdk):
    del session
    item = dict(ctx.get("item") or {})
    row_id = int(item.get("id") or 0)
    module_sdk.models.objective_mappings.delete(row_id)
    return _refresh_table(module_sdk, ctx)


@action("delete_selected_objective_mappings")
async def delete_selected_objective_mappings(ctx: dict, session: dict, module_sdk):
    del session
    for row_id in ctx.get("selected_ids", []):
        module_sdk.models.objective_mappings.delete(int(row_id))
    return _refresh_table(module_sdk, ctx)

Visibility And Permissions

show_if, hide_if, and required_permissions work on the table itself. row_actions and selection_actions can also define their own visibility and permissions.

table.set_show_if({
    "conditions": [
        {
            "left": bound.store("/components_test/datatable/show", scope="page", default=True),
            "op": "==",
            "right": True,
        }
    ]
})
table.set_hide_if({
    "conditions": [
        {
            "left": bound.store("/components_test/datatable/hide", scope="page", default=False),
            "op": "==",
            "right": True,
        }
    ]
})
table.set_required_permissions(["components.datatable.view"])

Mutable Capabilities

Capability Effect
rows.set Replaces the rendered row list.
rows.append Appends one row.
rows.remove Removes a row by collection patch value, usually {"id": row_id}.
rows.replace Replaces one row by collection patch value.
page.set Updates the current page.
page_size.set Updates the page size.
total_rows.set Updates the total result count.
sort.set Updates the sort state.
filters.set Updates the filter state.
visible.set Updates common runtime visibility.

DataTable exposes row collection and pagination capabilities by default. Declare capabilities explicitly in YAML when using direct runtime updates in examples or tests.

Screenshots

Desktop rendering:

DataTable desktop

Web rendering:

DataTable web