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.
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.
When to use it
Section titled “When to use it”- 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.
How it works
Section titled “How it works”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 options
Section titled “Render options”Your render function receives these arguments, in order.
| Parameter | Type | Description |
|---|---|---|
data | any | The value returned by your execute handler (e.g., an object or array of results). |
host | HTMLElement | The container element where you append your UI nodes (forms, cards, lists). |
header | HTMLElement | A header element you can style or populate with a title, instructions, or status. |
callback | (value: string, disableOnSubmit?: boolean) => void | Call 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 | () => void | Call this if the user aborts, so the agent resumes without new data. A default “Cancel” control is also rendered. |
Example
Section titled “Example”This show_task action fetches a task in execute, then renders an editable card in render. awaitUserInput: true pauses the agent until the user saves or cancels: Save passes the edited fields to callback, and Cancel aborts.
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. Tell the agent to wait for user input before continuing awaitUserInput: true,
// 3. 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); } } });});Related
Section titled “Related”- Execute actions: return data to the agent without rendering UI.
- What are Actions: the action model these build on.