Skip to content
Arxo Arxo

Plugin System

:::info For engine extenders This page describes internal extension APIs used when building the engine or adding custom metrics. When using the closed-source engine via arxo-loader or the FFI, you cannot register plugins; the public API is load, version, and analyze → JSON. See Rust API and FFI API for the public library API. :::

You can extend the engine with custom metric plugins when building or extending the engine. Plugins implement the MetricPlugin trait (from arxo-types), declare which data they need, and receive a DataStore at runtime to query graphs and derived indices.

All metric plugins implement this trait:

use arxo_types::core::types::{MetricPlugin, MetricContext, MetricResult, DataRequirement};
use async_trait::async_trait;
#[async_trait]
pub trait MetricPlugin: Send + Sync {
fn id(&self) -> &str;
fn version(&self) -> &str;
fn requires(&self) -> Vec<DataRequirement>;
async fn compute(&self, ctx: &MetricContext) -> anyhow::Result<MetricResult>;
}
  • id: Unique plugin identifier (e.g. "custom.my_metric").
  • version: Semantic version string for the plugin.
  • requires: List of data dependencies; the engine builds and caches these before calling compute.
  • compute: Async computation; receives MetricContext and returns a MetricResult.

During compute, you receive:

pub struct MetricContext<'a> {
pub data: &'a dyn DataStore, // Access to graphs and derived data
pub config: serde_json::Value, // Metric-specific config from Config.metrics[].config
pub cache: Cache, // Shared cache for plugin use
}
  • data: Implement the DataStore trait; use it to read import graph, call graph, SCC DAG, etc.
  • config: The config field from the metric entry in Config.metrics.
  • cache: Shared cache; you can store and retrieve values across plugin runs.

Declare what the engine must provide before running your plugin:

pub enum DataRequirement {
ImportGraph,
CallGraph,
EntityGraph,
CallReachability,
CallDependencies,
EffectIndex,
SccDag,
CallSccDag,
Reachability,
ExportsIndex,
GitHistory,
DataFlowGraph,
ComputedMetrics,
WorkspaceConfig,
BuildGraph,
Telemetry,
TelemetryMapping,
}

Only request what you need; the engine builds and caches data on demand. Dependencies between metrics are resolved automatically (e.g. if your plugin requires ComputedMetrics, other metrics that feed into that cache run first).

Through ctx.data you can access (all methods are async unless noted):

MethodReturnsDescription
import_graph()Arc<ImportGraph>Module/file import dependencies
call_graph()Arc<CallGraph>Function/method call graph
entity_graph()Arc<EntityGraph>Entity-level call graph (functions, classes, etc.)
type_graph()Arc<TypeGraph>Type inheritance/implementation graph
effect_index()Arc<EffectIndex>Side-effect detection per file
scc_dag()Arc<SccDag>Strongly connected components (cycles) on import graph
call_scc_dag()CallSccDagSCC DAG on call graph
reachability()Arc<ReachabilityIndex>Transitive dependency reachability
call_reachability()Arc<CallReachabilityIndex>Transitive call reachability
call_dependencies()Arc<CallDependencyIndex>Call dependency analysis
git_history()Arc<GitHistory>Git history (churn, co-change, authors)
telemetry()TelemetryOpenTelemetry trace data
telemetry_mapping()TelemetryMappingIndexTrace-to-code mapping
workspace_config()WorkspaceConfigMonorepo/workspace configuration
build_graph()Option<BuildGraph>Build dependency graph
computed_metrics()ComputedMetricsCacheResults from other metrics (values, UI details)
set_computed_metrics()()Write back to computed metrics cache
project_path()PathBufProject root path
exports_index()()Exports index (reserved)
dataflow_graph()()Dataflow graph (reserved)

Use only the accessors that match the DataRequirement values you declared in requires().

Your plugin returns:

pub struct MetricResult {
pub id: MetricId, // Same as plugin id
pub version: String, // Same as plugin version
pub values: HashMap<MetricKey, f64>, // Key-value metrics (e.g. "cycle_count" -> 2.0)
pub ui_schema: Option<MetricUISchema>,
pub details: Option<HashMap<String, serde_json::Value>>,
pub findings: Option<Vec<Finding>>,
}
  • values: Numeric metrics exposed to policy and reports (e.g. scc.cycle_count).
  • findings: Optional violations or insights with evidence and recommendations.
use arxo_types::core::types::{
DataRequirement, MetricContext, MetricPlugin, MetricResult,
};
use async_trait::async_trait;
use std::collections::HashMap;
struct MyCustomMetric;
#[async_trait]
impl MetricPlugin for MyCustomMetric {
fn id(&self) -> &str {
"custom.module_count"
}
fn version(&self) -> &str {
"1.0.0"
}
fn requires(&self) -> Vec<DataRequirement> {
vec![DataRequirement::ImportGraph]
}
async fn compute(&self, ctx: &MetricContext) -> anyhow::Result<MetricResult> {
let graph = ctx.data.import_graph().await?;
let node_count = graph.node_count();
let edge_count = graph.edge_count();
let mut values = HashMap::new();
values.insert("node_count".to_string(), node_count as f64);
values.insert("edge_count".to_string(), edge_count as f64);
Ok(MetricResult::new(
self.id().to_string(),
self.version().to_string(),
values,
None,
))
}
}

The engine exposes PluginRegistry (with new() and register(Box<dyn MetricPlugin>)). How custom plugins are registered depends on your distribution: some builds allow constructing an orchestrator with a custom registry; others use a fixed set of built-in metrics. See your distribution or engine documentation for the exact API (e.g. Orchestrator::with_registry(registry) if available).

Once registered, the engine runs your plugin when the config includes a metric with matching id (and enabled: true). Plugin order and dependencies are resolved from requires() and the computed-metrics cache.

Plugins that depend on other metrics use DataRequirement::ComputedMetrics. The engine fills the cache with results from plugins that have already run. In compute you can:

  • Read numeric values: ctx.data.computed_metrics().await?.get_metric("scc.cycle_count")
  • Read full plugin result: get_plugin_result(plugin_id)
  • Read UI details: get_plugin_ui_details(plugin_id)

Do not document or rely on internal cache keys; use the public metric ids from the config.

  1. Minimal requires: List only the DataRequirement values you use so the engine can skip building unused data.
  2. Stable id and version: Use a unique, stable id and semantic version for compatibility and caching.
  3. Errors: Return Err from compute on failure; the engine reports it and can skip dependent metrics.
  4. No side effects: Plugins should only read from DataStore and write their result; avoid mutating global state.