Skip to main content

Activation Events

Overview

Activation events control when an extension's JavaScript runtime is started. By default, if an extension has no activationEvents field (or the field is empty), the extension host does not know when to activate it and will log a warning. By declaring activation events in the manifest, you tell the editor exactly under which conditions your extension should be loaded into memory and executed.

This mechanism enables lazy activation -- extensions are not loaded until they are actually needed. A Python linter, for example, does not need to run until the user opens a .py file. A code formatter does not need to load until the user invokes its command. Lazy activation reduces app startup time, lowers memory usage, and keeps the editor responsive when many extensions are installed.

Activation events only apply to extensions that have a main entry point (a JavaScript file to execute). Extensions that only contribute static data such as themes or settings do not need activation events because they have no runtime code to load.

Supported Event Types

The extension system recognizes four activation event types:

EventFormatDescription
onStartupFinished"onStartupFinished"Activate immediately when the app finishes initialization.
*"*"Wildcard. Behaves identically to onStartupFinished -- the extension is always active.
onCommand:{commandId}"onCommand:myext.run"Activate when the specified command is invoked by the user or by another extension.
onFileOpen:{glob}"onFileOpen:*.py"Activate when a file matching the glob pattern is opened in a tab.

onStartupFinished

The extension is activated during the app's bootstrap sequence, right after all installed extensions are read from the database. This is the default event generated by the Create Extension scaffold.

Use this when your extension needs to be available at all times -- for example, a status bar widget that shows system information, or a theme extension that also registers runtime commands.

* (wildcard)

Functionally identical to onStartupFinished. The extension is treated as a startup extension and activated immediately. This exists as a shorthand for extensions that should always be active.

onCommand:{commandId}

The extension remains dormant until the user (or another extension) executes the specified command. The command ID must match exactly -- it is the same ID you declare in contributes.commands.

When the extension host encounters an onCommand: activation event during bootstrap, it registers a placeholder command in the command registry. This placeholder is visible in the command palette and can be invoked by the user. When the placeholder is triggered:

  1. The extension host activates the extension (reads main.js, starts the JS worker).
  2. Once the extension is ready, the command is forwarded for execution.

This means the user experiences a brief delay on the very first invocation while the extension loads, but subsequent commands execute instantly.

You can declare multiple onCommand: events to activate on any of several commands:

"activationEvents": [
"onCommand:myext.formatCode",
"onCommand:myext.showPanel"
]

onFileOpen:{glob}

The extension remains dormant until the user opens a file whose name matches the glob pattern. The matching is performed against the file name (not the full path) when a TabOpened event is received.

The glob syntax supports:

PatternMeaning
*Matches any characters except /
**Matches any characters including / (recursive)
.Matched literally (automatically escaped)

Examples:

GlobMatches
*.pymain.py, test.py
*.{js,ts}Not supported -- use separate events
*.test.jsapp.test.js, utils.test.js
**/*.mdREADME.md, docs/guide.md

To match multiple file types, declare multiple onFileOpen: events:

"activationEvents": [
"onFileOpen:*.py",
"onFileOpen:*.pyw"
]

Manifest Declaration

The activationEvents field is a string array at the root level of manifest.json:

{
"id": "my-extension",
"name": "My Extension",
"version": "1.0.0",
"main": "dist/main.js",
"activationEvents": ["onStartupFinished"],
"engineVersion": ">=0.1.0",
"contributes": {
"commands": [
{
"id": "my-extension.helloWorld",
"label": "Hello World",
"category": "My Extension"
}
]
}
}

Startup Extension

An extension that should always be running:

"activationEvents": ["onStartupFinished"]

Command-Activated Extension

An extension that only loads when the user invokes a specific command:

"activationEvents": ["onCommand:my-extension.runAnalysis"]

The command my-extension.runAnalysis must also be declared in contributes.commands so it appears in the command palette.

File-Activated Extension

An extension that loads when a Python file is opened:

"activationEvents": ["onFileOpen:*.py"]

Mixed Activation

You can combine multiple event types. The extension activates on whichever event fires first:

"activationEvents": [
"onCommand:python-tools.lint",
"onCommand:python-tools.format",
"onFileOpen:*.py",
"onFileOpen:*.pyw"
]

In this example, the extension activates if the user opens any Python file or manually runs one of the two commands -- whichever happens first.

How Lazy Activation Works

At startup, the app reads each extension's manifest.json and registers its static contributions (commands, themes, settings, and other contribution points). These contributions are available in the editor immediately -- for example, commands appear in the command palette and themes can be selected -- even though the extension's runtime has not started yet.

The extension itself remains dormant until one of its declared activation events fires. For onStartupFinished or *, that happens immediately during startup. For onCommand: or onFileOpen: events, the extension stays inactive until the user invokes the matching command or opens a matching file.

When a matching event occurs, the app loads the extension's main.js file, creates an isolated JS worker, and runs the extension code. The onLoad lifecycle callback is called, giving the extension a chance to initialize.

Once activated, the extension stays active until the app closes, or the extension is disabled, uninstalled, or crashes.

JavaScript Lifecycle Hooks

When an extension is activated (whether at startup or lazily), the JS runtime calls the onLoad callback. When the extension is deactivated, it calls onDispose. These hooks are your entry points for setup and teardown:

import editor from "@srcnexus/ext-sdk";

editor.onLoad(() => {
// Extension has been activated.
// Register commands, set up event listeners, initialize state.
editor.commands.registerCommand("myext.greet", () => {
editor.window.showToast("Hello from my extension!");
});
});

editor.onDispose(() => {
// Extension is being deactivated.
// Clean up resources, close connections, save state.
});

The onLoad callback runs regardless of which activation event triggered the activation. Your code does not need to distinguish between startup activation and lazy activation -- the SDK handles this transparently.

Async onLoad

The onLoad callback can return a Promise. The extension host waits for the Promise to resolve (with a 30-second timeout) before marking the extension as fully ready:

editor.onLoad(async () => {
const config = await editor.workspace.getConfiguration("myext.apiEndpoint");
// Use config to set up the extension...

editor.commands.registerCommand("myext.run", async () => {
// Command handler
});
});

Examples

Python Linter (File-Activated)

An extension that only loads when a Python file is opened:

manifest.json:

{
"id": "python-linter",
"name": "Python Linter",
"version": "1.0.0",
"main": "dist/main.js",
"activationEvents": ["onFileOpen:*.py", "onFileOpen:*.pyw"],
"engineVersion": ">=0.1.0",
"contributes": {
"commands": [
{
"id": "python-linter.lint",
"label": "Lint Current File",
"category": "Python Linter"
}
]
}
}

src/main.ts:

import editor from "@srcnexus/ext-sdk";

editor.onLoad(() => {
editor.commands.registerCommand("python-linter.lint", async () => {
const file = await editor.workspace.getActiveFile();
if (!file) {
editor.window.showToast("No active file");
return;
}
editor.window.showToast(`Linting ${file.name}...`);
// Perform linting...
});

// Listen for file saves to auto-lint
editor.events.onFileSave((event) => {
if (event.name.endsWith(".py")) {
editor.commands.execute("python-linter.lint");
}
});
});

Snippet Tool (Command-Activated)

An extension that only loads when the user explicitly requests it:

manifest.json:

{
"id": "snippet-tool",
"name": "Snippet Tool",
"version": "1.0.0",
"main": "dist/main.js",
"activationEvents": ["onCommand:snippet-tool.insert"],
"engineVersion": ">=0.1.0",
"contributes": {
"commands": [
{
"id": "snippet-tool.insert",
"label": "Insert Snippet",
"description": "Pick and insert a code snippet",
"category": "Snippets"
}
]
}
}

src/main.ts:

import editor from "@srcnexus/ext-sdk";

editor.onLoad(() => {
editor.commands.registerCommand("snippet-tool.insert", async () => {
const snippet = await editor.window.showPicker({
title: "Insert Snippet",
placeholder: "Search snippets...",
items: [
{ label: "Console Log", description: "console.log()", detail: "log" },
{ label: "Arrow Function", description: "() => {}", detail: "arrow" },
{ label: "Try/Catch", description: "try { } catch { }", detail: "trycatch" },
],
});
if (snippet) {
editor.window.showToast(`Inserted: ${snippet.label}`);
}
});
});

Always-On Extension (Startup)

An extension that provides a persistent status bar indicator:

manifest.json:

{
"id": "word-counter",
"name": "Word Counter",
"version": "1.0.0",
"main": "dist/main.js",
"activationEvents": ["onStartupFinished"],
"engineVersion": ">=0.1.0",
"contributes": {
"statusBarItems": [
{
"id": "word-counter.status",
"label": "Words: 0",
"commandId": "word-counter.count",
"alignment": "right",
"priority": 50
}
]
}
}

src/main.ts:

import editor from "@srcnexus/ext-sdk";

editor.onLoad(() => {
async function updateCount() {
const file = await editor.workspace.getActiveFile();
if (file) {
const content = await editor.workspace.fs.read(file.uri);
const words = content.split(/\s+/).filter(Boolean).length;
editor.window.setStatusBarText("word-counter.status", `Words: ${words}`);
}
}

editor.events.onActiveEditorChange(() => updateCount());
editor.events.onFileSave(() => updateCount());
updateCount();
});

Summary

ScenarioRecommended EventWhy
Status bar widget, background taskonStartupFinishedMust be active at all times
Code formatter invoked via command paletteonCommand:{id}Only needed when the user requests it
Language-specific tooling (linter, snippets)onFileOpen:*.{ext}Only needed when relevant files are open
Multi-purpose extensionMultiple events combinedActivates on whichever trigger comes first
Theme-only extension (no JS runtime)Not neededNo main entry point, nothing to activate