Skip to content
Talk to an engineer

Agentic UI

UI components in chat

Most actions return data the agent relays as text. An action that renders a UI component in the chat goes one step further: after it runs, it draws an interactive component — a form, a card, a confirmation dialog — directly into the chat. The user acts on it inline, and their input flows back to the agent.

Chatterblock use-case gallery: forms, tables, confirmations, and cards Form Submit Data table Confirmation ? Cancel Confirm Rich card

An action and its UI are two halves of one feature. execute(params) owns the logic — fetch or write data. render(data) owns the presentation — build the component the user sees and interacts with. Together they make up an execute-and-render action.

  • Show data the user should take in at a glance — a table, a summary card, a dashboard.
  • Collect input without leaving the chat — a form, a picker, an editable record.
  • Confirm a consequential change before it happens — an approve / reject dialog.

When you only need data back as text, with no UI, use a plain execute action instead.

When execute(params) resolves, the agent hands its result to your render function. You build DOM nodes into the container you’re given; when the user submits, you call callback(value) to send their input back to the agent, or cancel() to abort.

sequenceDiagram
  participant U as User
  participant A as Agent
  participant E as execute(params)
  participant R as render(data, host, ...)
  U->>A: Natural-language request
  A->>E: Call execute handler
  E-->>A: Return data object
  A->>R: Pass data to render
  R->>R: Build the UI component
  R->>U: Interactive component in chat
  U->>R: Interact (edit, confirm)
  R->>A: callback(value)
  A->>U: Continue with the submitted data
Render function parameters mapped to the Chatterblock UI Edit Task #42 Title: Fix login redirect bug Body: Cancel Save header host data callback cancel

Your render function receives these arguments, in order.

ParameterTypeDescription
dataanyThe value returned by your execute handler (e.g., an object or array of results).
hostHTMLElementThe container element where you append your UI nodes (forms, cards, lists).
headerHTMLElementA header element you can style or populate with a title, instructions, or status.
callback(value: string, disableOnSubmit?: boolean) => voidCall this when the user submits; the value is passed to the agent. disableOnSubmit defaults to true (the SDK disables your UI after submit) — set it to false to keep the component interactive.
cancel() => voidCall this if the user aborts, so the agent resumes without new data. A default “Cancel” control is also rendered.
readyState(value: any) => voidCall this once the component shows its data, to send that data to the agent without waiting for the user. The component stays active.
The componentThe agent receives
Calls callback(value)FUNCTION_TOOL_SUBMITTED_BY_USER, with value
Calls cancel()FUNCTION_TOOL_CANCELLED_BY_USER
Calls readyState(value)FUNCTION_TOOL_DISPLAYED, with value. The user has not acted.
Declares none of themFUNCTION_TOOL_DISPLAYED, with what execute returned. The SDK sends it.

The SDK reads your render function’s parameters to tell these apart. A render that declares callback, cancel or readyState but never uses them is reported as shown, and the SDK logs a warning. When the SDK can’t tell (rest parameters, arguments, a bound function, or a destructured fourth parameter), the agent waits.

This show_user action fetches a user in execute and draws a profile card in render. The render takes only data, host, and header, so the SDK tells the agent the card was shown and passes it what execute returned. The agent answers follow-ups such as “what’s their email?” from that data.

show-user.js
foldspace("when", "ready", () => {
foldspace.agent({ /* …common setup… */ })
.addActionHandlers({
show_user: {
// 1. Fetch the user by ID; the agent receives this object
execute: async (params) => {
const { userId } = params;
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
return response.json();
},
// 2. Draw a read-only card: no callback or cancel, so it is reported as shown
render: (user, host, header) => {
const style = document.createElement('style');
style.textContent = `
.user-card {
border: 1px solid #e3e3e3;
border-radius: 8px;
margin-top: 8px;
padding: 16px;
font-family: sans-serif;
}
.user-card-name {
font-weight: bold;
margin-bottom: 12px;
}
.user-card dl {
display: grid;
grid-template-columns: max-content 1fr;
gap: 4px 12px;
margin: 0;
font-size: 14px;
}
.user-card dt {
color: #666;
}
.user-card dd {
margin: 0;
}
`;
header.appendChild(style);
const card = document.createElement('div');
card.className = 'user-card';
const name = document.createElement('div');
name.className = 'user-card-name';
name.textContent = user.name;
const details = document.createElement('dl');
for (const [label, value] of [
['Email', user.email],
['Phone', user.phone],
['Company', user.company.name],
['City', user.address.city],
]) {
const term = document.createElement('dt');
term.textContent = label;
const desc = document.createElement('dd');
desc.textContent = value;
details.append(term, desc);
}
card.append(name, details);
host.appendChild(card);
}
}
});
});
A Chatterblock rendered inside a conversation showing an account overview card with details and support cases help me prepare for a meeting with Union Bank CRM AI ✦ Account Overview — Union Bank 1. Account Summary (Last 6 Months) Account: Union Bank Phone: +1 - 529 - 903 - 6123 Email: support@unionbank.com Assigned User: Will Westin Industry: Finance Website: unionbank.com Address: 266 West St, Boston, MA 2. Open Support Cases (Past 60 days) Case ID Title Status Date Opened 1027 Can't use other apps while playing music Assigned 1/24/2025 1025 Can't access Videos or Music on Android TV Assigned 1/24/2025 1019 Billing discrepancy on invoice #4821 Open 1/18/2025 ✦ I've displayed the Union Bank information card. Type a message...

This show_task action fetches a task in execute, then renders an editable card in render. Because render uses callback and cancel, the agent waits until the user saves or cancels: Save passes the edited fields to callback, and Cancel aborts.

show-task.js
foldspace("when", "ready", () => {
foldspace.agent({ /* …common setup… */ })
.addActionHandlers({
show_task: {
// 1. Fetch the task by ID
execute: async (params) => {
const { taskId } = params;
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${taskId}`
);
const data = await response.json();
// Return the task object to both agent and renderer
return data;
},
// 2. Render an editable card UI
render: (task, host, header, callback, cancel) => {
const style = document.createElement('style');
style.textContent = `
.task-card {
border: 1px solid #e3e3e3;
border-radius: 8px;
margin-top: 8px;
font-family: sans-serif;
overflow: hidden;
}
.task-card-header {
background-color: #f5f5f5;
padding: 12px 16px;
font-weight: bold;
}
.task-card-body {
padding: 16px;
}
.task-card-body label {
display: block;
margin-bottom: 4px;
font-size: 14px;
}
.task-card-body input,
.task-card-body textarea {
width: 100%;
margin-bottom: 12px;
padding: 8px;
box-sizing: border-box;
font-size: 14px;
}
.task-card-footer {
padding: 12px 16px;
text-align: right;
background-color: #fafafa;
}
.task-card-footer button {
margin-left: 8px;
padding: 8px 16px;
cursor: pointer;
}
`;
header.appendChild(style);
// Build the card container
const card = document.createElement('div');
card.className = 'task-card';
// Header section (title)
const cardHeader = document.createElement('div');
cardHeader.className = 'task-card-header';
cardHeader.textContent = `Edit Task #${task.id}`;
// Body section (form fields)
const body = document.createElement('div');
body.className = 'task-card-body';
const titleLabel = document.createElement('label');
titleLabel.textContent = 'Title:';
const titleInput = document.createElement('input');
titleInput.type = 'text';
titleInput.value = task.title;
const bodyLabel = document.createElement('label');
bodyLabel.textContent = 'Body:';
const bodyInput = document.createElement('textarea');
bodyInput.rows = 4;
bodyInput.value = task.body;
body.append(titleLabel, titleInput, bodyLabel, bodyInput);
// Footer section (buttons)
const footer = document.createElement('div');
footer.className = 'task-card-footer';
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save';
saveBtn.onclick = () => {
callback({
id: task.id,
title: titleInput.value,
body: bodyInput.value,
});
};
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = cancel;
footer.append(cancelBtn, saveBtn);
// Assemble and mount
card.append(cardHeader, body, footer);
host.appendChild(card);
}
}
});
});