Skip to main content

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.name and manifest.version.
  • Put side effects inside activate() or tool execution, not at module top level.
  • Return structured tool results that Northfox can use.
  • Include details.type when 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: string
  • name: string
  • version: string

Optional manifest fields

  • capabilities: string[]
  • permissions: string[]
  • contributes: object
  • compatibility: object
  • provenance: 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: string required
  • execute(args, context): Promise<ToolResult> | ToolResult required
  • description: string strongly recommended
  • parameters: object required when you want Northfox to understand the tool automatically
  • agent: object optional
  • timeoutMs: number optional
  • concurrencyLimit: number optional

Supported agent fields:

  • expose: boolean
  • label: string
  • parameters: object

Current behavior:

  • if agent.expose !== false, the tool is eligible for Northfox exposure
  • exposure requires both description and a valid top-level object parameters schema
  • 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: string
  • definition: ToolDefinition

This is implemented as registerTool({ ...definition, name }).

api.registerCard(definition)

Register a card renderer for details.type.

Definition fields:

  • type: string required
  • renderer(params, result, isStreaming) required
  • version: number optional
  • validate(details) optional

api.registerCardOverride(type, definition)

Replace an existing card renderer intentionally.

Parameters:

  • type: string
  • definition: 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: string
  • definition: 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 nothing
  • api.getFlag() returns undefined

Do not build important plugin behavior around them yet.

api.helpers.renderCardHeader(state, icon, title)

Returns a header block.

Parameters:

  • state: string
  • icon: string
  • title: string

api.helpers.renderCollapsibleSection(title, lines, defaultExpanded?)

Returns a section block.

Parameters:

  • title: string
  • lines: string[]
  • defaultExpanded?: boolean

api.helpers.renderMetricRow(label, value, change?)

Returns a metric block.

Parameters:

  • label: string
  • value: string | number
  • change?: object

api.helpers.renderErrorBlock(message, detail?)

Returns an error block.

Parameters:

  • message: string
  • detail?: 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:

  • string
  • number
  • boolean
  • object
  • array

Notes:

  • top-level schema must be an object
  • integer is normalized to number
  • nested objects use properties
  • arrays use items
  • required is supported on object schemas
  • per-property optional: true is 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: string
  • sessionId: string
  • recentCards: any[]
  • timestamp: number

Important:

  • context can be enriched by context hooks 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:

  • content is what Northfox reads next
  • details is what cards read next
  • details.type selects the card renderer
  • isError and error drive error handling
  • displayCard === false suppresses 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_call
  • tool_result
  • context
  • before_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:

  • header
  • text
  • metric
  • section
  • table
  • error
  • json
  • amchart-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.

  1. Create a dedicated extension directory.
  2. Open that directory in Codex or Claude Code.
  3. Give the coding agent the downloadable guide linked at the top of this page.
  4. Build the plugin as a single .js file or a folder containing index.js.
  5. Load it into Northfox.
  6. Open /plugins to confirm it appears and is enabled.
  7. Ask Northfox a natural-language question that should use the plugin.
  8. 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() and getFlag() are stubs
  • command registration exists, but commands are not yet the primary plugin UX path

So today, the stable things to build around are:

  • manifest
  • activate(api)
  • registerTool
  • registerCard
  • hooks
  • the tool result and card result contracts