Skip to main content

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

PlatformFolder
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}'
}
};
  • description is 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.
  • config fields marked secret: true are stored encrypted with the OS keyring and masked in the UI. Types: string, number (with min/max), boolean, select (with options).
  • group decides which tier the tool joins when Smart tool loading is on. custom is loaded on request; core is always loaded.
  • display placeholders: {count} (from created.length or data.results.length), {query}, {name}, {title}, {message}, {error}.
  • dependencies are installed with the system npm into the tool's folder the first time the tool loads (or when the list changes). npm must be on your PATH. 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 .js or the folder has an index.js; manifest.name must be unique.
  • Dependencies fail - npm isn't on the PATH DeepState 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.