Skip to main content

Capability Executors

A capability executor is a runtime engine that knows how to run capabilities of one or more types. Capabilities define what to do; executors define how to run them.

Executors are the bridge between the capability system and the actual code that does the work. When an agent starts a capability, the system finds the executor registered for that capability's type, spawns it as a child process, and the executor runs the capability logic using Codebolt APIs.

Executor architecture: capability types route to executors discovered from three sourcesCAPABILITY TYPESEXECUTORSSOURCESskillpowertalentcustomskill-executorsupportedTypes:[skill, power]talent-executorsupportedTypes:[talent]your-custom-executorgetExecutorForType()entry: dist/index.jsuses: codeboltjs SDKPROJECT.codebolt/capabilities/executors/priority 1 (highest)GLOBAL~/.codebolt/capabilities/executors/priority 2BUILT-IN<appRoot>/.codebolt/capabilities/executors/priority 3 (lowest)|

Architecture overview

Agent calls startCapability("my-skill", type: "skill")

├─ CapabilityManager
│ ├─ Looks up executor for type "skill" via CapabilityRegistry
│ ├─ Looks up capability "my-skill" to get its path and config
│ └─ Builds params: { capabilityPath, capabilityName, capabilityConfig, ... }

├─ SideExecutionManager
│ ├─ Spawns child Node.js process with env vars
│ ├─ Waits for WebSocket connection (30s timeout)
│ └─ Sends actionBlockInvocation message with params + threadContext

└─ Executor process (child)
├─ codeboltjs SDK auto-connects via WebSocket
├─ Receives invocation → calls your handler
├─ Your handler uses codebolt.fs, codebolt.terminal, etc.
├─ Returns result → SDK sends actionBlockComplete
└─ Process exits

Key components

ComponentRoleLocation
CapabilityRegistryDiscovers capabilities and executors from filesystem, maps types to executorsservices/capabilityRegistry.ts
CapabilityManagerOrchestrates capability execution — finds executor, builds params, tracks lifecyclemanagers/CapabilityManager.ts
SideExecutionManagerSpawns and manages child processes, handles WebSocket communication, process lifecyclemanagers/SideExecutionManager.ts
codeboltjs SDKClient-side library used by executors — auto-connects, receives invocations, sends resultspackages/codeboltjs

How executors communicate

Executors use the codeboltjs SDK — not raw WebSocket connections. The SDK handles the entire communication protocol automatically.

Connection flow

1. Server spawns executor as child Node.js process
└─ Sets env vars: SOCKET_PORT, IS_SIDE_EXECUTION=true,
SIDE_EXECUTION_ID, THREAD_ID, PARENT_AGENT_ID, etc.

2. codeboltjs SDK reads env vars and connects via WebSocket
└─ URL: ws://localhost:{SOCKET_PORT}/codebolt?IS_SIDE_EXECUTION=true&...
└─ All env vars passed as URL query parameters

3. Server detects IS_SIDE_EXECUTION=true in the connection
└─ Associates the socket with the parent agent's thread
└─ Sets ws.agentId = parentAgentId, ws.threadId = threadId
└─ Emits sideExecutionConnected event → resolves pending promise

4. Server sends actionBlockInvocation message
└─ Contains: params, threadContext, sideExecutionId, threadId, etc.
└─ Thread context sent via WebSocket (NOT env vars) to avoid HTTP 431 header size limits

5. codeboltjs calls your onActionBlockInvocation handler
└─ Passes (threadContext, metadata) to your function

6. Your handler runs, returns a result (or throws)

7. codeboltjs sends actionBlockComplete message
└─ Contains: sideExecutionId, result (or error)

8. Server resolves the pending completion promise
└─ CapabilityManager emits capabilityCompleted/capabilityFailed events
└─ Result returned to the calling agent

Environment variables

When the executor process starts, the codeboltjs SDK reads these environment variables automatically:

VariableDescription
SOCKET_PORTServer port for WebSocket connection
IS_SIDE_EXECUTIONAlways "true" — tells the SDK this is an executor process
SIDE_EXECUTION_IDUnique execution ID (format: side_<timestamp>_<uuid8>)
THREAD_IDThread ID — same as the parent agent's thread
PARENT_AGENT_IDParent agent identifier
PARENT_AGENT_INSTANCE_IDParent agent instance ID
ACTION_BLOCK_PATHPath to the executor directory
threadTokenJWT token for WebSocket authentication

You never need to read these manually — the SDK uses them to establish the connection.


Executor structure

Directory layout

my-executor/
executor.yaml # required — capability executor config
package.json # dependencies (must include @codebolt/codeboltjs)
src/
index.ts # source code
dist/
index.js # compiled entry point (built by webpack)
webpack.config.js # build configuration
tsconfig.json # TypeScript config

executor.yaml

name: skill-executor
version: 1.0.0
description: Executes skill-type capabilities
supportedTypes:
- skill
- power
entryPoint: dist/index.js
author: my-team
FieldTypeRequiredDescription
namestringyesUnique executor name
versionstringnoSemver (default 1.0.0)
descriptionstringnoWhat this executor does
supportedTypesstring[]yesCapability types this executor handles (at least one)
entryPointstringnoMain JS file (default dist/index.js)
authorstringnoAuthor name

Type-to-executor mapping

Each executor declares which capability types it supports via supportedTypes. The registry builds a mapping:

type "skill" → skill-executor
type "power" → skill-executor (same executor can handle multiple types)
type "talent" → talent-executor (different executor for different type)

A single executor can handle multiple types. If multiple executors declare the same type, the last one registered wins (project-level overrides global, which overrides built-in).

package.json

{
"name": "my-executor",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "npx webpack",
"dev": "npx tsx src/index.ts"
},
"dependencies": {
"@codebolt/codeboltjs": "latest"
},
"devDependencies": {
"typescript": "^5.3.3",
"ts-loader": "^9.5.1",
"webpack": "^5.103.0",
"webpack-cli": "^5.1.4"
}
}

Webpack configuration

Executors are bundled for Node.js using webpack:

// webpack.config.js
module.exports = {
target: 'node',
entry: './src/index.ts',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'commonjs2'
},
// Node built-ins are NOT polyfilled — executors run in Node.js
// @codebolt/* packages are bundled (not externalized)
};

Creating an executor

Step 1: Scaffold the project

mkdir -p my-executor/src
cd my-executor
npm init -y
npm install @codebolt/codeboltjs

Step 2: Write executor.yaml

name: my-executor
version: 1.0.0
description: Custom executor for power-type capabilities
supportedTypes:
- power
entryPoint: dist/index.js
author: my-team

Step 3: Write the executor code

// src/index.ts
import codebolt from '@codebolt/codeboltjs';

codebolt.onActionBlockInvocation(async (threadContext, metadata) => {
// Your capability logic here
const params = threadContext.params || {};

// Use any Codebolt API...
const result = await doWork(params);

// Return the result — SDK auto-sends it back
return result;
});

That's the entire pattern. Import codebolt, register a handler, return a result.

Step 4: Build and install

npm run build
cp -r . ../.codebolt/capabilities/executors/my-executor/

What your handler receives

threadContext

The full context from the parent agent's thread, with invocation params merged in:

{
// Conversation history from the parent thread
threadId: string;
messages: Array<{
messageId: string;
threadId: string;
content: string;
sender: string; // "user", "agent", "system"
timestamp: string;
templateType?: string;
payload?: any;
}>;

// Environment
projectPath: string; // path to the active project
agentId: string;
agentInstanceId: string;
metadata: Record<string, any>;

// Invocation parameters (passed by the calling agent)
params: {
capabilityPath: string; // filesystem path to the capability
capabilityName: string; // capability name
capabilityType: string; // capability type (e.g., "skill")
capabilityConfig: object; // full parsed capability.yaml
executionId: string; // unique execution ID
// ...plus any custom params from the caller
};
}

metadata

Execution metadata about the invocation:

{
sideExecutionId: string; // unique execution ID
threadId: string; // parent thread ID
parentAgentId: string; // parent agent ID
parentAgentInstanceId: string; // parent agent instance ID
timestamp: string; // invocation timestamp (ISO 8601)
}

Return value

Whatever you return from the handler becomes the capability result:

// Return data → sent as success result
return { files: ['a.ts', 'b.ts'], linesChanged: 42 };

// Return nothing → completion with no result data
return;

// Throw → sent as error result
throw new Error("File not found");

You can also return a structured result with additional context:

return {
result: { files: ['a.ts', 'b.ts'] },
additionalContext: {
executionTime: Date.now() - startTime,
warnings: ['deprecated API used']
}
};

Codebolt APIs available inside executors

Executors have access to all Codebolt APIs through the SDK. The executor runs with the same permissions as the parent agent and operates in the same thread context.

File system

// Read files
const content = await codebolt.fs.readFile('src/App.tsx');
const files = await codebolt.fs.readManyFiles(['src/a.ts', 'src/b.ts']);

// Write files
await codebolt.fs.createFile('src/NewComponent.tsx', componentCode);
await codebolt.fs.updateFile('src/App.tsx', updatedCode);

// Delete
await codebolt.fs.deleteFile('src/old.ts');
await codebolt.fs.deleteFolder('src/deprecated/');

// Search
const matches = await codebolt.fs.searchFiles('TODO', 'src/');
const grepResults = await codebolt.fs.grepSearch('console\\.log', 'src/');

Terminal

// Run commands
const result = await codebolt.terminal.executeCommand('npm test');
const build = await codebolt.terminal.executeCommand('npm run build');

// Long-running commands
await codebolt.terminal.executeCommandRunUntilError('npm run dev');

// Interrupt
await codebolt.terminal.sendManualInterrupt();

Other capabilities

// Start nested capabilities from within an executor
const lintResult = await codebolt.capability.startSkill('lint-check', { files });
const analysisResult = await codebolt.capability.startPower('deep-analysis');

// Start side executions
await codebolt.sideExecution.startWithActionBlock('my-action-block', params);

Full module list

ModuleExamples
codebolt.fsreadFile, createFile, updateFile, deleteFile, searchFiles, grepSearch
codebolt.terminalexecuteCommand, executeCommandRunUntilError, sendManualInterrupt
codebolt.projectgetProjectPath, getProjectFiles
codebolt.chatsendMessage, waitForReply
codebolt.browseropenUrl, screenshot, click
codebolt.capabilitylistSkills, startCapability, stopCapability
codebolt.sideExecutionstartWithActionBlock, startWithCode, stop, getStatus
codebolt.searchsearchCodebase, searchWeb
codebolt.taskcreateTask, updateTask, listTasks
codebolt.threadgetThread, listThreads
codebolt.llmchat, complete
codebolt.ragquery, index

Executors vs regular agents: API differences

FeatureExecutorRegular Agent
Entry pointcodebolt.onActionBlockInvocation()codebolt.onMessage() / codebolt.getMessage()
ReceivesthreadContext + metadata from parentUser messages independently
ThreadRuns in parent agent's threadHas its own thread
All Codebolt APIsYesYes
Can start nested capabilitiesYes (codebolt.capability.*)Yes
Can start side executionsYes (codebolt.sideExecution.*)Yes
Can receive user messagesNo — only receives invocationYes
Can spawn child agentsNoYes
Project scopeInherits parent's project pathOwn project scope

Full examples

Example: File processing executor

Handles analyze and format operations on files:

import codebolt from '@codebolt/codeboltjs';

codebolt.onActionBlockInvocation(async (threadContext, metadata) => {
const { filePath, operation } = threadContext.params;

if (!filePath) {
throw new Error("filePath parameter is required");
}

switch (operation) {
case 'analyze': {
const content = await codebolt.fs.readFile(filePath);
const lines = content.toString().split('\n').length;
return { filePath, lines, size: content.length };
}

case 'format': {
await codebolt.terminal.executeCommand(`npx prettier --write "${filePath}"`);
const formatted = await codebolt.fs.readFile(filePath);
return { filePath, formatted: true, content: formatted };
}

default:
throw new Error(`Unknown operation: ${operation}`);
}
});

Example: Test runner executor

Runs tests and returns structured results:

import codebolt from '@codebolt/codeboltjs';

codebolt.onActionBlockInvocation(async (threadContext, metadata) => {
const { testCommand, testFile } = threadContext.params;
const command = testCommand || (testFile ? `npm test -- ${testFile}` : 'npm test');

const result = await codebolt.terminal.executeCommand(command);

return {
success: result.exitCode === 0,
command,
output: result.stdout,
errors: result.stderr,
exitCode: result.exitCode
};
});

Example: Capability chaining executor

Orchestrates multiple capabilities together:

import codebolt from '@codebolt/codeboltjs';

codebolt.onActionBlockInvocation(async (threadContext, metadata) => {
const { files } = threadContext.params;

// Step 1: Run lint check
const lintResult = await codebolt.capability.startSkill('lint-check', { files });

if (!lintResult.success || !lintResult.result?.errors?.length) {
return { clean: true, message: "No lint errors found" };
}

// Step 2: Auto-fix lint errors
const fixResult = await codebolt.capability.startSkill('auto-fix', {
errors: lintResult.result.errors
});

// Step 3: Re-check
const recheckResult = await codebolt.capability.startSkill('lint-check', { files });

return {
autoFixed: true,
originalErrors: lintResult.result.errors.length,
fixesApplied: fixResult.result?.fixCount || 0,
remainingErrors: recheckResult.result?.errors?.length || 0
};
});

Example: Context-aware executor

Uses the parent agent's conversation history:

import codebolt from '@codebolt/codeboltjs';

codebolt.onActionBlockInvocation(async (threadContext, metadata) => {
const messages = threadContext.messages || [];
const lastUserMessage = messages
.filter(m => m.sender === 'user')
.pop();

// Use project context
const projectPath = threadContext.projectPath;

// Use the conversation context to inform the capability's work
return {
messageCount: messages.length,
lastUserRequest: lastUserMessage?.content,
projectPath,
parentAgent: metadata.parentAgentId,
executionId: metadata.sideExecutionId
};
});

Process lifecycle

Startup sequence

  1. SideExecutionManager spawns the executor as a child Node.js process
    • Working directory set to the executor's path
    • stdio: ['ignore', 'pipe', 'pipe', 'ipc'] — stdout/stderr piped, IPC enabled
  2. codeboltjs SDK initialises, reads env vars, connects via WebSocket
  3. Server detects IS_SIDE_EXECUTION=true, associates socket with parent thread
  4. Server sends actionBlockInvocation message with params and thread context
  5. Handler runs — your code executes
  6. SDK sends actionBlockComplete with result or error
  7. Process exits

Timeout handling

TimeoutDurationDescription
Connection timeout30 secondsExecutor must connect via WebSocket within this window
Execution timeoutConfigurableSet per-execution via timeout parameter (no default limit)
Force kill timeout3 secondsAfter SIGTERM, process is force-killed if still running
Shutdown timeout5 secondsTotal graceful shutdown window

Process termination

When an execution completes or is stopped:

  1. SIGTERM sent to the child process
  2. Wait up to 5 seconds for graceful shutdown
  3. SIGKILL if process is still alive after 3 seconds
  4. Temp files cleaned up (for runtime code executions)
  5. Execution metadata removed from tracking maps
  6. Events emitted (capabilityCompleted, capabilityFailed, or capabilityTimeout)

Error handling

Error codeWhen
ACTION_BLOCK_NOT_FOUNDExecutor entry point doesn't exist
INVALID_ACTION_BLOCKExecutor config is malformed
SYNTAX_ERRORJavaScript syntax error in executor code
EXECUTION_TIMEOUTExecution exceeded timeout
PROCESS_CRASHEDChild process exited with non-zero code
CONNECTION_FAILEDWebSocket connection not established within 30s
INVALID_REQUESTBad parameters passed to startCapability

If your handler throws an error, the SDK catches it and sends an actionBlockComplete message with the error. The calling agent receives:

{
success: false,
executionId: "cap_...",
error: "Your error message here"
}

Installing executors

Local installation

# Project-level (highest priority)
cp -r my-executor/ .codebolt/capabilities/executors/my-executor/

# Global (available across all projects)
cp -r my-executor/ ~/.codebolt/capabilities/executors/my-executor/

Then refresh the registry:

curl -X POST http://localhost:PORT/api/capability/refresh

Via REST API

curl -X POST http://localhost:PORT/api/capability/executors/create \
-H "Content-Type: application/json" \
-d '{
"name": "my-executor",
"version": "1.0.0",
"description": "Custom executor",
"supportedTypes": ["power"],
"entryPoint": "dist/index.js"
}'

This creates a scaffold with placeholder code. Replace dist/index.js with your built executor.

From marketplace

curl -X POST http://localhost:PORT/api/capability/download-executor \
-H "Content-Type: application/json" \
-d '{ "executorId": "executor-id-from-marketplace" }'

Executor priority and overriding

When multiple executors support the same capability type, the last one registered wins:

PrioritySourcePath
1 (highest)Project<project>/.codebolt/capabilities/executors/
2Global~/.codebolt/capabilities/executors/
3 (lowest)Built-in<appRoot>/.codebolt/capabilities/executors/

You can override a built-in executor by placing a custom one with the same supportedTypes at the project or global level.


Executor validation

An executor directory is valid when:

CheckRequiredFailure
Directory existsyesSkipped
executor.yaml or executor.yml presentyesSkipped with error log
name field in configyesSkipped with error log
supportedTypes with at least one entryyesSkipped with error log
entryPoint file existsnoWarning only (may need to be built)

Invalid executors are logged as warnings and excluded from the registry. They do not cause the registry to fail.


Debugging executors

Connection fails (30s timeout):

  • Check that dist/index.js exists and is valid JavaScript
  • Verify @codebolt/codeboltjs is installed and bundled
  • Check build output for errors: npm run build

Handler never called:

  • Ensure codebolt.onActionBlockInvocation() is called at the top level (not inside a conditional)
  • Check that the executor's supportedTypes includes the capability type you're starting

No result returned:

  • Make sure your handler returns a value or throws. Returning undefined sends a completion with no result data.
  • Check for unhandled promise rejections — add try/catch in your handler.

Console output:

  • console.log and console.error output is piped to the server debug logs
  • Use these for debugging during development

Check active executions:

  • SDK: await codebolt.capability.getExecutionStatus(executionId)
  • WebSocket: send { action: 'getActiveExecutions' }
  • REST: GET /api/capability/stats

See also