Custom Tools
Pro and Enterprise users can create custom tools to extend the AI assistant with new capabilities. Custom tools are JavaScript files that integrate seamlessly with the existing tool system.
Overview
Custom tools allow you to:
- Add new research sources (APIs, databases, services)
- Create specialized node types
- Automate repetitive tasks
- Integrate with your organization's tools
Getting Started
Tool Location
Place custom tools in the custom tools directory:
| Platform | Path |
|---|---|
| macOS | ~/.config/redline/custom-tools/ |
| Windows | %APPDATA%/redline/custom-tools/ |
| Linux | ~/.config/redline/custom-tools/ |
Tool Formats
Single-file tool:
custom-tools/
my-tool.js
Folder-based tool (with dependencies):
custom-tools/
my-tool/
index.js
package.json (auto-generated)
node_modules/ (auto-installed)
Creating a Simple Tool
Here's a minimal custom tool:
// quick_note.js
export const manifest = {
name: 'quick_note',
displayName: 'Quick Note',
description: 'Create a quick note with a timestamp',
version: '1.0.0',
group: 'core',
schema: {
type: 'object',
properties: {
content: {
type: 'string',
description: 'Note content'
}
},
required: ['content']
}
};
export async function call(input, ctx) {
const timestamp = new Date().toISOString();
const node = await ctx.createNode({
type: 'note',
title: `Quick Note - ${timestamp}`,
content: input.content
});
return {
success: true,
message: `Created quick note: ${node.id}`,
created: [{ id: node.id }]
};
}
Manifest Reference
The manifest defines your tool's metadata and interface:
export const manifest = {
// Required fields
name: 'my_tool', // Unique identifier (snake_case)
displayName: 'My Tool', // Shown in UI
description: 'What the AI sees when deciding to use this tool',
version: '1.0.0',
// Optional fields
group: 'custom', // Tool tier: core, entities, research,
// social, media, narratives, custom
schema: { // JSON Schema for tool input
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query for the AI'
},
limit: {
type: 'number',
description: 'Maximum results',
default: 10
}
},
required: ['query']
},
config: { // User-configurable settings (shown in Settings)
apiKey: {
type: 'string',
label: 'API Key',
secret: true // Encrypted storage
},
baseUrl: {
type: 'string',
label: 'API Base URL',
default: 'https://api.example.com'
}
},
dependencies: { // npm packages (folder-based tools only)
'axios': '^1.6.0',
'cheerio': '^1.0.0'
}
};
Schema Types
Support JSON Schema types:
stringnumberbooleanarrayobject
Include description for each property - the AI uses these to understand parameters.
Config Types
| Type | UI Element |
|---|---|
string | Text input |
string (secret: true) | Password input (encrypted storage) |
number | Number input |
boolean | Checkbox |
Context API
The call function receives (input, ctx):
Properties
// Access user configuration
const apiKey = ctx.config.apiKey;
// Current board ID
const boardId = ctx.boardId;
Board Operations
// Create a node
const node = await ctx.createNode({
type: 'note', // note, actor, organization, event, etc.
title: 'My Node',
content: 'Node content'
});
// Create a connection
await ctx.createConnection({
sourceId: 'node-1',
targetId: 'node-2',
label: 'relates to',
type: 'related'
});
// Search the board
const results = await ctx.searchBoard({
query: 'search term',
type: 'actor' // optional filter
});
// Get all nodes (optionally filter by type)
const actors = await ctx.getNodes({ type: 'actor' });
// Get a specific node
const node = await ctx.getNode('node-id');
// Get all narratives
const narratives = await ctx.getNarratives();
Utilities
// Scrape a URL
const content = await ctx.scrapeUrl('https://example.com');
// Returns: { title, content, url, ... }
// Summarize content with AI
const summary = await ctx.summarize({
content: 'Long text to summarize...',
maxLength: 200 // optional
});
Logging
// Log messages (appear in Settings > Custom Tools > Recent Logs)
ctx.log('Processing started');
ctx.warn('Rate limit approaching');
ctx.error('Failed to fetch data');
Return Value
Your tool should return an object:
return {
success: true, // Required: did it work?
message: 'Created 3 notes', // Required: human-readable result
// Optional: IDs of created nodes (triggers board refresh)
created: [
{ id: 'node-id-1' },
{ id: 'node-id-2' }
],
// Optional: any extra data for the AI
data: {
searchResults: [...],
metadata: {...}
}
};
Example: Hacker News Search
A folder-based tool with npm dependencies:
hacker_news/
index.js
// hacker_news/index.js
export const manifest = {
name: 'search_hacker_news',
displayName: 'Search Hacker News',
description: 'Search Hacker News for stories and discussions',
version: '1.0.0',
group: 'research',
schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
limit: {
type: 'number',
description: 'Max results',
default: 5
},
create_nodes: {
type: 'boolean',
description: 'Create embed nodes for results',
default: false
}
},
required: ['query']
},
config: {
default_limit: {
type: 'number',
label: 'Default Result Limit',
default: 5
}
},
dependencies: {
'node-fetch': '^3.3.0'
}
};
export async function call(input, ctx) {
const fetch = (await import('node-fetch')).default;
const limit = input.limit || ctx.config.default_limit || 5;
ctx.log(`Searching Hacker News for: ${input.query}`);
const response = await fetch(
`https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(input.query)}&hitsPerPage=${limit}`
);
const data = await response.json();
const results = data.hits.map(hit => ({
title: hit.title,
url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`,
points: hit.points,
comments: hit.num_comments,
author: hit.author
}));
const created = [];
if (input.create_nodes) {
for (const result of results) {
const node = await ctx.createNode({
type: 'embed',
url: result.url,
title: result.title
});
created.push({ id: node.id });
}
}
return {
success: true,
message: `Found ${results.length} stories${input.create_nodes ? `, created ${created.length} nodes` : ''}`,
created,
data: { results }
};
}
Tool Groups
Assign your tool to a group to control when it's loaded:
| Group | When Loaded |
|---|---|
core | Always available |
entities | When AI needs entity tools |
research | When AI needs research tools |
social | When AI needs social media tools |
media | When AI needs media tools |
narratives | When AI needs narrative tools |
custom | When AI requests custom tools |
The AI can request additional tool groups with get_more_tools.
Managing Custom Tools
Settings UI
Access custom tool management in Settings > Custom Tools:
- View all installed tools
- Configure tool settings
- View recent logs
- Enable/disable individual tools
Hot Reloading
Custom tools are reloaded when files change. No restart needed.
Dependency Installation
For folder-based tools with dependencies:
- Define dependencies in
manifest.dependencies - Redline auto-generates
package.json - Dependencies install automatically on first use
Debugging
View Logs
Check Settings > Custom Tools > Recent Logs for:
ctx.log()messagesctx.warn()warningsctx.error()errors- Execution errors
Common Issues
Tool not appearing:
- Check file is in correct directory
- Verify
manifest.nameis unique - Check for JavaScript syntax errors
Dependencies not installing:
- Use folder-based tool format
- Check
manifest.dependenciessyntax - Check network connectivity
Tool errors:
- Use
try/catchand return{ success: false, message: 'error' } - Log errors with
ctx.error() - Check the Recent Logs in Settings
Best Practices
- Clear descriptions - The AI uses your
descriptionto decide when to use the tool - Validate input - Check required fields before processing
- Handle errors - Always return
success: falsewith an error message on failure - Log progress - Use
ctx.log()for debugging - Return created nodes - Include
createdarray so the board updates - Respect rate limits - Add delays when calling external APIs
- Secure secrets - Use
secret: truefor API keys