Overview
The Repeatable block renders a single child template for every element of a bound data array. Design the row once, and the block stamps it out for every item with that item's data in scope. It supports vertical and horizontal list layouts, CSS grid layouts, optional drag-to-reorder, page-based and scroll-based pagination, and a per-row state collection feature that lets you read every row's inner block values from outside the list.
Tip: Use Repeatable when your rows need a free-form layout you design — contact cards, product tiles, feeds, checklists. Prefer the Table block when users need columns, sorting, filtering, or bulk actions. Prefer Kanban when items must be dragged between groups.
Repeatable List and Repeatable Grid are sibling blocks that use the same rendering engine but ship pre-configured as a list or a two-column grid, respectively. They intentionally omit callable methods, pagination wiring, and the per-row state collection that the base Repeatable offers.
Data Binding
Two properties power everything: the array the block iterates, and the unique key for each row.
| Property | Type | Default | Description |
| data | array (bindable) | Sample items | The array to repeat — one template instance per element. Usually a binding to a data source query result. |
| primaryKey | field / Index / expression | id | Unique identity of each row. Required for selection targeting, drag-to-reorder, and scroll-to. Use Index only when order is stable. |
Warning: If your data source returns {{ query1.data.items }}, bind to that path — not {{ query1.data }}. A non-array value renders no rows without any error.
Item Template
The Repeatable renders one template block — a Card by default — once per row. Select it on the canvas or layers panel and edit it like any block. Everything added to the template repeats for every row.
Row-Scoped Bindings
Inside the template, a special set of bindings expose the current row's data. These must use bracket notation — dot notation is not supported:
{{ repeatable1['context']['item']['name'] }} ✅ correct {{ repeatable1['context']['index'] }} ✅ zero-based row position {{ repeatable1['context']['primaryKey'] }} ✅ the row's resolved key {{ repeatable1.context.item.name }} ❌ dot notation not supported {{ repeatable1['currentItem']['name'] }} ❌ no such shorthand
Note: While editing bindings in the builder, the expression picker always previews with the first row's data. At runtime each row correctly receives its own data.
Per-Row Isolation
Every block placed in the template gets its own copy per row automatically:
A sibling reference like
{{ price_input.value }}resolves to this row's copy of the input.A method call on a sibling (e.g., Form submit) targets this row's copy only.
An event handler on an inner block fires for that row only — perfect for per-row automations.
Content Properties
| Property | Type | Default | Description |
| sortable | boolean | off | Enables drag-to-reorder. Appears only after a Primary Key is set. Reordering updates {{ id.order }} but does not mutate the bound data. |
| dragHandleBlockId | block picker | — (whole row) | Restricts dragging to one block inside the template (e.g., a grip icon). Appears when Sortable is on. |
| autoSelect | none | firstItem | lastItem | none | Automatically selects the first or last row when nothing is selected. Fires On Select Item like a real click. |
| collectChildStates | boolean | off | Exposes each row's inner block states under {{ id.instances }}. Mutually exclusive with pagination. |
| addOns (Pagination) | add-on | — | Adds page-based or scroll-based loading. Requires wiring your data source's limit/offset to {{ id.pagination.pageSize }} and {{ id.pagination.offset }}. |
| containerId | block picker | — | The container block whose scrolling drives pagination. Appears when pagination is configured. |
Appearance Properties
| Property | Type | Default | Description |
| layout | list | grid | list | Stack rows as a list or arrange them in a CSS grid. |
| direction | vertical | horizontal | vertical | List flow direction. Only appears when Layout is list. |
| columns | number | 3 when unset | Grid column count. Only appears when Layout is grid. Columns are fixed width — not responsive. |
| separator | toggle + styles | off | A divider between rows with color and size options. Only when Layout is list. |
| styles | style set | gap lg | Gap between rows, height, padding, overflow, border. |
| visibility | condition | always visible | Show or hide the whole block based on a condition. |
Pagination
Pagination is an add-on that loads rows in chunks. It comes in two modes:
🔒 Page-Based vs Scroll-Based Pagination
Page-Based: A numbered pager bar renders below the rows. Bind your data source's limit/offset inputs to {{ id.pagination.pageSize }} and {{ id.pagination.offset }}. Bind Total Records to show the page bar (required).
Scroll-Based (Infinite): More rows load as the user nears the list bottom. Same wiring required. Also unlocks the On Pull Down event for pull-to-refresh.
Warning: Binding a static 200-element array with page size 30 renders all 200 rows. Pagination only works when the data source itself consumes the block's page size and offset and returns one page at a time.
Pagination State Keys
| State Key | Type | Description |
| {{ id.pagination.pageSize }} | number | Rows per page — bind your data source's limit input to this |
| {{ id.pagination.offset }} | number | Rows to skip — bind your data source's offset input to this |
| {{ id.pagination.currentPage }} | number | Zero-based current page |
| {{ id.pagination.total }} | number | Total record count (from your data source) |
Events
| Event | Trigger | Payload |
| On Select Item | A row is clicked, or auto-select picks one | selectedItem, selectedItemKey, selectedItemIndex |
| On Order Change | User drops a dragged row in a new position | New order in {{ id.order }} |
| On Pull Down | User pulls the list down from the top | — (scroll-based pagination or mobile) |
Methods
Callable via the Control Block Method action or from a script at the block's own name.
| Method | Params | Description |
| selectNextItem | — | Moves selection to the next row and scrolls it into view. Requires a Primary Key. |
| selectPreviousItem | — | Moves selection to the previous row. Requires a Primary Key. |
| scrollTo | key | Scrolls to the row with the given primary key or index. |
| resetOrder | — | Discards user drag-reordering and restores data order. |
| goToPage | pageNumber | Jumps the pager. Note: loads all pages from 1 through the target. |
| resetPageOffset | — | Collapses scroll-loaded pages back to the first and refreshes. |
| resetInstances | — | Clears all collected per-row inner block states back to template defaults. |
| clearSelection | — | Clears the selected item. With Auto Select active, a new selection is immediately made. |
Exposed State
| State Key | Type | Description |
| {{ id.selectedItem }} | object | The currently selected row's data |
| {{ id.selectedItemKey }} | string | The selected row's primary key |
| {{ id.selectedItemIndex }} | number | The selected row's position |
| {{ id.order }} | array | Row keys in their current (possibly user-dragged) order |
| {{ id.currentData }} | array | Snapshot of the bound data — populated when data comes from a data source |
| {{ id.instances }} | array | Per-row inner block states — only when Collect Child States is on |
| {{ id.context.item }} | object | Current row data — inside template only, use bracket notation |
| {{ id.context.index }} | number | Zero-based row position — inside template only |
| {{ id.context.primaryKey }} | string | Row's primary key — inside template only |
The Shape of instances
When Collect Child States is on, each element of {{ id.instances }} holds currentIndex, currentPrimaryKey, and one entry per direct child block. Three special lookups are also available:
instances.byKey(key)— returns a row's full aggregate by its primary keyinstances.indexOfKey(key)— returns a row's position by primary keyinstances.keys— returns every row's primary key as a plain array
{{ repeatable1.instances.keys.map(key => repeatable1.instances.byKey(key).checkbox1.value) }} → [true, false, true] // one entry per row's checkbox1
Repeatable List & Repeatable Grid
These are pre-configured siblings that use the same engine as Repeatable but with different defaults and reduced capabilities:
| Feature | Repeatable | Repeatable List | Repeatable Grid |
| Default layout | list (configurable) | list (fixed) | grid (fixed, 2 columns) |
| Callable methods | Yes (full set) | None | None |
| Working pagination | Yes | No (UI shows but inert) | No (UI shows but inert) |
| Collect Child States / instances | Yes | No (UI shows but inert) | No (UI shows but inert) |
| context.index / context.primaryKey | Yes | No (context.item only) | No (context.item only) |
| Starter card | Simple text card | Rich gradient card (web) / user row (mobile) | Gradient card grid (web) / product card grid (mobile) |
Note: Repeatable List and Grid are excellent for quick mockups and simple displays. For any flow that needs pagination, per-row state reading, or imperative control (next/previous selection, scroll-to), use the base Repeatable block.
Nesting Repeatables
A Repeatable's template can contain another Repeatable — ideal for 2D data like orders with line items:
// Inner Data Source binding: {{ outerRepeatable['context']['item']['lineItems'] }} // Reading inner row state from outside both: {{ outerRepeatable.instances[0].innerRepeatable.instances[1].skuInput.value }}
Behavior & Gotchas
⚠ Sortable toggle is hidden until a Primary Key is set
The Sortable toggle does not appear in the panel until a Primary Key is chosen. Set a Primary Key first, then the toggle becomes available.
⚠ Enabling pagination silently disables Collect Child States
Saving with a pagination add-on forces Collect Child States off. Paginated lists recycle off-screen rows, so per-row state collection would be partial. Choose one or the other per Repeatable.
⚠ Selection clears when the row disappears from data
When the selected row's key no longer exists (a refetch removed it), the selection resets automatically. With Auto Select active, a new item is immediately selected and On Select Item fires.
⚠ New rows land at the bottom of a user-sorted list
After a drag-reorder, incoming rows are appended after the remembered order, not at their natural data position. Call resetOrder to restore data order.
⚠ Per-row state persists across reuse
Collected instances state persists until cleared. A Drawer or modal that reuses the same Repeatable shows previous open's row state (e.g., ticked checkboxes). Call resetInstances on the Drawer's open event.
⚠ Mobile limitations
In mobile apps: drag-to-reorder and the numbered page bar are not available. Only the scrollTo method is wired. Lists use infinite scroll and pull-to-refresh instead.
Common Patterns
Card List with Selection Drawer
Bind data and set primary key: Set Data Source to your query result and Primary Key to the record's
idfield.Design the template card: Open the template card and bind its blocks using row-scoped pills:
{{ repeatable1['context']['item']['name'] }}.Wire On Select Item: On On Select Item, open a Drawer bound to
{{ repeatable1.selectedItem }}.
Paginated Product Grid
Set layout to grid: Set Layout to
gridand pick a column count (e.g., 3).Add pagination add-on: Enable the Pagination add-on, set type to Page based, and bind the data source's limit/offset inputs to
{{ id.pagination.pageSize }}and{{ id.pagination.offset }}.Bind Total Records: Bind Total Records to the data source's count output. The page bar appears once this is set.
Sortable Checklist
Enable sorting: Set a Primary Key, then toggle Sortable on. Optionally pick a Drag Handle Block inside the template.
Persist order: On On Order Change, persist
{{ repeatable1.order }}to your backend.Add a reset button: Offer a "Reset Order" button that calls the
resetOrdermethod.
Related Blocks
| Block | Relationship |
| Repeatable Grid | Same engine, pre-configured for grid layout |
| Repeatable List | Same engine, pre-configured for list layout |
| Table | Prefer for dense records with columns, sorting, filtering, bulk actions |
| Kanban | Prefer when items are dragged between groups/lanes |
| Tree View | For hierarchical, expandable/collapsible parent-child data |
Frequently Asked Questions
Can I nest one Repeatable inside another?
Yes. Drop a Repeatable block inside the item template of an outer Repeatable. The inner block gets its own context key. Use bracket notation to access the inner row: {{ innerRepeatable['context']['item']['field'] }}. Keep nesting shallow — more than two levels deep can make bindings difficult to reason about.
Why does {{ repeatable1.context.item.name }} not work?
Row-scoped context bindings in Repeatable only support bracket notation: {{ repeatable1['context']['item']['name'] }}. Dot notation is not supported inside the item template. This is by design to allow dynamic key access. Switching to bracket notation resolves the issue.
What is the difference between Repeatable, Repeatable List, and Repeatable Grid?
Repeatable is the full-featured block with a freeform item template, pagination, sorting, filtering, per-row state, and all exposed methods. Repeatable List and Repeatable Grid are simplified variants with a fixed layout. They support fewer configuration options and are best used when you only need a quick list or grid with minimal customization.
How do I access the item the user clicked on?
Listen to the On Item Click event. In the event action, the clicked record is available as the event payload. Store it in a page variable using a Set Variable action, then bind other blocks (e.g., a detail drawer) to that variable.
My pagination controls appear but clicking them does nothing. Why?
Pagination in the Repeatable block is a contract with the data source — the block does not slice the array itself. When the user changes page, the block updates its exposed currentPage state and fires the pagination event. You must use those values in your data source query (as offset/limit parameters) and re-fetch. If your data source always returns all records regardless of page, pagination controls will appear but nothing will visually change.