Events are notifications sent from the extension to the webview. They provide real-time updates about workflow execution, state changes, and system status. All events are defined in workflow/Core/Contracts/Protocol.ts and follow a strict type-safe contract.
Extension (VS Code) → Webview (React UI)
Events are notifications - they inform the webview about changes but don’t expect a response. The webview listens for events and updates its UI accordingly.
These events track the lifecycle of workflow operations:
These events track the execution status of workflows and nodes:
These events stream content from LLM and CLI nodes:
These events provide system configuration information:
These events handle subflow operations:
These events are forwarded when viewing a subflow:
These events handle clipboard operations:
Type: workflow_loaded
Payload: WorkflowPayloadDTO
Description: Sent when workflow data is successfully loaded from disk.
Payload Structure:
{
nodes?: WorkflowNodeDTO[]
edges?: EdgeDTO[]
state?: WorkflowStateDTO
resume?: ResumeDTO
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'workflow_loaded') {
const { nodes, edges, state } = event.data.data
// Update canvas with loaded workflow
setNodes(nodes)
setEdges(edges)
// Restore execution state
if (state) {
restoreExecutionState(state)
}
}
})
Type: workflow_saved
Payload: { path?: string }
Description: Sent when workflow is successfully saved to disk.
Payload Structure:
{
path?: string // Optional path where workflow was saved
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'workflow_saved') {
const path = event.data.data?.path
showNotification(`Workflow saved${path ? ` to ${path}` : ''}`)
}
})
Type: workflow_save_failed
Payload: { error?: string }
Description: Sent when workflow save operation fails.
Payload Structure:
{
error?: string // Optional error message
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'workflow_save_failed') {
const error = event.data.data?.error ?? 'Unknown error'
showNotification(`Save failed: ${error}`, 'error')
}
})
Type: execution_started
Payload: None
Description: Sent when workflow execution begins.
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'execution_started') {
setExecutionState('running')
resetNodeResults()
}
})
Type: execution_completed
Payload: { stoppedAtNodeId?: string }
Description: Sent when workflow execution completes successfully.
Payload Structure:
{
stoppedAtNodeId?: string // Optional node ID where execution stopped
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'execution_completed') {
const stoppedAt = event.data.data?.stoppedAtNodeId
setExecutionState('completed')
showNotification(
stoppedAt
? `Execution completed at node ${stoppedAt}`
: 'Execution completed successfully'
)
}
})
Type: execution_paused
Payload: { stoppedAtNodeId?: string }
Description: Sent when workflow execution is paused (e.g., for user approval).
Payload Structure:
{
stoppedAtNodeId?: string // Node ID where execution paused
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'execution_paused') {
const stoppedAt = event.data.data?.stoppedAtNodeId
setExecutionState('paused')
showNotification(`Execution paused at node ${stoppedAt}`)
}
})
Type: node_execution_status
Payload: NodeExecutionPayload
Description: Sent when a node’s execution status changes.
Payload Structure:
{
nodeId: string
status: 'running' | 'completed' | 'error' | 'interrupted' | 'pending_approval'
result?: string
multi?: string[]
command?: string
}
Fields:
nodeId: Node identifierstatus: Current execution statusresult: Execution result (for completed/error nodes)multi: Multi-output results (for nodes like Subflow)command: CLI command (for CLI nodes)Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'node_execution_status') {
const { nodeId, status, result } = event.data.data
updateNodeStatus(nodeId, status)
if (status === 'completed' && result) {
updateNodeResult(nodeId, result)
} else if (status === 'error') {
showNodeError(nodeId, result)
} else if (status === 'pending_approval') {
showApprovalDialog(nodeId, event.data.data.command)
}
}
})
Type: node_assistant_content
Payload: NodeAssistantContentEvent
Description: Streams assistant content from LLM nodes during execution.
Payload Structure:
{
nodeId: string
threadID?: string
content: AssistantContentItem[]
mode?: 'workflow' | 'single-node'
}
Fields:
nodeId: Node identifierthreadID: Thread identifier for chat continuitycontent: Array of assistant content itemsmode: Execution mode hintContent Types:
text: Plain text contentuser_message: User message in conversationthinking: Assistant’s internal reasoningtool_use: Tool/function call requesttool_result: Tool/function call resultserver_tool_use: Server-side tool executionserver_web_search_result: Web search resultsUsage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'node_assistant_content') {
const { nodeId, content } = event.data.data
content.forEach(item => {
switch (item.type) {
case 'text':
appendNodeContent(nodeId, item.text)
break
case 'tool_use':
showToolCall(nodeId, item.name, item.inputJSON)
break
case 'tool_result':
showToolResult(nodeId, item.resultJSON)
break
}
})
}
})
Type: node_output_chunk
Payload: NodeOutputChunkEvent
Description: Streams output chunks from CLI nodes during execution.
Payload Structure:
{
nodeId: string
chunk: string
stream: 'stdout' | 'stderr'
}
Fields:
nodeId: Node identifierchunk: Output chunk textstream: Output stream typeUsage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'node_output_chunk') {
const { nodeId, chunk, stream } = event.data.data
if (stream === 'stdout') {
appendStdout(nodeId, chunk)
} else {
appendStderr(nodeId, chunk)
}
}
})
Type: token_count
Payload: TokenCountEvent
Description: Reports token count for text processing.
Payload Structure:
{
count: number
nodeId: string
}
Fields:
count: Token countnodeId: Node identifierUsage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'token_count') {
const { count, nodeId } = event.data.data
updateNodeTokenCount(nodeId, count)
}
})
Type: models_loaded
Payload: Model[]
Description: Reports available LLM models.
Payload Structure:
Model[] // Array of available models
Model Structure:
interface Model {
id: string
title?: string
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'models_loaded') {
const models = event.data.data
setAvailableModels(models)
}
})
Type: provide_custom_nodes
Payload: WorkflowNodeDTO[]
Description: Reports custom nodes loaded from storage.
Payload Structure:
WorkflowNodeDTO[] // Array of custom nodes
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'provide_custom_nodes') {
const customNodes = event.data.data
setCustomNodes(customNodes)
}
})
Type: storage_scope
Payload: { scope: 'workspace' | 'user'; basePath?: string }
Description: Reports current storage scope configuration.
Payload Structure:
{
scope: 'workspace' | 'user'
basePath?: string
}
Fields:
scope: Current storage scopebasePath: Base path for storageUsage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'storage_scope') {
const { scope, basePath } = event.data.data
updateStorageScopeDisplay(scope, basePath)
}
})
Type: subflow_saved
Payload: { id: string }
Description: Confirms subflow was successfully saved.
Payload Structure:
{
id: string // Subflow identifier
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'subflow_saved') {
const subflowId = event.data.data.id
showNotification(`Subflow ${subflowId} saved`)
refreshSubflowList()
}
})
Type: provide_subflow
Payload: SubflowDefinitionDTO
Description: Provides subflow data when requested.
Payload Structure:
SubflowDefinitionDTO // Complete subflow definition
SubflowDefinitionDTO Structure:
interface SubflowDefinitionDTO {
id: string
title: string
version: string
inputs: SubflowPortDTO[]
outputs: SubflowPortDTO[]
graph: SubflowGraphDTO
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'provide_subflow') {
const subflow = event.data.data
loadSubflowIntoCanvas(subflow)
}
})
Type: provide_subflows
Payload: Array<{ id: string; title: string; version: string }>
Description: Provides list of available subflows.
Payload Structure:
Array<{
id: string
title: string
version: string
}>
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'provide_subflows') {
const subflows = event.data.data
setAvailableSubflows(subflows)
}
})
Type: subflow_copied
Payload: { nodeId: string; oldId: string; newId: string }
Description: Confirms subflow duplication.
Payload Structure:
{
nodeId: string // Node that received the new subflow
oldId: string // Original subflow ID
newId: string // New subflow ID
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'subflow_copied') {
const { nodeId, oldId, newId } = event.data.data
updateNodeSubflowId(nodeId, newId)
showNotification(`Subflow duplicated: ${oldId} → ${newId}`)
}
})
Type: subflow_node_execution_status
Payload: { subflowId: string; payload: NodeExecutionPayload }
Description: Forwards node execution status from a subflow.
Payload Structure:
{
subflowId: string
payload: NodeExecutionPayload
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'subflow_node_execution_status') {
const { subflowId, payload } = event.data.data
updateSubflowNodeStatus(subflowId, payload.nodeId, payload.status)
}
})
Type: subflow_node_assistant_content
Payload: SubflowNodeAssistantContentEvent
Description: Forwards assistant content from nodes within a subflow.
Payload Structure:
{
subflowId: string
nodeId: string
threadID?: string
content: AssistantContentItem[]
mode?: 'workflow' | 'single-node'
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'subflow_node_assistant_content') {
const { subflowId, nodeId, content } = event.data.data
appendSubflowNodeContent(subflowId, nodeId, content)
}
})
Type: clipboard_paste
Payload: WorkflowPayloadDTO
Description: Provides pasted clipboard content.
Payload Structure:
{
nodes?: WorkflowNodeDTO[]
edges?: EdgeDTO[]
state?: WorkflowStateDTO
resume?: ResumeDTO
}
Usage:
Example:
window.addEventListener('message', (event) => {
if (event.data.type === 'clipboard_paste') {
const { nodes, edges } = event.data.data
if (nodes && edges) {
insertNodesAndEdges(nodes, edges)
}
}
})
import type { ExtensionToWorkflow } from './Protocol'
function handleEvent(message: ExtensionToWorkflow) {
switch (message.type) {
case 'node_execution_status':
// TypeScript knows message.data is NodeExecutionPayload
console.log(message.data.nodeId, message.data.status)
break
case 'node_assistant_content':
// TypeScript knows message.data has content array
message.data.content.forEach(item => {
console.log(item.type, item.text)
})
break
case 'workflow_loaded':
// TypeScript knows message.data is WorkflowPayloadDTO
const { nodes, edges, state } = message.data
// Update UI...
break
// ... other event types
}
}
window.addEventListener('message', (event) => {
const message = event.data as ExtensionToWorkflow
// Filter by event type
if (message.type === 'node_execution_status') {
// Handle execution status
}
// Filter by category
if (message.type.endsWith('_event')) {
// Handle all events
}
// Filter by pattern
if (message.type.startsWith('execution_')) {
// Handle execution events
}
})
class EventAggregator {
private listeners: Map<string, ((data: any) => void)[]> = new Map()
on(eventType: string, callback: (data: any) => void) {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, [])
}
this.listeners.get(eventType)!.push(callback)
}
handleEvent(message: ExtensionToWorkflow) {
const callbacks = this.listeners.get(message.type)
if (callbacks) {
callbacks.forEach(cb => cb(message.data))
}
}
}
// Usage
const aggregator = new EventAggregator()
aggregator.on('node_execution_status', (data) => {
console.log('Node status:', data.nodeId, data.status)
})
workflow_loaded → execution_started → execution_completednode_execution_status (running) → node_execution_status (completed)node_assistant_content or node_output_chunk events for a single nodeSome events may be throttled to prevent UI overload:
node_assistant_content and node_output_chunk are batchednode_execution_status may be throttled for high-frequency updatestoken_count events are debouncedwindow.addEventListener('message', (event) => {
const message = event.data as ExtensionToWorkflow
switch (message.type) {
case 'node_execution_status':
if (!message.data) {
console.error('Missing execution status data')
return
}
// Process data...
break
case 'workflow_loaded':
if (!message.data?.nodes) {
console.warn('Loaded workflow has no nodes')
}
// Process data...
break
}
})
function validateEvent(message: ExtensionToWorkflow): boolean {
switch (message.type) {
case 'node_execution_status':
return (
typeof message.data?.nodeId === 'string' &&
['running', 'completed', 'error', 'interrupted', 'pending_approval']
.includes(message.data.status)
)
case 'workflow_loaded':
return Array.isArray(message.data?.nodes)
default:
return true
}
}
node_assistant_content, node_output_chunk, node_execution_statusworkflow_loaded, execution_completed, models_loadedclass EventBuffer {
private buffer: ExtensionToWorkflow[] = []
private maxBufferSize = 100
add(message: ExtensionToWorkflow) {
this.buffer.push(message)
if (this.buffer.length > this.maxBufferSize) {
this.buffer.shift() // Remove oldest
}
}
flush(): ExtensionToWorkflow[] {
const events = [...this.buffer]
this.buffer = []
return events
}
}