# Scoped extensions Use `installGraphExtension` for resource-owning behaviors and plugins. Pure filters, layouts and themes retain their existing contracts. ```ts import { installGraphExtension } from '@graphora/engine' const counter = installGraphExtension(engine, { id: 'render-counter', category: 'plugin', activate({ engine, signal, onCleanup }) { onCleanup(engine.onLifecycle('render:after', () => updateCounter())) window.addEventListener('online', refreshData, { signal }) const timer = setInterval(refreshData, 30_000) onCleanup(() => clearInterval(timer)) }, dispose() { // Release any instance-level resources retained while disabled. } }) counter.disable() counter.enable() counter.dispose() ``` Installation enables by default; pass `{ enabled: false }` as the third argument to start disabled. Each enable period calls `activate` with a fresh AbortSignal and cleanup scope. Returning a cleanup function is equivalent to registering it. Disable aborts the signal and executes registered cleanups in reverse order; dispose also runs terminal instance cleanup and releases installation ownership. Engine destruction disposes enabled and disabled extensions automatically. Repeated enable, disable and dispose calls are safe. Enable after disposal throws. IDs are unique per engine and an extension object cannot be installed twice at the same time. Failed activation releases its resources and rolls back installation. Cleanup errors do not prevent other registered resources from being released. Reentrant disable/dispose closes the old scope immediately; a cleanup returned later by that activation executes immediately and cannot join a newer scope. The context exposes the public engine, not private engine or renderer fields. Register external listeners, subscriptions and timers with `onCleanup` or the signal. Unregistered external resources remain the extension author's responsibility. Callbacks are synchronous. During engine destruction cleanup should release resources rather than invoke engine APIs requiring an active instance. Unmount is not destruction: plugins can remain active across remounts. DOM tied to a mounted renderer belongs in the layer lifecycle. Try the counter in the [basic example](../../vanilla/basic/index.html). Use [custom layers](./custom-layers.md) for renderer-owned Canvas drawing and HTML mount resources. Neither API introduces a global registry.