AI in multi-root and multi-editor setups
CKEditor AI features support multi-root editors and multiple editors sharing a Context. When your application splits content into separate editing areas – for example, a title and a body – AI Chat, AI Review, and AI Translate operate seamlessly across all of them.
The editor below splits content into a title, a description, and a body. AI Chat, AI Review, and AI Translate work across all three.
- Multi-root editor – one
MultiRootEditorinstance with several editable regions (like a title, a description, a body, and so on). AI features operate across all roots. - Multiple editors sharing a
Context–Context.create( { /* ... */ } ). AI features operate across the editors in theContextinstance.
This is an early release of AI support for multi-root and multi-editor setups. Expect improvements and fixes in upcoming releases, which may result in breaking changes.
- AI Chat – the AI reads each root’s content together with its
labelanddescription, then decides which root to address based on the user’s prompt. - AI Chat history – conversation history is scoped per editor in a
Context. Loading a previous conversation maps correctly even if some editors or roots have been destroyed. - AI Review and AI Translate – run across all roots of a multi-root editor and across all editors sharing a
Context. Suggestions and translations land in the correct root and do not bleed across boundaries. Compatible with real-time collaboration.
This section shows how to configure CKEditor AI in the two supported setups. The base setup follows the AI integration guide; the snippets below highlight only what differs.
The fields worth pausing on are the root name, label, and description of each root – they look similar but each serves a distinct purpose. The AI uses them differently from assistive technologies, and the way they are declared differs between single-root editors and multi-root editors.
- Root name – for multi-root editors, the key under
config.rootsproperty. It identifies the root in the editor’s model and in collaboration sessions, and must be stable and unique per editor. Single-root editors (ClassicEditor,BalloonEditor,InlineEditor,DecoupledEditor) do not configure a name – their root is internally namedmainand the name cannot be customized. Root name is used only for content mapping purposes and is not processed by LLM. label– thearia-labelof the editable area. Used by the LLM to understand the meaning of particular section. Set it onconfig.rootfor single-root editors and on eachconfig.roots.<rootName>entry for multi-root editors. Users can refer particular sections in the prompt – “rewrite the description”, “add a paragraph to the body” – AI will uselabelto find correct target for the query.description– a short, human-readable description of what the root contains. Provides additional context for particular section beyondlabel. Treat the description like a short editor tooltip: a sentence that says what the root is for, oriented around its role (“Article body”, “Footnotes panel”), not its position on the page (“Right column”).
Without labels and descriptions, in multi-root or multi-editor setups, the AI cannot reliably distinguish editing areas: a suggested edit may land in the wrong area. The editor logs ai-document-root-missing-label and ai-chat-documents-missing-description warnings when this is detected.
In a multi-root editor setup, a single editor exposes multiple editing areas. The AI feature plugins load directly on the editor, just like in any single-root setup. Each editing area is then declared under config.roots.<rootName> with its DOM element, initial data, accessible label, and a description used by the AI.
MultiRootEditor
.create( {
plugins: [
AIChat, AIChatHistory, AIChatShortcuts,
AIQuickActions, AIReviewMode, AITranslate,
AIEditorIntegration, TrackChanges, /* ... */
],
roots: {
title: {
element: document.querySelector( '#title' ),
initialData: '<h1>...</h1>',
label: 'Article title',
description: 'Article title that names the piece for readers.'
},
description: {
element: document.querySelector( '#description' ),
initialData: '<p>...</p>',
label: 'Article description',
description: 'Article description with a short summary of the piece.'
},
body: {
element: document.querySelector( '#body' ),
initialData: '<p>...</p>',
label: 'Article body',
description: 'Main article body — the primary content of the piece.'
}
},
toolbar: [ 'toggleAi', 'aiQuickActions', /* ... */ ],
ai: {
container: {
type: 'sidebar',
element: document.querySelector( '.ai-sidebar' )
}
}
// ... Other configuration options ...
} )
.then( /* ... */ )
.catch( /* ... */ );
When several editors share a Context, the AI feature plugins move to the Context-level config.plugins array, so a single Chat, History, Review, and Translate UI is shared across all editors. The editor-integration plugins stay on each editor. The config.ai.container configuration also sits on the Context – the individual editors do not need their own config.ai configuration.
The Context must declare its own config.collaboration.channelId, separate from any channel IDs on the individual editors. AI Chat history is scoped per Context (not per editor), and AI will throw ai-chat-missing-channel-id if the Context has no channel ID configured.
Context
.create( {
plugins: [ AIChat, AIChatHistory, AIChatShortcuts, AIReviewMode, AITranslate ],
ai: {
container: {
type: 'sidebar',
element: document.querySelector( '.ai-sidebar' )
}
},
collaboration: {
channelId: 'shared-context-channel-id'
}
// ... Other configuration options ...
} )
.then( context => Promise.all( [
ClassicEditor.create( {
context,
attachTo: document.querySelector( '#article-editor' ),
plugins: [ AIEditorIntegration, AIQuickActions, TrackChanges, /* ... */ ],
root: {
label: 'Article body',
description: 'Main article body — the primary content of the piece.'
}
} ),
ClassicEditor.create( {
context,
attachTo: document.querySelector( '#sidebar-editor' ),
plugins: [ AIEditorIntegration, AIQuickActions, TrackChanges, /* ... */ ],
root: {
label: 'Related links',
description: 'Sidebar listing articles and resources related to the main piece.'
}
} )
] ) )
.then( /* ... */ )
.catch( /* ... */ );
This is an experimental feature. We will continue to refine the integration in upcoming releases, and more limitations may surface here as early adoption progresses.