A list is the right starting point because it's the smallest complete loop the engine runs: SQL goes in, a row template gets repeated once per result, HTML comes out. Nothing else on the platform is simpler than this, and almost everything else is built from the same idea.
The component for this is dynamiclist — a repeating HTML structure driven by data. You write real HTML tags and mark two of them: the container with type="dynamiclist", the per-row template with type="row". Add a SQL query, and the engine runs it and renders your template once per row returned, with that row's values substituted in.
Here's a complete, working dynamiclist block for a Contacts list — nothing trimmed:
<block> <div type="dynamiclist" class="contact-grid"> <div type="row" class="contact-card"> <div class="contact-avatar">%initials%</div> <div> <div class="contact-name">%contact_name%</div> <div class="contact-role">%job_title%</div> <div class="contact-meta">%email%</div> </div> </div> <!-- Inside a dynamiclist, the query follows the row template --> <sql> <query name="contacts_list"><![CDATA[ SELECT contact_id, contact_name, job_title, email, UPPER(LEFT(contact_name,1)) AS initials FROM contacts WHERE dept_id = '%deptid%' ORDER BY contact_name ASC ]]></query> </sql> </div> </block>
That's the whole thing. No loop to write, no result-set iteration, no string concatenation to build up a row of markup — just a template and a query.
What each piece is doing
<block>, not the dynamiclist itself. It's transparent — it never emits a tag of its own, so the list content sits one level in and the first real tag in the output is the one you wrote.<div> carrying it is the outer container the whole list renders inside — here a grid, styled by the contact-grid class.type="row". It's processed as a full element tree, so anything you can build elsewhere on the platform (nested containers, conditional blocks, other blocks) works inside it.type="row" subtree and nowhere else — a sibling element outside the list can't see them, and neither can the next row. That scoping is enforced by the engine, not by convention.dynamiclist, the query follows the row template, as above. A block-level <sql> — one feeding the whole block rather than a list — is the opposite: it must be the first child of <block>, or the tokens it produces aren't resolved yet when the elements above it render.The engine runs contacts_list, gets back however many rows match, and renders the type="row" template once per row with that row's tokens substituted. For four contacts, the output looks like this:
<div class="contact-grid"> <div class="contact-card"> <div class="contact-avatar">S</div> <div> <div class="contact-name">Sarah Whitfield</div> <div class="contact-role">Operations Manager</div> <div class="contact-meta">s.whitfield@example.com</div> </div> </div> <!-- … repeated once per row, no two the same — the template is fixed, the data isn't … --> </div>
Every card is the identical markup structure — only the token values change. That's the entire value of a dynamiclist: write the shape once, let row count vary freely.
And here's that generated HTML, live, in a real browser — not a mockup:
Why not just write a PHP loop?
Why should you? Because doing this manually means backend code, escaping values, wiring templates, and keeping everything in sync — work you don't need to do. dynamiclist lets you define the structure and the SQL together, and the engine handles the repetitive part.
In the next post, we'll look at dynamiclisttable — the component built specifically for tabular, column-based data — and how it extends the same idea for spreadsheet-like layouts.
Building a Table: sortable, structured data with dynamiclisttable
Post 4 takes the same Contacts data and puts it in a proper structured table — columns, headers, and the platform's built-in sorting.