Extension Lifecycle Hooks
Deep dive into the Extension trait methods: when each hook is called and how to implement metadata, schemas, routing, jobs, and providers.
On this page
The Extension trait is the contract between your code and the SystemPrompt runtime. It defines methods that the runtime calls at specific points during startup, request handling, and job execution. Each method has a sensible default that returns empty or no-op behavior, so you implement only what you need.
This reference documents every hook in the Extension trait, when it is called, and how to implement it effectively.
Overview
Extensions follow a declarative pattern. Instead of imperative startup code, you return data structures that describe your capabilities. The runtime interprets these declarations and wires everything together. This approach enables deterministic startup, where the same extension with the same configuration always produces the same behavior.
The Extension trait groups methods by concern:
| Category | Methods | Purpose |
|---|---|---|
| Metadata | metadata(), config_prefix(), priority(), dependencies() |
Identity and ordering |
| Database | schemas(), migration_weight() |
Table definitions |
| Routing | router(), router_config() |
HTTP endpoints |
| Providers | page_data_providers(), content_data_providers(), component_renderers(), page_prerenderers() |
Template integration |
| Jobs | jobs() |
Background tasks |
| Assets | declares_assets(), required_assets() |
CSS and JavaScript |
All methods have default implementations that return empty vectors or None. Override only the hooks your extension needs.
Metadata Hooks
Metadata hooks establish your extension's identity and control initialization order.
metadata()
Returns the extension's identifier, human-readable name, and version. The runtime logs this information at startup and uses the ID for configuration namespacing.
fn metadata(&self) -> ExtensionMetadata {
ExtensionMetadata {
id: "web",
name: "Web Content & Navigation",
version: env!("CARGO_PKG_VERSION"),
}
}
The ID should be a short, lowercase string suitable for prefixing configuration keys. Use the crate's CARGO_PKG_VERSION for automatic versioning.
config_prefix()
Returns the prefix for environment variables and configuration keys. When you return Some("web"), the runtime looks for WEB_* environment variables and passes them to your extension.
fn config_prefix(&self) -> Option<&str> {
Some("web")
}
Return None if your extension has no configuration.
priority()
Controls the order in which extensions initialize. Higher values run earlier. Use this when your extension provides services that other extensions depend on.
fn priority(&self) -> u32 {
100 // High priority, initializes early
}
The default priority is 50. Core extensions typically use 100 or higher.
dependencies()
Returns a list of extension IDs that must initialize before this one. The runtime verifies dependencies are present and orders initialization accordingly.
fn dependencies(&self) -> Vec<&str> {
vec!["auth", "database"]
}
Most extensions do not need explicit dependencies because the priority system handles ordering.
Database Hooks
Database hooks define the tables and migrations your extension requires.
schemas()
Returns SQL schema definitions that the runtime executes during database initialization. Each schema has a name and SQL content. Schemas execute in migration_weight() order across all extensions.
fn schemas(&self) -> Vec<SchemaDefinition> {
vec![
SchemaDefinition::inline("markdown_content", SCHEMA_MARKDOWN_CONTENT),
SchemaDefinition::inline("campaign_links", SCHEMA_CAMPAIGN_LINKS),
]
}
The web extension uses include_str! to embed SQL files at compile time:
pub const SCHEMA_MARKDOWN_CONTENT: &str = include_str!("../schema/001_markdown_content.sql");
Schemas should be idempotent, using CREATE TABLE IF NOT EXISTS and similar patterns. The runtime may execute them multiple times across restarts.
migration_weight()
Controls schema execution order relative to other extensions. Higher weights execute earlier. Use this when your tables have foreign key dependencies on other extensions' tables.
fn migration_weight(&self) -> u32 {
100 // Run early
}
Routing Hooks
Routing hooks expose HTTP endpoints from your extension.
router()
Returns an Axum router with your extension's HTTP handlers. The runtime mounts this router at the path returned by base_path() or configured via router_config().
fn router(&self, ctx: &dyn ExtensionContext) -> Option<ExtensionRouter> {
let db_handle = ctx.database();
let db = db_handle.as_any().downcast_ref::<Database>()?;
let pool = db.pool()?;
let router = api::router(pool, self.validated_config.clone());
Some(ExtensionRouter::new(router, "/api/v1/links"))
}
The ExtensionContext provides access to shared resources like the database. You downcast to the concrete type to access connection pools. Return None if your extension has no HTTP routes.
router_config()
Returns configuration for how the router is mounted. Override this to customize path prefixes, middleware, or CORS settings.
Provider Hooks
Provider hooks integrate your extension with the template rendering system. Providers run during page generation to inject data, render components, or generate static pages.
page_data_providers()
Returns providers that inject data into page templates. Each provider receives page metadata and returns JSON that becomes available in templates.
fn page_data_providers(&self) -> Vec<Arc<dyn PageDataProvider>> {
vec![
Arc::new(NavigationPageDataProvider::new(nav_config)),
Arc::new(HomepagePageDataProvider::new(homepage_config)),
Arc::new(DocsPageDataProvider::new()),
]
}
The NavigationPageDataProvider reads navigation.yaml and provides menu structures. The DocsPageDataProvider extracts after_reading_this and related links from frontmatter.
content_data_providers()
Returns providers that enrich content during ingestion. They process frontmatter and body content before storage.
fn content_data_providers(&self) -> Vec<Arc<dyn ContentDataProvider>> {
vec![Arc::new(DocsContentDataProvider::new())]
}
component_renderers()
Returns renderers for template partials. Each renderer produces HTML for a named component: headers, footers, scripts, or custom elements.
fn component_renderers(&self) -> Vec<Arc<dyn ComponentRenderer>> {
vec![
Arc::new(HeadAssetsPartialRenderer),
Arc::new(HeaderPartialRenderer),
Arc::new(FooterPartialRenderer),
Arc::new(ScriptsPartialRenderer),
]
}
Component renderers have priorities that control their execution order. Critical components like head assets run first.
page_prerenderers()
Returns prerenderers that generate static HTML pages. These run during content publishing to create pages that do not come from markdown files.
fn page_prerenderers(&self) -> Vec<Arc<dyn PagePrerenderer>> {
config.pages
.iter()
.map(|page| Arc::new(FeaturePagePrerenderer::new(page.clone())) as Arc<dyn PagePrerenderer>)
.collect()
}
The web extension uses prerenderers to generate feature pages from YAML configuration.
Job Hooks
Job hooks register background tasks with the scheduler.
jobs()
Returns job implementations that the scheduler can execute. Jobs run on cron schedules or on-demand via CLI.
fn jobs(&self) -> Vec<Arc<dyn Job>> {
vec![
Arc::new(ContentIngestionJob),
Arc::new(CopyExtensionAssetsJob),
Arc::new(PublishPipelineJob),
]
}
Each job implements the Job trait:
#[async_trait]
impl Job for ContentIngestionJob {
fn name(&self) -> &'static str {
"blog_content_ingestion"
}
fn description(&self) -> &'static str {
"Ingests markdown content from configured directories"
}
fn schedule(&self) -> &'static str {
"0 0 * * * *" // Every hour
}
async fn execute(&self, ctx: &JobContext) -> Result<JobResult> {
// Implementation
}
}
The JobContext provides database access and configuration. Return JobResult with statistics and duration.
Asset Hooks
Asset hooks declare CSS and JavaScript files your extension needs.
declares_assets()
Returns true if your extension has assets to publish. When true, the runtime calls required_assets() during build.
fn declares_assets(&self) -> bool {
true
}
required_assets()
Returns definitions for CSS and JavaScript files. The build process copies these from source locations to the web distribution directory.
fn required_assets(&self, paths: &dyn AssetPaths) -> Vec<AssetDefinition> {
let storage_css = paths.storage_files().join("css");
vec![
AssetDefinition::css(
storage_css.join("core/variables.css"),
"css/core/variables.css",
),
AssetDefinition::css(
storage_css.join("homepage.css"),
"css/homepage.css",
),
]
}
Assets are copied from storage/files/css/ to web/dist/css/. Never place source CSS in extension directories.
Registration
After implementing the Extension trait, register your extension with the register_extension! macro:
register_extension!(WebExtension);
This macro uses the inventory crate to submit your extension for discovery at compile time. The runtime finds all registered extensions when it starts.
Remember to reference your extension in the template's __force_extension_link() function to prevent linker stripping.