---
title: "An AI-assisted rich text editor component for Retool"
description: "Rich text editing inside a Retool app, with an AI model drafting alongside the user."
canonical: "https://stackdrop.co/blog/ai-rich-text-editor-retool-component"
published: "2026-04-27T09:00:00.000Z"
updated: "2026-10-24T00:36:59.900Z"
authors: ["John Miniadis"]
topics: ["Retool", "Building with AI"]
readers: ["builders"]
---

# An AI-assisted rich text editor component for Retool

A support agent wants the model to rewrite one paragraph of a draft reply and leave the other four alone. A text area hands the app one string. To touch a single paragraph, you would write your own selection and parsing logic, and then parse the result again before it reaches a query.

We needed two things for a client's internal support tool. AI had to act on one block of text at a time. The output had to arrive as structured JSON, ready for queries, tables, and workflows without a parsing step. The result is an open-source Retool custom component built on [Editor.js](https://editorjs.io). We walked through it live at Retool's Build Together session. The code is on [GitHub](https://github.com/StackdropCO/retool-ai-editor).

If you want the block editor without the AI layer, see [Editor.js for Retool](https://stackdrop.co/blog/editor-js-retool-component).

## What the component does

Editor.js stores every paragraph, heading, and list as its own JSON block. The component adds three AI actions on top:

- **Rewrite** and **Summarize** sit in each block's settings menu (the three-dot toolbar). The response replaces that block in place through `editor.blocks.update(id, data)`.
- **Draft reply** sits in the slash menu. Typing `/` and picking it inserts a placeholder block, then fills it with one paragraph block per chunk of the response.

None of the three calls a model from inside the component. The component fires a Retool event, a Retool query reads the context and calls the model, and the query writes the answer back. The prompt, the model, and the context stay in the Retool app, where you can change them without touching component code. That also means any endpoint works: Retool AI, OpenAI, Anthropic, or your own.

All Retool-specific wiring lives in one file, `src/components/AIEditor/index.tsx`. Swap a few hooks and the same editor runs as a standalone React component.

## How the event model works

Retool custom component events carry no payload. The component sets its state properties first, then fires the event. Your query reads the state.

```typescript title="src/components/AIEditor/index.tsx (excerpt)"
import { Retool } from "@tryretool/custom-component-support";

// Output state: the component writes, the app reads
const [, setTransformBlockContent] = Retool.useStateString({
  name: "transformBlockContent",
  inspector: "hidden",
});
const [, setTransformType] = Retool.useStateString({ name: "transformType", inspector: "hidden" });
const [transformBlockId, setTransformBlockId] = Retool.useStateString({
  name: "transformBlockId",
  inspector: "hidden",
});

// Input state: the app writes, the component reads
const [transformResponse] = Retool.useStateString({
  name: "transformResponse",
  inspector: "text",
  label: "Transform response",
});

const aiTransformRequest = Retool.useEventCallback({ name: "aiTransformRequest" });

// On a block action: set state first, then fire
setTransformBlockContent(content);
setTransformType(type);
setTransformBlockId(blockId);
aiTransformRequest();
```

When the query returns, the component compares the new `transformResponse` with the last one it applied. If the model returns identical text twice in a row, the component sees no change and nothing happens. The fix is to append a zero-width space in the query, `String.fromCharCode(8203)`, so every response registers as new.

## Component properties

Output properties (the component writes, your app reads):

- `editorData` (object): the full Editor.js JSON, updated on every change, debounced at 400 ms.
- `transformBlockContent` (string): the text of the block being transformed, empty for a draft insert.
- `transformType` (string): `summarize`, `rewrite`, or `draft`.
- `transformBlockId` (string): the Editor.js id of the block being transformed or replaced.
- `transformScope` (string): `block` for the settings-menu actions, `insert` for Draft reply.

Input properties (your app writes, the component reads):

- `transformResponse` (string): the model's response. Bind it to your query result.
- `readOnly` (boolean): toggles read-only mode.

Two events: `aiTransformRequest` fires when someone picks any AI action, and `editorChange` fires on content changes, debounced at 400 ms.

## Install it

You need Node.js 20 or later and admin permissions on your Retool organization. You also need a Retool API access token with read and write scopes for Custom Component Libraries.

```bash title="terminal"
git clone https://github.com/StackdropCO/retool-ai-editor.git ai-editor-retool
cd ai-editor-retool
npm install

npx retool-ccl login    # paste the API access token
npx retool-ccl init     # first time only
npx retool-ccl dev      # syncs local changes to Retool on every save
```

Open any Retool app and drag the `AIEditor` component onto the canvas. To see the output, bind a Table or JSON Viewer to `{{ AIEditor1.editorData.blocks }}` and watch the blocks update as you type.

When it is ready to ship:

```bash title="terminal"
npx retool-ccl deploy
```

Deploy pushes an immutable version to your Retool organization. Pin each app to a specific version under Custom Component settings in the app editor. Anyone in the organization can then add the component from the component panel.

## Wire up the AI query

Create a query that calls your model endpoint. Branch on `{{ AIEditor1.transformScope }}` first, to pick the prompt shape, then on `{{ AIEditor1.transformType }}` for the action. For block actions, pass `{{ AIEditor1.transformBlockContent }}` as the user content.

For Draft reply, the block content is empty. Ground the draft in app state instead: the selected ticket, the order record, the customer's last message. The query runs in the app's full scope, so reference any query or component directly:

```text title="prompt context"
{{ ticketQuery.data }}
{{ ticketsTable.selectedRow }}
{{ toneSelect.value }}
```

Return paragraphs separated by blank lines. The component splits them and inserts one paragraph block per chunk.

Then add an event handler on `AIEditor1`:

1. Event: `aiTransformRequest`.
2. Action: trigger your transform query.
3. On success: set `AIEditor1.transformResponse` to `{{ aiTransformQuery.data }}`.

The component tracks `transformBlockId` itself, so your query never needs to.

## Where it fits, and where it does not

The component suits replies, drafts, and short-form content: support tools, sales operations, and content review. Three limits come from the editor and the current design:

- It gets heavy past about 50 blocks, so it is the wrong choice for long documents.
- Editor.js has no built-in collaborative editing, so two people cannot edit the same draft at once.
- Responses do not stream. The editor shows a "Drafting reply..." state until the full response arrives.

## Our view

We recommend routing every AI action through a Retool query, because the prompt and the context then stay where the app's owner can change them. A model called from inside the component hides both. The same choice keeps the model swappable and puts each call under the app's existing permissions on that resource. The component only edits text. For when a custom component earns its place at all, see [what a custom Retool component is, and when you need one](https://stackdrop.co/blog/what-is-a-custom-retool-component). Pull requests and new use cases are welcome on the repository.
