Creating custom plugins
If you need custom API routes or background logic, implement an AppKit plugin. The fastest way is to use the CLI:
npx @databricks/appkit plugin create
For a deeper understanding of the plugin structure, read on.
Basic plugin example
Extend the Plugin class and export with toPlugin():
import { Plugin, toPlugin } from "@databricks/appkit";
import type express from "express";
class MyPlugin extends Plugin {
name = "myPlugin";
// Define resource requirements in the static manifest
static manifest = {
name: "myPlugin",
displayName: "My Plugin",
description: "A custom plugin",
resources: {
required: [
{
type: "secret",
alias: "apiKey",
resourceKey: "apiKey",
description: "API key for external service",
permission: "READ",
fields: {
scope: { env: "MY_SECRET_SCOPE", description: "Secret scope" },
key: { env: "MY_API_KEY", description: "Secret key name" }
}
}
],
optional: []
}
};
async setup() {
// Initialize your plugin
}
myCustomMethod() {
// Some implementation
}
async shutdown() {
// Clean up resources
}
exports() {
// an object with the methods from this plugin to expose
return {
myCustomMethod: this.myCustomMethod
}
}
}
export const myPlugin = toPlugin<typeof MyPlugin, Record<string, never>, "myPlugin">(
MyPlugin,
"myPlugin",
);
Config-dependent resources
The manifest defines resources as either required (always needed) or optional (may be needed).
For resources that become required based on plugin configuration, implement a static
getResourceRequirements(config) method:
interface MyPluginConfig extends BasePluginConfig {
enableCaching?: boolean;
}
class MyPlugin extends Plugin<MyPluginConfig> {
name = "myPlugin";
static manifest = {
name: "myPlugin",
displayName: "My Plugin",
description: "A plugin with optional caching",
resources: {
required: [
{ type: "sql_warehouse", alias: "warehouse", resourceKey: "sqlWarehouse", description: "Query execution", permission: "CAN_USE", fields: { id: { env: "DATABRICKS_WAREHOUSE_ID" } } }
],
optional: [
// Listed as optional in manifest for static analysis
{ type: "database", alias: "cache", resourceKey: "cache", description: "Query result caching (if enabled)", permission: "CAN_CONNECT_AND_CREATE", fields: { instance_name: { env: "DATABRICKS_CACHE_INSTANCE" }, database_name: { env: "DATABRICKS_CACHE_DB" } } }
]
}
};
// Runtime: Convert optional resources to required based on config
static getResourceRequirements(config: MyPluginConfig) {
const resources = [];
if (config.enableCaching) {
// When caching is enabled, Database becomes required
resources.push({
type: "database",
alias: "cache",
resourceKey: "cache",
description: "Query result caching",
permission: "CAN_CONNECT_AND_CREATE",
fields: {
instance_name: { env: "DATABRICKS_CACHE_INSTANCE" },
database_name: { env: "DATABRICKS_CACHE_DB" },
},
required: true // Mark as required at runtime
});
}
return resources;
}
}
This pattern allows:
- Static tools (CLI, docs) to show all possible resources
- Runtime validation to enforce resources based on actual configuration
Key extension points
- Route injection: Implement
injectRoutes()to add custom endpoints usingIAppRouter - Lifecycle hooks: Override
setup(), andshutdown()methods - Shared services:
- Cache management: Access the cache service via
this.cache. SeeCacheConfigfor configuration. - Telemetry: Instrument your plugin with traces and metrics via
this.telemetry. SeeITelemetry.
- Cache management: Access the cache service via
- Execution interceptors: Use
execute()andexecuteStream()withStreamExecutionSettings
Consuming your plugin programmatically
Optionally, you may want to provide a way to consume your plugin programmatically using the AppKit object.
To do that, your plugin needs to implement the exports method, returning an object with the methods you want to expose. From the previous example, the plugin could be consumed as follows:
const AppKit = await createApp({
plugins: [
server({ port: 8000 }),
analytics(),
myPlugin(),
],
});
AppKit.myPlugin.myCustomMethod();
See the Plugin API reference for complete documentation.