Building an extension
Package a watcher, a navigable entry type, a server-side data provider, and a declarative dashboard page into one installable Telescope extension — step by step, using the durable workflows surface as the worked example.
An extension bundles everything a feature needs to show up in Telescope — a navigable entry type, dashboard pages, and the server-side queries those pages read — into one object you register with forRoot. The extension ships no UI: it emits a declarative spec, and the fixed dashboard renders it. This recipe builds one end to end, using the shape of @dudousxd/nestjs-durable-telescope (its durableTelescopeExtension()) as the worked example.
See Extensions for the full contract and the panel IR.
1. Define the extension
Author the object with defineTelescopeExtension for inference. Wrap it in a factory so consumers can pass options:
import { defineTelescopeExtension } from '@dudousxd/nestjs-telescope';
export function durableTelescopeExtension() {
return defineTelescopeExtension({
name: 'durable', // used in collision errors + the "<name>." id prefix
});
}The name is the extension's identity. It shows up in collision errors and, by convention, prefixes every dashboard id and provider name you contribute.
2. Add an entry type
entryTypes makes the dashboard nav dynamic. Each entry is { id, label, dot }: id is the backend type filter, label is the nav label, and dot is a Tailwind bg-* class for the nav dot.
entryTypes: () => [
{ id: 'durable', label: 'Workflows', dot: 'bg-amber-400' },
],Now durable entries (recorded by your watcher) are browsable from the nav like any built-in type.
3. Add a data provider
A DataProvider is a named server-side query. It resolves a host service through ctx.moduleRef — never import the host app, ask the Nest container for what you need. Use { strict: false } so it finds the provider in any module:
import type { DurableEngine } from 'your-durable-package';
dataProviders: () => [
{
name: 'durable.state', // "<name>.<provider>"
async resolve(_query, ctx) {
const engine = ctx.moduleRef.get<DurableEngine>('DURABLE_ENGINE', {
strict: false,
});
const dead = await engine.countDead();
return { value: dead }; // stat → { value: number }
},
},
],The return shape depends on the panel kind that binds to it:
| Panel kind | Provider returns |
|---|---|
stat | { value: number } |
timeseries | { rows: ({ label } & Record<string, number>)[] } |
topN | { items: { label, value, id? }[] } |
table | { rows: Record<string, unknown>[] } |
The provider runs behind the dashboard's read authorizer, fetched at GET <path>/api/ext/:ext/data/:provider. Query params arrive as strings — coerce them inside resolve if you need numbers or dates.
4. Add a dashboard spec
A DashboardSpec is a page of declarative panels. Its id must be globally unique and follows "<extName>.<page>" — the UI derives the owning extension from the id prefix to resolve the page's providers, so the prefix is load-bearing, not cosmetic.
Each panel binds its data to a provider via { provider, query? }:
dashboards: () => [
{
id: 'durable.workflows', // "<name>.<page>" — globally unique
label: 'Workflows',
panels: [
{
kind: 'stat',
title: 'Dead now',
format: 'number',
data: { provider: 'durable.state' },
},
{
kind: 'timeseries',
title: 'Throughput',
series: ['completed', 'failed'],
style: 'stacked',
data: { provider: 'durable.timeseries', query: { window: '24h' } },
},
{
kind: 'table',
title: 'Recent failures',
data: { provider: 'durable.recentFailures' },
columns: [
{ key: 'workflow', label: 'Workflow' },
{
key: 'runId',
label: 'Run',
link: { href: '/durable/runs/{runId}' }, // {key} filled from the row
},
],
},
],
},
],A table column's link.href is a URL template — {runId} is replaced with that row's runId. Set external: true to open it in a new tab.
5. Register it
Pass the extension to forRoot. It's additive — extensions coexists with watchers (an extension's own watchers are merged into the same list):
import { TelescopeModule } from '@dudousxd/nestjs-telescope';
import { durableTelescopeExtension } from '@dudousxd/nestjs-durable-telescope';
TelescopeModule.forRoot({
extensions: [durableTelescopeExtension()],
});That single registration adds the Workflows nav entry, the Workflows dashboard page, and wires every panel to its provider.
Keep ids unique
Entry-type ids, dashboard ids, and provider names are global namespaces shared across every installed extension. A duplicate throws at boot, naming both owners. Prefix everything with your extension's name (durable.workflows, durable.timeseries) so two installed extensions never collide.
What you didn't write
No React, no routes, no controller. The extension is a spec plus a few async queries; the fixed UI fetches the data and renders the panels. To go deeper on the panel IR, the data request flow, and the read gate, see Extensions.
Custom watcher
Capture a source Telescope doesn't ship a watcher for — a tiny WebSocket-events watcher built on ctx.record(), and the real instrument(emit, ctx) escape hatch for a bespoke cache, both correlated to the request that caused them.
Capture @nestjs/axios traffic
HttpClientWatcher patches global fetch out of the box, but @nestjs/axios calls bypass it. Wire the axios source to capture HttpService and plain axios instances too — no monkey-patching.