Custom tools PRO
A custom tool is a JavaScript module the assistant can call like any built-in tool. Write one to wrap an internal database, a paid data source you have access to, a scraper for a site you follow, or a repetitive board-building routine. Tools run inside DeepState with access to the board, the scraper and the summarizer.
Pro and Enterprise-Pro licenses load custom tools; the Custom Tools tab in Settings appears for them.
Where tools live
| Platform | Folder |
|---|---|
| macOS | ~/Library/Application Support/deepstate/custom-tools/ |
| Windows | %APPDATA%\deepstate\custom-tools\ |
| Linux | ~/.config/deepstate/custom-tools/ |
Settings → Custom Tools has an Open folder button. Two layouts are recognised:
custom-tools/
quick_note.js # single-file tool
hacker_news/ # folder tool
index.js
manifest.json # optional; manifest may also be exported from index.js
node_modules/ # created automatically if dependencies are declared
Tools are loaded when the app starts. The folder is watched, and when something changes a notice in Settings asks you to restart the app to pick it up - there is no live reload.
A minimal tool
// quick_note.js
export const manifest = {
name: 'quick_note',
displayName: 'Quick Note',
description: 'Create a note with a timestamp in the title',
version: '1.0.0',
group: 'custom',
schema: {
type: 'object',
properties: {
content: { type: 'string', description: 'Note body (Markdown)' }
},
required: ['content']
}
};
export async function call(input, ctx) {
const node = await ctx.createNode({
type: 'note',
title: `Quick note - ${new Date().toISOString().slice(0, 16)}`,
content: input.content
});
return { success: true, message: 'Created quick note', created: [{ id: node.id }] };
}
Modules are loaded as ES modules: use export, and await import() for dependencies.
Manifest
export const manifest = {
name: 'my_tool', // unique, snake_case - this is what the model calls
displayName: 'My Tool', // shown in Settings and the chat
description: 'What the tool does and when to use it. The model reads this.',
version: '1.0.0',
group: 'research', // core | entities | research | social | media | narratives | custom
schema: { // JSON Schema for the arguments
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
limit: { type: 'number', description: 'Max results', default: 10 }
},
required: ['query']
},
config: { // settings the user fills in under Settings → Custom Tools
apiKey: { type: 'string', label: 'API key', secret: true },
region: { type: 'select', label: 'Region', options: ['us', 'eu'], default: 'us' },
limit: { type: 'number', label: 'Default limit', default: 5, min: 1, max: 50 },
strict: { type: 'boolean', label: 'Strict matching', default: false }
},
dependencies: { // npm packages, folder tools only
'cheerio': '1.0.0'
},
display: { // how calls look in the chat
icon: 'Search', // any lucide-react icon name
pendingMessage: 'Searching…',
successMessage: 'Found {count} results for "{query}"',
failureMessage: 'Search failed: {error}'
}
};
descriptionis the most important field: the model decides whether to call your tool from it. Say what it returns and when it is the right choice.configfields markedsecret: trueare stored encrypted with the OS keyring and masked in the UI. Types:string,number(withmin/max),boolean,select(withoptions).groupdecides which tier the tool joins when Smart tool loading is on.customis loaded on request;coreis always loaded.displayplaceholders:{count}(fromcreated.lengthordata.results.length),{query},{name},{title},{message},{error}.dependenciesare installed with the systemnpminto the tool's folder the first time the tool loads (or when the list changes).npmmust be on yourPATH. Pin versions.
The call function
export async function call(input, ctx) { … }
input is the validated argument object. ctx is:
ctx.boardId // current board
ctx.config // values from the manifest's config fields
// board
ctx.searchBoard({ query, types?, limit? }) // full-text search → [{ id, type, title, content, tags, createdAt, updatedAt }]
ctx.createNode({ type, title, content?, tags?, x?, y? }) // → { id }
ctx.createConnection({ sourceId, targetId, label?, description? }) // → { id }
ctx.getNodes({ types?, limit? }) // → nodes with full content
ctx.getNode(id) // → node or null
ctx.getNarratives() // → [{ id, title, … }]
// utilities
ctx.scrapeUrl(url) // → { title, content, description, author, publishedAt, siteName, image, url }
ctx.summarize({ content, title?, url? }) // summarizer model → { summary, keyPoints, sentiment, topics }
// logging → Settings → Custom Tools → Recent logs
ctx.log(msg); ctx.warn(msg); ctx.error(msg)
createNode accepts every type except region. Node content is a string for notes and the
type's content object otherwise (for an embed, { url }; for an event, { description, eventDate, … }). Nodes are placed near the viewport centre unless you give x/y.
Return an object:
return {
success: true, // required
message: 'Found 3 filings', // required - shown to the user and the model
created: [{ id: node.id }], // optional - nodes you made; the canvas refreshes
data: { results } // optional - anything the model should see
};
On failure return { success: false, message: 'why' } rather than throwing; thrown errors are
caught and logged but give the model less to work with.
Example: a folder tool with a dependency
hacker_news/index.js
export const manifest = {
name: 'search_hacker_news',
displayName: 'Search Hacker News',
description: 'Search Hacker News stories and discussions. Returns title, URL, points, comment count and author. Optionally creates Embed nodes.',
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' }
},
required: ['query']
},
config: { default_limit: { type: 'number', label: 'Default result limit', default: 5 } },
dependencies: { 'cheerio': '1.0.0' },
display: { icon: 'Newspaper', pendingMessage: 'Searching Hacker News…', successMessage: 'Found {count} stories' }
};
export async function call(input, ctx) {
const limit = input.limit || ctx.config.default_limit || 5;
ctx.log(`Searching HN for ${input.query}`);
const res = await fetch(`https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(input.query)}&hitsPerPage=${limit}`);
if (!res.ok) return { success: false, message: `HN returned ${res.status}` };
const { hits } = await res.json();
const results = hits.map((h) => ({
title: h.title,
url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,
points: h.points, comments: h.num_comments, author: h.author
}));
const created = [];
if (input.create_nodes) {
for (const r of results) {
const node = await ctx.createNode({ type: 'embed', title: r.title, content: { url: r.url } });
created.push({ id: node.id });
}
}
return { success: true, message: `Found ${results.length} stories`, created, data: { results } };
}
(fetch is available globally; cheerio is declared only to show the dependency mechanism.)
Managing tools
Settings → Custom Tools lists every tool with its status - loaded, error (with the message) or missing config (a required config field is empty). Each card has a config form, recent logs, and the manifest details. Reload shows what changed; the actual reload happens on restart. The tab also contains a getting-started guide and the full API reference, so you can write a tool without leaving the app.
Security
Custom tools run with the same privileges as the app: they can read any file, make any network
request, and modify the board. Only install tools you have read. Secrets in config are
encrypted at rest, but a tool can of course read its own config - that is the point.
Troubleshooting
- Not listed after restart - check for a syntax error in the log; make sure the file ends in
.jsor the folder has anindex.js;manifest.namemust be unique. - Dependencies fail -
npmisn't on thePATHDeepState sees (launch from a terminal to test), or you are offline. - The model never calls it - improve the
description; test with Plan mode and ask "which tools do you have for X?". - Runs but the board doesn't update - return the created node IDs in
created.