# UI components in chat

Let an action render an interactive component — a form, card, or confirmation — directly in the chat, then send the user's input back to the agent.

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.

<ChatterblockGallery />

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

- **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](/guides/executing-actions/) instead.

## 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.

<Mermaid chart={`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
`} />

:::note
For a component that takes input or a confirmation, set `awaitUserInput: true` so the agent pauses until the user submits or cancels.
:::

## Render options

<RenderParams />

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

<ChatterblockExample />

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.

```javascript
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

- [Execute actions](/guides/executing-actions/): return data to the agent without rendering UI.
- [What are Actions](/guides/text-driven-actions/): the action model these build on.
