nebulaflow

Events API Reference

Overview

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.

Event Flow

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.

Event Categories

Workflow State Events

These events track the lifecycle of workflow operations:

  1. workflow_loaded - Workflow data loaded from disk
  2. workflow_saved - Workflow successfully saved
  3. workflow_save_failed - Workflow save failed

Execution Events

These events track the execution status of workflows and nodes:

  1. execution_started - Workflow execution started
  2. execution_completed - Workflow execution completed
  3. execution_paused - Workflow execution paused
  4. node_execution_status - Node execution status update

Content Streaming Events

These events stream content from LLM and CLI nodes:

  1. node_assistant_content - LLM assistant content stream
  2. node_output_chunk - CLI output stream
  3. token_count - Token count result

Model & Configuration Events

These events provide system configuration information:

  1. models_loaded - Available LLM models loaded
  2. provide_custom_nodes - Custom nodes loaded
  3. storage_scope - Storage scope information

Subflow Events

These events handle subflow operations:

  1. subflow_saved - Subflow saved successfully
  2. provide_subflow - Subflow data provided
  3. provide_subflows - List of subflows provided
  4. subflow_copied - Subflow duplicated

Subflow-Scoped Events

These events are forwarded when viewing a subflow:

  1. subflow_node_execution_status - Node execution status in subflow
  2. subflow_node_assistant_content - Assistant content in subflow

Clipboard Events

These events handle clipboard operations:

  1. clipboard_paste - Clipboard paste result

Event Details

Workflow State Events

workflow_loaded

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)
        }
    }
})

workflow_saved

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}` : ''}`)
    }
})

workflow_save_failed

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')
    }
})

Execution Events

execution_started

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()
    }
})

execution_completed

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'
        )
    }
})

execution_paused

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}`)
    }
})

node_execution_status

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:

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)
        }
    }
})

Content Streaming Events

node_assistant_content

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:

Content Types:

Usage:

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
            }
        })
    }
})

node_output_chunk

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:

Usage:

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)
        }
    }
})

token_count

Type: token_count
Payload: TokenCountEvent

Description: Reports token count for text processing.

Payload Structure:

{
    count: number
    nodeId: string
}

Fields:

Usage:

Example:

window.addEventListener('message', (event) => {
    if (event.data.type === 'token_count') {
        const { count, nodeId } = event.data.data
        updateNodeTokenCount(nodeId, count)
    }
})

Model & Configuration Events

models_loaded

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)
    }
})

provide_custom_nodes

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)
    }
})

storage_scope

Type: storage_scope
Payload: { scope: 'workspace' | 'user'; basePath?: string }

Description: Reports current storage scope configuration.

Payload Structure:

{
    scope: 'workspace' | 'user'
    basePath?: string
}

Fields:

Usage:

Example:

window.addEventListener('message', (event) => {
    if (event.data.type === 'storage_scope') {
        const { scope, basePath } = event.data.data
        updateStorageScopeDisplay(scope, basePath)
    }
})

Subflow Events

subflow_saved

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()
    }
})

provide_subflow

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)
    }
})

provide_subflows

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)
    }
})

subflow_copied

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}`)
    }
})

Subflow-Scoped Events

subflow_node_execution_status

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)
    }
})

subflow_node_assistant_content

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)
    }
})

Clipboard Events

clipboard_paste

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)
        }
    }
})

Event Handling Patterns

Type-Safe Event Handling

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
    }
}

Event Filtering

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
    }
})

Event Aggregation

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)
})

Event Timing & Ordering

Event Ordering Guarantees

  1. Workflow Lifecycle: workflow_loadedexecution_startedexecution_completed
  2. Node Execution: node_execution_status (running) → node_execution_status (completed)
  3. Streaming: Multiple node_assistant_content or node_output_chunk events for a single node
  4. Subflows: Subflow events are forwarded after inner node events

Event Throttling

Some events may be throttled to prevent UI overload:

Error Handling

Missing Event Data

window.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
    }
})

Event Validation

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
    }
}

Performance Considerations

Event Volume

Memory Management

class 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
    }
}