Sentinel
1.x
Docs/Sentinel/Custom Plugin
Open

Reading3 min
Updated31 Jul 2026
Sourcev1/guides/custom-plugin.mdx

Sentinel's plugin system uses Go interfaces — implement only the hooks you need and register your plugin with the engine.

The base interface01

Every plugin must implement plugin.Extension:

import "github.com/xraph/sentinel/plugin"

type Extension interface {
    Name() string
}

Choosing hooks02

Sentinel defines 16 lifecycle hooks as separate interfaces. Implement any combination:

HookSignature
EvalRunStartedOnEvalRunStarted(ctx, suiteID, runID, model)
EvalRunCompletedOnEvalRunCompleted(ctx, suiteID, runID, passRate, elapsed)
EvalRunFailedOnEvalRunFailed(ctx, suiteID, runID, err)
CaseStartedOnCaseStarted(ctx, runID, caseID)
CaseCompletedOnCaseCompleted(ctx, runID, caseID, score, elapsed)
CaseFailedOnCaseFailed(ctx, runID, caseID, err)
RegressionDetectedOnRegressionDetected(ctx, suiteID, baselineID, delta)
BaselineSavedOnBaselineSaved(ctx, suiteID, baselineID)
RedTeamStartedOnRedTeamStarted(ctx, suiteID, attackCount)
RedTeamCompletedOnRedTeamCompleted(ctx, suiteID, bypassCount, elapsed)
PersonaEvalStartedOnPersonaEvalStarted(ctx, runID, personaName)
PersonaEvalCompletedOnPersonaEvalCompleted(ctx, runID, personaName, dimensions)
PromptVersionCreatedOnPromptVersionCreated(ctx, suiteID, pvID, version)
ComparisonCompletedOnComparisonCompleted(ctx, suiteID, models, elapsed)
ShutdownOnShutdown(ctx)

Example: Slack notifier03

A plugin that sends a Slack message when a regression is detected:

package slacknotifier

import (
    "context"
    "fmt"

    "github.com/xraph/sentinel/id"
    "github.com/xraph/sentinel/plugin"
)

// Compile-time checks.
var (
    _ plugin.Extension         = (*SlackNotifier)(nil)
    _ plugin.RegressionDetected = (*SlackNotifier)(nil)
    _ plugin.EvalRunCompleted   = (*SlackNotifier)(nil)
)

type SlackNotifier struct {
    webhookURL string
}

func New(webhookURL string) *SlackNotifier {
    return &SlackNotifier{webhookURL: webhookURL}
}

func (s *SlackNotifier) Name() string { return "slack-notifier" }

func (s *SlackNotifier) OnRegressionDetected(
    ctx context.Context,
    suiteID id.SuiteID,
    baselineID id.BaselineID,
    delta float64,
) error {
    msg := fmt.Sprintf("Regression detected in suite %s: %.1f%% drop", suiteID, delta*100)
    return sendSlackMessage(s.webhookURL, msg)
}

func (s *SlackNotifier) OnEvalRunCompleted(
    ctx context.Context,
    suiteID id.SuiteID,
    runID id.EvalRunID,
    passRate float64,
    elapsed time.Duration,
) error {
    msg := fmt.Sprintf("Eval run %s completed: %.1f%% pass rate in %s", runID, passRate*100, elapsed)
    return sendSlackMessage(s.webhookURL, msg)
}

Registering with the engine04

Pass your plugin via engine.WithExtension:

import (
    "github.com/xraph/sentinel/engine"
    slacknotifier "myapp/sentinel-slack"
)

eng, err := engine.New(
    engine.WithStore(store),
    engine.WithExtension(slacknotifier.New("https://hooks.slack.com/...")),
)

Or when using the Forge extension adapter:

import "github.com/xraph/sentinel/extension"

sentinelExt := extension.New(
    extension.WithStore(store),
    extension.WithExtension(slacknotifier.New("https://hooks.slack.com/...")),
)

How dispatch works05

The engine's plugin registry uses type assertions at registration time to discover which hooks each plugin implements. When an event fires, only plugins that implement that specific hook are called. Hooks are invoked sequentially in registration order — if any hook returns an error, it is logged but does not abort the operation.

Built-in plugins06

Sentinel ships two built-in plugins you can use as reference implementations:

PluginPackageHooks
MetricsobservabilityEvalRunStarted, EvalRunCompleted, EvalRunFailed, CaseCompleted, CaseFailed, RegressionDetected
Auditaudit_hookAll 16 hooks

See Observability and Audit Trail for details.