Events Overview
Keelstone uses two types of events to communicate between your Salesforce LWC components and the Office taskpane:
- Outbound events (LWC → taskpane):
postMessagemessages that trigger Office API calls - Office events (taskpane → LWC): real-time document change notifications forwarded to your flow
How messages reach Excel
LWC components run in Salesforce's domain and cannot call Excel APIs directly. When running inside the Keelstone add-in taskpane via Lightning Out, window.parent is the taskpane itself:
LWC component
│
└─ window.parent.postMessage(payload, '*')
│
Excel taskpane (window.addEventListener('message'))
│
Excel.run() → reads/writes workbook
│
event.source.postMessage(response, '*') [for two-way messages]
│
LWC component (window.addEventListener('message'))
Your LWC only needs to call window.parent.postMessage. The taskpane handles all Excel API calls.
Message format
All messages are plain JSON objects with a type discriminator:
{
type: 'KEELSTONE_WRITE',
// ...type-specific fields
}
Outbound events (LWC → taskpane)
| Event | Description |
|---|---|
KEELSTONE_INSERT | Insert a new worksheet from a base64 .xlsx file (template merge) |
KEELSTONE_WRITE | Write a 2D array to a range; optionally creates an Excel Table + named range |
KEELSTONE_QUERY | Execute SOQL server-side and write results to a range |
KEELSTONE_GET_RANGE | Request the current Excel selection address |
KEELSTONE_SAVE_TABLES | Read all tables on the active sheet and POST to /api/data/save |
Response events (taskpane → LWC)
Some messages trigger a response posted back to the LWC via event.source.postMessage. Listen with window.addEventListener('message') and filter by event.data.type.
| Response | Triggered by | Description |
|---|---|---|
KEELSTONE_WRITE_RESULT | KEELSTONE_WRITE | { tableName, address, rowCount } — table name is null when namedRange: false |
KEELSTONE_RANGE_RESPONSE | KEELSTONE_GET_RANGE | { address } — the full sheet-qualified selection address |
KEELSTONE_SAVE_RESULT | KEELSTONE_SAVE_TABLES | { saved, errors } — record counts from /api/data/save |
KS_OFFICE_EVENT — Real-time document events
The taskpane monitors the open document and forwards events to any open flow dialog or inline taskpane flow via window.postMessage. These are not triggered by LWC code — they arrive automatically whenever the user interacts with the document.
{
type: 'KS_OFFICE_EVENT',
eventType: 'excel:selectionChanged', // or word:selectionChanged, excel:changed, etc.
data: { ... } // event-specific payload
}
Supported event types
eventType | App | Fires when | data payload |
|---|---|---|---|
word:selectionChanged | Word | User selects or moves the cursor | { selectedText: "..." } |
excel:selectionChanged | Excel | User selects a range | { address: "Sheet1!A1:B3", values: [[...]] } |
excel:changed | Excel | Cell content changes | { address: "A1", worksheetId: "..." } |
excel:sheetActivated | Excel | User switches to a different sheet | { worksheetId: "..." } |
ppt:selectionChanged | PowerPoint | User selects a slide | { slides: [{ id, index, title }] } |
Events are debounced at 250 ms to prevent flooding during rapid selections. No flow or action configuration is required — forwarding is active whenever the taskpane is open.
Listening for office events in your LWC
connectedCallback() {
this._handler = this._onMessage.bind(this);
window.addEventListener('message', this._handler);
}
disconnectedCallback() {
window.removeEventListener('message', this._handler);
}
_onMessage(event) {
const msg = event.data;
if (!msg) return;
// Office event from the taskpane
if (msg.type === 'KS_OFFICE_EVENT') {
const { eventType, data } = msg;
if (eventType === 'excel:selectionChanged') {
this.selectedRange = data.address;
this.selectedValues = data.values;
}
if (eventType === 'word:selectionChanged') {
this.selectedText = data.selectedText;
}
if (eventType === 'excel:sheetActivated') {
this.activeSheet = data.worksheetId;
}
return;
}
// Standard taskpane response events
switch (msg.type) {
case 'KEELSTONE_RANGE_RESPONSE':
this.activeRange = msg.address;
break;
case 'KEELSTONE_WRITE_RESULT':
this.tableName = msg.tableName;
this.recordCount = msg.rowCount;
break;
case 'KEELSTONE_SAVE_RESULT':
// show toast or update UI
break;
}
}
How events reach flows launched in a dialog
When the flow runs in a dialog (the default Launch Target), the relay server bridges the events:
taskpane emits ks:office-event on its socket
↓
server looks up the dialog socket for the same session
↓
server relays ks:office-event to the dialog
↓
flow-host converts it to window.postMessage({ type: 'KS_OFFICE_EVENT', ... })
↓
your LWC receives it via window.addEventListener('message')
For flows launched in the taskpane itself (Launch Target = Taskpane), events are posted directly without the relay.
Listening for responses in your LWC
Always clean up message listeners in disconnectedCallback:
connectedCallback() {
this._handler = this._onMessage.bind(this);
window.addEventListener('message', this._handler);
}
disconnectedCallback() {
window.removeEventListener('message', this._handler);
}
Security
The taskpane validates the origin of all incoming outbound event messages:
if (!/\.salesforce\.com$|\.force\.com$/.test(event.origin)) return;
if (!msg?.type?.startsWith('KEELSTONE_')) return;
Only messages from Salesforce domains with a KEELSTONE_ type prefix are processed. KS_OFFICE_EVENT messages flow in the opposite direction (taskpane → LWC) and do not require origin validation on the receiving end.