Example cookbook
Thirteen supplied packages demonstrate the public contract. Commentary is written for this guide; code excerpts are copied verbatim from the named files. Some original strings are French. Regenerate excerpts with node scripts/gen-examples.mjs.
Obtain the example tree with your developer distribution. There is no public repository link. Use a licensed test instance and follow Testing before installing third-party code.
A movable card
Source: examples/plugin-widget. Manifest ID: example-widget. Minimum API: 1.
Use this for a browser-only card. React comes from the platform; the returned slot disposer releases the card when the plugin unloads.
Permissions: none. Read the related guide.
timon plugin add ./examples/plugin-widget installs this package on the target instance.
From examples/plugin-widget/lib/client.js, line 7 (excerpt):
export function apply(ctx) {
function ExampleWidget() {
return React.createElement(
'section',
{ style: { margin: 12, padding: 16, border: '1px solid currentColor', borderRadius: 12 } },
React.createElement('strong', null, 'Example widget'),
React.createElement('p', null, `plugin ${ctx.id}`),
)
}
return ctx.slots.register({ slot: 'cockpit.widget' }, ExampleWidget)
}
A route and two UI insertions
Source: examples/plugin-hello. Manifest ID: plugin-hello. Minimum API: 1.
The host serves a greeting from a setting. The browser calls its own plugin route with ctx.api and registers navigation plus a widget. The original French strings are preserved in these verbatim source excerpts.
Permissions: settings. Read the related guide.
timon plugin add ./examples/plugin-hello installs this package on the target instance.
From examples/plugin-hello/lib/index.js, line 1 (excerpt):
export const name = 'plugin-hello'
export const inject = ['settings']
export function apply(ctx) {
const router = ctx.router()
router.get('/hello', (_request, response) => {
response.json({
plugin: ctx.id,
greeting: ctx.settings.get('greeting') ?? 'Bonjour depuis un plugin',
})
})
}
From examples/plugin-hello/lib/client.js, line 15 (excerpt):
export function apply(ctx) {
function HelloWidget() {
const [message, setMessage] = React.useState('Chargement…')
React.useEffect(() => {
let active = true
ctx.api('hello')
.then((response) => response.json())
.then((body) => { if (active) setMessage(String(body.greeting)) })
.catch(() => { if (active) setMessage('Plugin indisponible') })
return () => { active = false }
}, [])
return React.createElement(
'section',
{ style: { margin: 12, padding: 16, border: '1px solid currentColor', borderRadius: 12 } },
React.createElement('strong', null, 'Plugin Hello'),
React.createElement('p', null, message),
)
}
const disposeNav = ctx.slots.register({ slot: 'nav.item' }, HelloNav)
const disposeWidget = ctx.slots.register({ slot: 'cockpit.widget' }, HelloWidget)
return () => {
disposeWidget()
disposeNav()
}
}
A page with a child slot
Source: examples/plugin-page. Manifest ID: example-page. Minimum API: 1.
Open /example after installation. The route key and manifest route agree; the child slot is rendered by the page, not merely declared.
Permissions: none. Read the related guide.
timon plugin add ./examples/plugin-page installs this package on the target instance.
From examples/plugin-page/lib/client.js, line 9 (excerpt):
export function apply(ctx) {
function ExamplePage() {
return React.createElement(
'main',
{ style: { padding: 24 } },
React.createElement('h1', null, 'Example page'),
React.createElement(Slot, { name: 'example.widget' }),
)
}
return ctx.slots.register(
{ slot: 'page', key: '/example', children: { 'example.widget': 'list' } },
ExamplePage,
)
}
A card and an in-app overlay
Source: examples/plugin-overlay. Manifest ID: example-overlay. Minimum API: 1.
Install once to obtain both surfaces. Each registration gets a disposer, and unloading releases both. The overlay positions itself inside the browser window.
Permissions: none. Read the related guide.
timon plugin add ./examples/plugin-overlay installs this package on the target instance.
From examples/plugin-overlay/lib/client.js, line 7 (excerpt):
function Body() {
return React.createElement('strong', null, 'Example overlay')
}
export function apply(ctx) {
const disposeWidget = ctx.slots.register(
{ slot: 'cockpit.widget' },
function OverlayCard() {
return React.createElement(
'section',
{ style: { margin: 12, padding: 16, border: '1px solid currentColor', borderRadius: 12 } },
React.createElement(Body),
)
},
)
const disposeOverlay = ctx.slots.register(
{ slot: 'overlay' },
function OverlayMascot() {
return React.createElement(
'div',
{
style: {
position: 'fixed',
right: 16,
bottom: 16,
zIndex: 40,
padding: 12,
borderRadius: 16,
border: '1px solid currentColor',
},
},
React.createElement(Body),
)
},
)
return () => {
disposeOverlay()
disposeWidget()
}
}
A token palette
Source: examples/plugin-theme. Manifest ID: example-theme. Minimum API: 1.
Select this installed theme in Appearance. TOKENS contains the dark and light palettes; this excerpt shows how a React-owned style is mounted and removed. The old template comment about missing tokens predates the runtime theme floor; see the theme guide for partial palettes.
Permissions: none. Read the related guide.
timon plugin add ./examples/plugin-theme installs this package on the target instance.
From examples/plugin-theme/lib/client.js, line 165 (excerpt):
export function apply(ctx) {
// No `fallback: true`: the product theme owns that role. Claiming the slot
// is what makes the palette visible, and disposing restores the product's.
return ctx.slots.register({ slot: 'theme' }, function ExampleTheme() {
return React.createElement('style', {
id: 'example-theme-tokens',
dangerouslySetInnerHTML: { __html: TOKENS },
})
})
}
A minimal root layout
Source: examples/plugin-blank-layout. Manifest ID: blank-layout. Minimum API: 1.
This replaces the chrome with the reserved Next page tree. It declares only page, so other layout-owned regions are absent. Test it on a separate instance and retain a way to restore the product layout.
Permissions: none. Read the related guide.
timon plugin add ./examples/plugin-blank-layout installs this package on the target instance.
From examples/plugin-blank-layout/client.js, line 4 (excerpt):
export function apply(ctx) {
return ctx.slots.register(
// `page` is keyed by route: a layout paints the Next tree's reserved
// key, never the whole slot — every plugin page would stack up.
{ slot: 'root', children: { page: 'keyed' } },
function BlankLayout() { return React.createElement(SlotKeyed, { name: 'page', entryKey: 'next-router' }); },
);
}
Native and custom settings
Source: examples/plugin-settings. Manifest ID: example-settings. Minimum API: 1.
The first registration adds a row in Missions. The second supplies the plugin category and opens a child slot for other extensions. Both registrations are disposed together.
Permissions: none. Read the related guide.
timon plugin add ./examples/plugin-settings installs this package on the target instance.
From examples/plugin-settings/lib/client.js, line 35 (excerpt):
const disposeGraft = ctx.slots.register({ slot: 'settings.missions' }, MissionsGraft)
const disposeSection = ctx.slots.register(
{ slot: 'settings.section', children: { 'settings.acme': 'list' } },
OwnCategory,
)
return () => {
disposeSection()
disposeGraft()
}
}
Install a page and widget together
Source: examples/plugin-bundle. Manifest ID: example-bundle. Minimum API: 1.
The manifest contains two child IDs, while npm dependencies resolve their packages. The sibling file paths are for the local example tree, not a published registry package. Its host apply intentionally does nothing.
Permissions: none. Read the related guide.
timon plugin add ./examples/plugin-bundle installs this package on the target instance.
From examples/plugin-bundle/package.json, line 1:
{
"name": "@timon-ai/plugin-bundle",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "lib/index.js",
"dependencies": {
"@timon-ai/plugin-page": "file:../plugin-page",
"@timon-ai/plugin-widget": "file:../plugin-widget"
},
"timon": {
"id": "example-bundle",
"apiVersion": 1,
"kind": "bundle",
"label": "Tableau exemple",
"description": "Installe la page d'exemple et son widget. Chaque enfant reste un plugin à part.",
"contains": ["example-page", "example-widget"],
"permissions": [],
"settings": [],
"license": { "required": false }
}
}
From examples/plugin-bundle/lib/index.js, line 2 (excerpt):
export const name = 'example-bundle'
export const inject = []
export function apply() {}
A provider pack without code
Source: examples/plugin-provider-pack. Manifest ID: provider-pack-example. Minimum API: 2.
Two declarative profiles reuse installed CLI dialects. Replace the example endpoints and model IDs, then enter the key in the plugin settings. The manifest contains no main and no JavaScript is loaded. Signature and licence rules still apply.
Permissions: backends:register. Read the related guide.
timon plugin add ./examples/plugin-provider-pack installs this package on the target instance.
From examples/plugin-provider-pack/package.json, line 1:
{
"name": "@timon-ai/plugin-provider-pack",
"version": "0.1.0",
"private": true,
"type": "module",
"timon": {
"id": "provider-pack-example",
"apiVersion": 2,
"kind": "behavior",
"label": "Example provider pack",
"description": "Two declarative providers using existing CLI dialects.",
"author": "Timon",
"permissions": [
"backends:register"
],
"settings": [
{
"key": "apiKey",
"type": "secret",
"label": "Provider API key"
}
],
"backends": [
{
"id": "pack-anthropic",
"dialect": "claude",
"label": "Example Anthropic endpoint",
"env": {
"ANTHROPIC_BASE_URL": "https://anthropic.example.test",
"ANTHROPIC_AUTH_TOKEN": {
"setting": "apiKey"
}
},
"defaultModel": "example-model",
"models": [
{
"slug": "example-model",
"label": "Example model"
}
]
},
{
"id": "pack-responses",
"dialect": "codex",
"label": "Example Responses endpoint",
"env": {
"EXAMPLE_API_KEY": {
"setting": "apiKey"
}
},
"providerConfig": {
"id": "example",
"name": "Example Responses",
"baseUrl": "https://responses.example.test/v1",
"envKey": "EXAMPLE_API_KEY",
"wireApi": "responses"
},
"defaultModel": "example-model",
"models": [
{
"slug": "example-model",
"label": "Example model"
}
]
}
],
"license": {
"required": false
}
}
}
Profiles and dialects
Source: examples/plugin-provider. Manifest ID: provider-example. Minimum API: 2.
This demonstration registers its profile unconditionally: the env function throws while the secret setting is empty, which keeps the backend out of the menus and out of dispatch until a key is saved. It also includes an echo dialect describing a fictitious JSON-speaking executable, not the operating system echo command. It is a protocol template, not a working AI backend. Provider endpoints and model IDs below are literal example data, not current catalogue recommendations.
Permissions: backends:register, settings. Read the related guide.
timon plugin add ./examples/plugin-provider installs this package on the target instance.
From examples/plugin-provider/lib/index.js, line 15 (excerpt):
ctx.backends.registerProfile({
id: 'ollama-cloud-example',
dialect: 'codex',
label: 'Ollama Cloud (example)',
vendor: 'Ollama Cloud',
brand: 'ollama',
custom: true,
// Resolve on every mission launch, including short-lived token refresh.
// The daemon never relays the error message; keep credentials out of it.
env: () => {
const apiKey = ctx.settings.get('apiKey')
if (!apiKey) throw new Error('apiKey setting is empty')
return { OLLAMA_API_KEY: apiKey }
},
providerConfig: {
id: 'ollama-cloud-example',
name: 'Ollama Cloud (example)',
baseUrl: 'https://ollama.com/v1',
envKey: 'OLLAMA_API_KEY',
wireApi: 'responses',
},
defaultModel: 'gpt-oss:120b',
models: [
{ slug: 'gpt-oss:120b', label: 'gpt-oss:120b', contextWindow: 128_000 },
{ slug: 'gpt-oss:20b', label: 'gpt-oss:20b', contextWindow: 32_000 },
],
})
From examples/plugin-provider/lib/echo-dialect.js, line 27 (excerpt):
async buildSpawn(opts, profile) {
const args = ['--model', opts.cfg.model ?? 'echo-1', '--system', opts.systemPrompt];
if (opts.resumeSession) args.push('--resume', opts.resumeSession);
args.push(opts.message);
return {
cmd: profile.cmd,
args,
// Use the resolved profile environment, never process.env.
env: { ...profile.env },
cwd: opts.cfg.cwd,
initialStdin: null,
};
},
Usage without fabricated zeroes
Source: examples/plugin-quota-monitor. Manifest ID: quota-monitor. Minimum API: 2.
The monitor combines host storage/settings with topbar, widget and settings surfaces. This function computes risk only from known finite measurements. Unknown usage remains null. The full template also preserves the last known reading during transient failures.
Permissions: usage:read, storage, settings. Read the related guide.
timon plugin add ./examples/plugin-quota-monitor installs this package on the target instance.
From examples/plugin-quota-monitor/lib/index.js, line 27 (excerpt):
export function usageRisk(usage) {
if (!usage) return null
const values = Object.values(usage.windows ?? {})
.filter((window) => window && Number.isFinite(window.utilization))
.map((window) => window.utilization)
if (usage.extra?.is_enabled && Number.isFinite(usage.extra.utilization)) values.push(usage.extra.utilization)
return values.length ? Math.max(...values) : null
}
Owner-only live updates
Source: examples/plugin-realtime. Manifest ID: example-realtime. Minimum API: 4.
API 4. An authenticated route increments a per-account counter and publishes changed to that account only. The widget subscribes, groups bursts with a 100 ms debounce, reloads through its own API and unsubscribes on unmount. Open two tabs as the same account to see both update; another account receives nothing. The counter is in memory and resets when the plugin unloads.
Permissions: realtime:publish. Read the related guide.
timon plugin add ./examples/plugin-realtime installs this package on the target instance.
From examples/plugin-realtime/lib/index.js, line 15 (excerpt):
router.post('/increment', (_request, response) => {
const ownerUserId = ctx.user().id
const count = (counts.get(ownerUserId) ?? 0) + 1
counts.set(ownerUserId, count)
ctx.realtime.publish('changed', { count }, { ownerUserId })
response.json({ count })
})
From examples/plugin-realtime/lib/client.js, line 22 (excerpt):
const unsubscribe = clientCtx.realtime.subscribe('changed', () => {
clearTimeout(timer)
timer = setTimeout(() => { void reload() }, 100)
})
void reload()
return () => { active = false; clearTimeout(timer); unsubscribe() }
}, [])
Business events, workflows and messaging
Source: examples/plugin-webhook-bridge. Manifest ID: webhook-bridge. Minimum API: 3.
API 3 registers a business event, an uppercase executor, a signal trigger and an authenticated transport. The receive handler verifies the timestamp and HMAC before accepting an external ID. The full send handler uses a per-fragment idempotency key and checkpoint. Configure connections with vault secret names and operator policy before sending traffic; no AI provider is needed for the uppercase executor.
Permissions: transports:register, connections:read, connections:write, missions:spawn, missions:read, secrets:read, http:outbound, events:register, events:publish, workflows:triggers:register, workflows:executors:register. Read the related guide.
timon plugin add ./examples/plugin-webhook-bridge installs this package on the target instance.
From examples/plugin-webhook-bridge/lib/index.js, line 33 (excerpt):
ctx.businessEvents.register({ name: 'message.received', version: 1, payloadSchema: textSchema });
ctx.workflows.registerExecutor({
name: 'uppercase', version: 1, configSchema: emptySchema, outputPorts: ['out'],
execute: ({ input }) => ({ output: { text: textSchema.parse(input).text.toUpperCase() }, port: 'out' }),
});
ctx.workflows.registerTrigger({
name: 'signal', version: 1, configSchema: topicSchema,
start(context) { signals.add(context); return () => { signals.delete(context); }; },
});
From examples/plugin-webhook-bridge/lib/index.js, line 56 (excerpt):
async receive(context, request) {
const stamp = request.headers['x-timon-timestamp'] ?? '';
const signature = request.headers['x-timon-signature'] ?? '';
if (!/^[0-9]{10}$/.test(stamp) || Math.abs(Date.now() / 1000 - Number(stamp)) > 300 || !/^[a-f0-9]{64}$/.test(signature)) {
throw new Error('Invalid signature envelope');
}
const signing = await context.secret('signing');
if (!signing || signing.length < 24) throw new Error('Signing secret unavailable');
const expected = createHmac('sha256', signing).update(`${stamp}.`).update(request.body).digest();
if (!timingSafeEqual(expected, Buffer.from(signature, 'hex'))) throw new Error('Invalid signature');
const message = object(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(request.body)));
if (message.room !== context.connection.config.room) throw new Error('Room not allowed');
// Inbound URLs, credentials and identity fields never select the callback.
return { externalId: text(message.id), text: text(message.text, 64_000), replyContext: { room: text(message.room) } };
},