A card grid is free-form — each row can contain whatever layout you like. A table is not: it has columns, a header row, and every row needs to line up under the same headings. That structural difference is exactly why the engine gives it a separate element, dynamiclisttable, rather than making you fake a table out of a styled dynamiclist.
Same Contacts table, same tenant scoping, this time with a department column added to make the tabular shape worth showing:
<block> <table type="dynamiclisttable" class="contacts-table"> <tr type="headrow"> <th>Name</th> <th>Department</th> <th>Email</th> <th class="num">Open Orders</th> </tr> <tr type="row"> <td class="name">%contact_name%</td> <td><span class="dept-pill">%department%</span></td> <td>%email%</td> <td class="num">%open_order_count%</td> </tr> <!-- one query, after the row template — same rule as dynamiclist --> <sql> <query name="contacts_table"><![CDATA[ SELECT c.contact_id, c.contact_name, c.department, c.email, COUNT(o.order_id) AS open_order_count FROM contacts c LEFT JOIN orders o ON o.contact_id = c.contact_id AND o.status = 'open' WHERE c.dept_id = '%deptid%' GROUP BY c.contact_id ORDER BY c.contact_name ASC ]]></query> </sql> </table> </block>
What's different from a dynamiclist
<block>, with the table nested one level in as its child, not as the root itself.<table> tag: <table type="dynamiclisttable">. Writing a bare <dynamiclisttable> instead is accepted without complaint but drops the <table> element from the output entirely, which quietly breaks every table style on the page. Always write the real tag.<thead> row. A dynamiclist has no equivalent — a card grid has no column headings to keep aligned.<tbody> template — the same repeat-per-result-row idea as the list's own type="row", just constrained to <td> cells that line up under the header.<table> and the two <tr> templates; the engine supplies the <thead> and <tbody> around them. Don't open or close those by hand — the whole table has to come from this one element.<query> per element. Need data from more than one table? JOIN it into that single query, the way open_order_count is joined in above, rather than adding a second query.<table class="contacts-table"> <thead> <tr> <th>Name</th><th>Department</th><th>Email</th><th class="num">Open Orders</th> </tr> </thead> <tbody> <tr> <td class="name">Sarah Whitfield</td> <td><span class="dept-pill">Operations</span></td> <td>s.whitfield@example.com</td> <td class="num">3</td> </tr> <!-- repeated once per row --> </tbody> </table>
| Name | Department | Open Orders | |
|---|---|---|---|
| Sarah Whitfield | Operations | s.whitfield@example.com | 3 |
| David Okonkwo | Procurement | d.okonkwo@example.com | 7 |
| Maria Santos | Finance | m.santos@example.com | 0 |
| James Cooper | Warehouse | j.cooper@example.com | 2 |
open_order_count column comes from a JOIN and a COUNT in the same single query — not a second round trip, not a per-row lookup. One query, every column.When to reach for which
Neither component is "better" — dynamiclisttable is purpose-built for column-based data: it generates the header row and keeps every column aligned underneath it automatically. dynamiclist is the more versatile of the two, capable of far more than card-style rows — including composing with dynamiclisttable in ways worth their own dedicated look. Post 3's grid and this post's table are rendering the same underlying contacts — dynamiclisttable just meant not having to hand-build the header row and column alignment ourselves.
A Detail Screen: single-record view with block-level SQL
Post 5 zooms into one contact. A different data-access rule entirely — a single row's data doesn't stay scoped to a row template the way list and table data do.