Your workflows already have the data. Now make it look the part.
Dynamic HTML lets you turn live workflow data into polished, interactive interfaces — styled tables, product cards, custom forms — without leaving Zingtree. Instead of static content, your agents and end users see exactly what's relevant to them, generated in real time from the data already flowing through your workflow.
Why Teams Love This Pattern
- No external tools required. Everything happens inside a Script Node and a Content Node — no embeds, no iframes, no separate front-end to maintain.
- Build it once, reuse it everywhere. A well-built HTML component becomes a reusable building block across your workflows.
- Capture richer input. Dynamic forms let users interact with each item in a dataset individually — quantities, selections, confirmations — and feed those values right back into your workflow logic.
If you can describe it in HTML and CSS, you can render it in a workflow.
Using Zingtree Script Nodes to Generate Dynamic, Custom HTML
This guide explains how to use a Zingtree Script Node to generate Dynamic HTML and display it to your end users.
The core idea is to use workflow data to generate dynamic HTML and render it in a Content Node.
Use Case Scenarios
- Display arrays or lists of data in customized and styled HTML elements
- Capture unique values for any fields for each object in the array
- Create dynamic tables and cards using custom HTML and CSS designs
- Create dynamic HTML form inputs
- Build reusable custom UI components directly inside workflows
The Basic Pattern
Image placeholder: Example rendered Dynamic HTML card or chart
- In a Script Node, build an HTML string, including custom HTML, styling, and form inputs.
- Save the HTML using
ZT.setFormData("custom_html", html_variable_name). - Render it in a Content Node using
${custom_html}.
Example
let object = {
id: 1,
name: "Example Item",
description: "This is an example description.",
qty: 0
};
let html_string = `
<style>...custom css here...</style>
<div class="item-card">
<h3>${object.name}</h3>
<p><span class="item-label">ID:</span> ${object.id}</p>
<p><span class="item-label">Description:</span> ${object.description}</p>
<p><span class="item-label">Quantity:</span> ${object.qty}</p>
</div>
`;
ZT.setTransformData("custom_html", html_string);Display the HTML in a Content Node
In the Content Node, render the generated HTML using:
${transforms.custom_html}The Array List Pattern
- Retrieve array data from
transformsoractions. - Iterate over the array.
- In a Script Node, build an HTML string with custom HTML, styling, and form inputs.
- Save using
ZT.setFormData("custom_html", html_variable_name). - Render in a Content Node using
${custom_html}.
Example
let items = [
{
id: 1,
name: "Example Item 1",
description: "First example description.",
qty: 0
}
];
let styled_items = items.map(item => {
return `
<tr>
<td>${item.id}</td>
<td>${item.name}</td>
<td>${item.description}</td>
<td>${item.qty}</td>
</tr>
`;
});
let html_string = `
<style>...custom css here...</style>
<table class="items-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Description</th>
<th>Qty</th>
</tr>
</thead>
<tbody>
${styled_items}
</tbody>
</table>
`;
ZT.setTransformData("custom_html", html_string);The Form Data Pattern
- Retrieve array data from
transformsoractions. - Iterate over the array.
- In a Script Node, build HTML form inputs dynamically.
- Ensure each input includes
class="zt-data". - Ensure each input includes
name="variable_name".
- Ensure each input includes
- Save using
ZT.setFormData("custom_html", html_variable_name). - Render in a Content Node using
${custom_html}.
Example Setup
Script - Create HTML
let items = [
{
id: 1,
name: "Example Item 1",
description: "First example description.",
qty: 0
}
];
let dynamic_form = items.map(item => {
return `
<tr>
<td>${item.name}</td>
<td>${item.description}</td>
<td>
<input
type="number"
min="0"
class="zt-data"
name="qty-${item.id}"
value="0"
/>
</td>
</tr>
`;
});
let html_string = `
<style>...custom css here...</style>
<div class="zt-table-wrapper">
<table class="zt-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
${dynamic_form}
</tbody>
</table>
</div>
`;
ZT.setTransformData('custom_html', html_string);Content - Display HTML
${transforms.custom_html}Script - Stitch Form Data
let items = [
{
id: 1,
name: "Example Item 1",
description: "First example description.",
qty: 0
}
];
/*
Iterate the dataset
Retrieve the value from each dynamic input
Store the selected qty back onto the object
*/
items.forEach(item => {
// Input name created earlier:
// name="qty-${item.id}"
const qty = ZT.getVariableValue(
`qty-${item.id}`,
0
);
// Store selected value back onto object
item.qty = qty;
});
/*
Final dataset now contains
the selected qty values
*/
ZT.setTransformData(
"updated_items",
items
);
console.log(items);Accessing Simple Values from Custom HTML Inputs
Custom HTML inputs can be accessed anywhere later in the workflow using:
ZT.getVariableValue(<HTML_ELEMENT_NAME>, <FALLBACK_VALUE>)
This allows dynamically generated HTML inputs to behave like standard Zingtree form fields.
Example
Accessing the quantity selected from the standard table example:
const qty = ZT.getVariableValue('qty-product_id_1', 0);You can then save that selected value using:
ZT.setFormData('selected_qty', qty);This retrieves the selected quantity from:
<input name="qty-product_id_1" />
Accessing Complex Values Arrays or Lists
This works because the generated HTML inputs used:
<input name=`qty-${item.id}` />Then a Script Node can dynamically reference this unique name attribute on the HTML form input.
let items = [
{
id: 1,
name: "Example Item 1",
description: "First example description.",
qty: 0
}
];
items.forEach(item => {
let qty = ZT.getVariableValue(`qty-${item.id}`, 0)
items[item.id].qty = qty;
})
ZT.setTransformData("items", items)The workflow can then reconstruct the final dataset using the selected values from each dynamically generated HTML element.
Mental Model
Think of the Script Node as the renderer.
The Script Node answers:
What HTML should this user see based on the data we have right now?
The Content Node answers:
Where should that generated HTML appear?
Keep logic in the Script Node and presentation in the Content Node.