Extension Development
If you want help building a Northfox plugin, open Codex or Claude Code in your extension directory first, then give it this guide:
That markdown file is written for a coding agent and can be used as a starting prompt or working guide while the plugin is being built.
If your goal is a reusable workflow rather than a deeper integration, build a skill instead: Skills: Create Your Own.
When to build a plugin
Build a plugin when you need Northfox to gain a new capability, such as:
- a custom external data source
- a custom tool
- a custom result card
- a workflow that depends on code rather than instructions alone
Core design principle
A good Northfox plugin gives the app small, clear capabilities.
That usually means:
- one tool should do one job
- tool names should be easy to understand
- arguments should stay simple
- results should return facts and structure, not a full polished final answer
Minimal plugin
Create a CommonJS module:
module.exports = {
manifest: {
id: 'example.echo',
name: 'Echo Plugin',
version: '1.0.0',
capabilities: ['tools', 'cards'],
contributes: {
tools: ['company_note'],
},
},
activate(api) {
api.registerTool({
name: 'company_note',
description: 'Return a short structured note for a ticker.',
parameters: {
type: 'object',
properties: {
symbol: {
type: 'string',
description: 'Ticker symbol to summarize.',
},
},
},
execute: async (args) => ({
content: [
{
type: 'text',
text: JSON.stringify({
symbol: args.symbol,
note: `Example note for ${args.symbol}`,
}),
},
],
details: {
type: 'company-note-card',
symbol: args.symbol,
note: `Example note for ${args.symbol}`,
},
isError: false,
}),
});
api.registerCard({
type: 'company-note-card',
version: 1,
validate: (details) => details && details.type === 'company-note-card',
renderer: (_params, result) => {
if (!result) {
return {
content: [api.helpers.renderCardHeader('pending', 'spinner', 'Company Note')],
isFullBleed: false,
};
}
if (result.isError) {
return {
content: [
api.helpers.renderCardHeader('error', 'error', 'Company Note'),
api.helpers.renderErrorBlock(result.error || 'Failed'),
],
isFullBleed: false,
};
}
return {
content: [
api.helpers.renderCardHeader('complete', 'check', 'Company Note'),
api.helpers.renderMetricRow('Symbol', result.details.symbol),
api.helpers.renderCollapsibleSection('Summary', [result.details.note]),
],
isFullBleed: false,
};
},
});
},
};
Required rules
- Use
module.exports, not ESM exports. - Give the plugin a unique
manifest.id. - Always include
manifest.nameandmanifest.version. - Put side effects inside
activate()or tool execution, not at module top level. - Return structured tool results that Northfox can use.
- Include
details.typewhen you want a custom card renderer. - Prefer small, focused tools over "do everything" tools.
Plugin package format
Northfox plugins are plain CommonJS modules. A plugin exports:
module.exports = {
manifest: { id, name, version, ...optionalFields },
activate(api) {},
};
Required manifest fields
id: stringname: stringversion: string
Optional manifest fields
capabilities: string[]permissions: string[]contributes: objectcompatibility: objectprovenance: object
The plugin API
Inside activate(api), Northfox exposes the methods below.
api.on(eventName, handler)
Register a hook handler.
Parameters:
eventName: 'tool_call' | 'tool_result' | 'context' | 'before_agent_start'handler: Function
Alias:
api.events.on(eventName, handler)
api.registerTool(definition)
Register a tool.
Definition fields:
name: stringrequiredexecute(args, context): Promise<ToolResult> | ToolResultrequireddescription: stringstrongly recommendedparameters: objectrequired when you want Northfox to understand the tool automaticallyagent: objectoptionaltimeoutMs: numberoptionalconcurrencyLimit: numberoptional
Supported agent fields:
expose: booleanlabel: stringparameters: object
Current behavior:
- if
agent.expose !== false, the tool is eligible for Northfox exposure - exposure requires both
descriptionand a valid top-level objectparametersschema - invalid metadata hides the tool and records a diagnostic
- hidden tools still exist in the registry and can still run through explicit host calls
api.registerToolOverride(name, definition)
Register a tool using an explicit override name.
Parameters:
name: stringdefinition: ToolDefinition
This is implemented as registerTool({ ...definition, name }).
api.registerCard(definition)
Register a card renderer for details.type.
Definition fields:
type: stringrequiredrenderer(params, result, isStreaming)requiredversion: numberoptionalvalidate(details)optional
api.registerCardOverride(type, definition)
Replace an existing card renderer intentionally.
Parameters:
type: stringdefinition: CardDefinition
If multiple plugins try to override the same card, the first override wins.
api.registerCommand(name, definition)
Register a named command in the command registry.
Parameters:
name: stringdefinition: object
Important:
- the command registry exists in the plugin manager today
- command conflicts use first-wins semantics
- commands are not yet the main plugin path for chat UX
api.registerFlag() and api.getFlag()
These currently exist as stubs.
Current behavior:
api.registerFlag()does nothingapi.getFlag()returnsundefined
Do not build important plugin behavior around them yet.
api.helpers.renderCardHeader(state, icon, title)
Returns a header block.
Parameters:
state: stringicon: stringtitle: string
api.helpers.renderCollapsibleSection(title, lines, defaultExpanded?)
Returns a section block.
Parameters:
title: stringlines: string[]defaultExpanded?: boolean
api.helpers.renderMetricRow(label, value, change?)
Returns a metric block.
Parameters:
label: stringvalue: string | numberchange?: object
api.helpers.renderErrorBlock(message, detail?)
Returns an error block.
Parameters:
message: stringdetail?: string
Tool definition reference
Northfox expects this tool shape:
type ToolDefinition = {
name: string;
description?: string;
parameters?: object;
agent?: {
expose?: boolean;
label?: string;
parameters?: object;
};
timeoutMs?: number;
concurrencyLimit?: number;
execute: (args: Record<string, any>, context: ToolContext) => Promise<ToolResult> | ToolResult;
};
Tool parameter schema format
Northfox accepts simple JSON-schema-like objects.
Supported field types:
stringnumberbooleanobjectarray
Notes:
- top-level schema must be an
object integeris normalized tonumber- nested objects use
properties - arrays use
items requiredis supported on object schemas- per-property
optional: trueis also recognized
Example:
parameters: {
type: 'object',
properties: {
symbol: { type: 'string', description: 'Ticker symbol' },
limit: { type: 'number', description: 'Number of rows to fetch' },
includeExtended: { type: 'boolean', description: 'Whether to include extended hours data' },
filters: {
type: 'object',
properties: {
sector: { type: 'string' },
},
},
symbols: {
type: 'array',
items: { type: 'string' },
},
},
required: ['symbol'],
}
Tool execution context
Your execute(args, context) function receives a context object with useful runtime information.
The most useful fields are:
message: stringsessionId: stringrecentCards: any[]timestamp: number
Important:
- context can be enriched by
contexthooks before your tool runs - avoid depending on undocumented fields unless you control the environment where the plugin will run
Tool result contract
Your tool should return:
type ToolResult = {
content?: Array<{ type: string; text?: string }>;
details?: Record<string, any>;
isError?: boolean;
error?: string;
displayCard?: boolean;
};
Current behavior:
contentis what Northfox reads nextdetailsis what cards read nextdetails.typeselects the card rendererisErroranderrordrive error handlingdisplayCard === falsesuppresses card rendering
If your tool throws, Northfox converts the failure into a structured error result and records a diagnostic.
Hook reference
Northfox supports these hook names:
tool_calltool_resultcontextbefore_agent_start
tool_call
Incoming payload:
{
toolName: string;
args: Record<string, any>;
context: ToolContext;
blocked: false;
}
Useful return patterns:
- return
{ ...payload, args: rewrittenArgs }to normalize args - return
{ ...payload, blocked: true, reason: '...' }to block execution
tool_result
Incoming payload:
{
toolName: string;
args: Record<string, any>;
result: ToolResult;
context: ToolContext;
}
Useful return pattern:
- return
{ ...payload, result: rewrittenResult }
context
Incoming payload:
ToolContext
Useful return pattern:
- return an object to merge extra fields into tool context
before_agent_start
Incoming payload:
{
toolNames: string[];
}
Current behavior:
- runs after plugin loading
- should be treated as a lifecycle hook, not as a place to do slow work
Card definition reference
The host currently expects this card shape:
type CardDefinition = {
type: string;
version?: number;
validate?: (details: Record<string, any>) => boolean;
renderer: (
params: Record<string, any>,
result: ToolResult | null,
isStreaming: boolean
) => Promise<CardRenderResult> | CardRenderResult;
};
Card render result
Your renderer can return:
type CardRenderResult = {
content?: CardBlock[];
compactContent?: CardBlock[];
expandedContent?: CardBlock[];
isFullBleed?: boolean;
warning?: string | null;
referenceLabel?: string | null;
modalTitle?: string | null;
modalHint?: string | null;
};
The host normalizes and wraps this into its own host-card payload.
Supported card blocks
Northfox currently supports these normalized card block types:
headertextmetricsectiontableerrorjsonamchart-stock-history
Current shapes:
{ type: 'header', state, icon, title }
{ type: 'text', text }
{ type: 'metric', label, value, change? }
{ type: 'section', title, lines: string[] }
{ type: 'table', title, columns: string[], rows: string[][] }
{ type: 'error', lines: string[] }
{ type: 'json', title, text }
{ type: 'amchart-stock-history', symbol, range, height?, points }
The plugin never touches the DOM directly. It only returns structured card data.
Design for Northfox
When you author a plugin, assume the user will ask a natural-language question and Northfox will decide whether your tool is relevant.
That means your tool should:
- have a clear name the model can understand
- accept simple arguments
- perform one clear action
- return factual output, not the final polished answer
Good:
company_info(symbol)earnings_calendar(symbol)analyst_targets(symbol)
Bad:
answer_user_question_about_anything(message)compare_two_companies(symbolA, symbolB, include_everything, write_full_report)
If Northfox needs two company snapshots, it should be able to call the same tool twice. That is better than forcing one giant comparison tool.
Recommended workflow
- Create a dedicated extension directory.
- Open that directory in Codex or Claude Code.
- Give the coding agent the downloadable guide linked at the top of this page.
- Build the plugin as a single
.jsfile or a folder containingindex.js. - Load it into Northfox.
- Open
/pluginsto confirm it appears and is enabled. - Ask Northfox a natural-language question that should use the plugin.
- Check plugin diagnostics if something does not work.
Plugin checks inside the app
Use /plugins to:
- confirm the plugin is visible
- enable or disable the plugin
- enable or disable individual tools
- inspect diagnostics if loading fails
Current limitations
Current constraints for plugin authors:
- there is no dedicated plugin SDK package yet
- there is no host-provided secret manager or database connector API yet
- there is no sandboxed plugin permissions layer yet
registerFlag()andgetFlag()are stubs- command registration exists, but commands are not yet the primary plugin UX path
So today, the stable things to build around are:
manifestactivate(api)registerToolregisterCard- hooks
- the tool result and card result contracts