This repo contains the custom component built during the Retool webinar on building custom components. It's an Editor.js block editor wired into Retool with block-level AI transformations.
Editor.js outputs clean JSON instead of HTML. Each paragraph, heading, and list is a discrete block with structured data, which makes it a better fit for internal tools where you need that data flowing through queries, tables, and workflows in the rest of your Retool app.
src/
components/
AIEditor/
index.tsx # React component with all Retool state/event wiring
AITransformTune.ts # Custom Block Tune for per-block AI actions
AIAction.ts # Custom Block Tool for slash-menu AI inserts (Draft reply)
AIEditor.module.css # Container styles
index.tsx # Barrel export for the AIEditor component
Every block gets an AI action menu in its settings panel (the three-dot toolbar). Options are Summarize and Rewrite. Each fires a Retool event with the block's content, transformation type, and block id. The LLM response replaces that block's text in place via editor.blocks.update(id, data); no index math, no re-inserts.
The menu is built with Editor.js's MenuConfig return value from the tune's render() method, so the buttons, icons, and hover states come from Editor.js itself.
Typing / opens the slash menu with a Draft reply entry. Picking it inserts a dimmed, pulsing placeholder block and fires aiTransformRequest with transformScope: 'insert' and transformType: 'draft'. When your query writes the response back to transformResponse, the component splits it on blank lines and replaces the placeholder with one paragraph block per chunk.
- Node.js v20+
- A Retool account with admin permissions
- A Retool API access token with read/write scopes for Custom Component Libraries
git clone <this-repo-url> ai-editor-retool
cd ai-editor-retool
npm installLog in to your Retool instance:
npx retool-ccl loginInitialize the library (first time only):
npx retool-ccl initStart dev mode:
npx retool-ccl devThis syncs your local changes to Retool on every save. Open any Retool app and drag the AIEditor component onto the canvas.
The component communicates with your Retool app through state properties and events.
Drag a Table or JSON Viewer onto the canvas. Bind its data to:
{{ AIEditor1.editorData.blocks }}
You'll see the block-level JSON update in real time as you type (debounced at 400ms).
Create a query that calls your LLM endpoint (Retool AI, OpenAI, Anthropic, etc.). Switch on {{ AIEditor1.transformScope }} first to pick the right prompt shape, then on {{ AIEditor1.transformType }} for the action. Use {{ AIEditor1.transformBlockContent }} as the user content for block scope. Reference any other app state in the prompt to ground the response.
For insert scope (Draft reply from the slash menu), transformBlockContent is empty; ground the draft entirely from app state relevant to what you want to draft. Return blank-line-separated paragraphs; the component splits the response and inserts one paragraph block per chunk, replacing the placeholder.
Add an event handler on the AIEditor1 component:
- Event:
aiTransformRequest - Action: Trigger
aiTransformQuery - Then: Set
AIEditor1.transformResponseto{{ aiTransformQuery.data }}
The component reads transformBlockId internally to know which block to update or replace, so your query doesn't need to touch it.
Your transform query runs in the Retool app's full scope, so reference any query or component directly in the prompt:
{{ someQuery.data }}
{{ someTable.selectedRow }}
{{ someInput.value }}
No extra binding on the component is needed. The component owns the editor; the query owns the context.
| Property | Type | Description |
|---|---|---|
editorData |
object | Full Editor.js JSON output, updated on every change |
transformBlockContent |
string | Text content of the block being transformed (empty for insert scope) |
transformType |
string | summarize, rewrite, or draft |
transformBlockId |
string | Editor.js id of the block being transformed or replaced |
transformScope |
string | block (tune-menu transforms) or insert (slash-menu Draft reply) |
| Property | Type | Description |
|---|---|---|
transformResponse |
string | LLM response for block transforms. Bind to your query result. |
readOnly |
boolean | Toggle read-only mode from your app. |
| Event | Fires when |
|---|---|
aiTransformRequest |
User picks a transformation from the block menu or Draft reply from the slash menu |
editorChange |
Editor content changes (debounced 400ms) |
Retool custom component events carry no payload. The pattern is: set state first, then fire the event. Your event handler reads the state to get the data it needs.
// Component sets block content, type, and id, then fires the event
setTransformBlockContent(content)
setTransformType(type)
setTransformBlockId(blockId)
aiTransformRequest()
// In Retool, the query reads {{ AIEditor1.transformBlockContent }} and {{ AIEditor1.transformType }}This is the same pattern used across all Retool custom components. The CCL TypeScript API docs cover this in detail.
When you're ready to ship:
npx retool-ccl deployThis pushes an immutable version to your Retool instance. Pin specific versions per-app under Custom Component settings in the app editor.