Plugins (Experimental) Edit

OPA can be extended with custom built-in functions and plugins that implement functionality like support for new protocols.

This page focuses on how to build Go plugins that can be loaded when OPA starts however the steps are similar if you are embedding OPA as a library or building from source.

Building Go Plugins

At minimum, your Go plugin must implement the following:

package main

func Init() error {
    // your init function

When OPA starts, it will invoke the Init function which can:

  • Register custom built-in functions.
  • Register custom OPA plugins (e.g., decision loggers, servers, etc.)
  • …or do anything else.

See the sections below for examples.

To build your plugin into a shared object file (.so), you will (minimally) run the following command:

go build -buildmode=plugin plugin.go

This will produce a file named that you can pass to OPA with the --plugin-dir flag. OPA will load all of the .so files out of the directory you give it.

opa --plugin-dir=/path/to/plugins run

NOTE: You must build your plugin against the same version of the OPA that will eventually load the shared object file. If you build your plugin against a different version of the OPA source, the OPA will fail to start. You will see an error message like:

Error: plugin.Open("plugin/logger"): plugin was built with a different version of package

Built-in Functions

To implement custom built-in functions your Init function should call:

  • ast.RegisterBuiltin to declare the built-in function.
  • topdown.RegisterFunctionalBuiltin[X] to register the built-in function implementation (where X is replaced by the number of parameters your function receives.)

For example:

package main

import (

var HelloBuiltin = &ast.Builtin{
	Name: "hello",
	Decl: types.NewFunction(

func HelloImpl(a ast.Value) (ast.Value, error) {
	s, err := builtins.StringOperand(a, 1)
	if err != nil {
		return nil, err
	return ast.String("hello, " + string(s)), nil

func Init() error {
	topdown.RegisterFunctionalBuiltin1(HelloBuiltin.Name, HelloImpl)
	return nil

If you build this file into a shared object and start OPA with it you can call it like other built-in functions:

> hello("bob")
"hello, bob"

For more details on implementing built-in functions, see the OPA Go Documentation.

Custom Plugins

OPA defines a plugin interface that allows you to customize certain behaviour like decision logging or add new behaviour like different query APIs. To implement a custom plugin you must implement two interfaces:

You can register your factory with OPA by calling inside your Init function.

Putting It Together

The example below shows how you can implement a custom Decision Logger that writes events to a stream (e.g., stdout/stderr).

type Config struct {
	Stderr bool `json:"stderr"` // false => stdout, true => stderr

type PrintlnLogger struct {
	mtx sync.Mutex
	config Config

func (p *PrintlnLogger) Start(ctx context.Context) error {
	// No-op.
	return nil

func (p *PrintlnLogger) Stop(ctx context.Context) {
	// No-op.

func (p *PrintlnLogger) Reconfigure(ctx context.Context, config interface{}) {
    defer p.mtx.Unlock()
    p.config = config.(Config)

func (p *PrintlnLogger) Log(ctx context.Context, event logs.EventV1) error {
    defer p.mtx.Unlock()
    w := os.Stdout
    if p.config.Stderr {
        w = os.Stderr
    fmt.Fprintln(w, event) // ignoring errors!
    return nil

Next, implement a factory function that instantiates your plugin:

type Factory struct{}

func (Factory) New(_ *plugins.Manager, config interface{}) plugins.Plugin {
	return &PrintlnLogger{
		config: config.(Config),

func (Factory) Validate(_ *plugins.Manager, config []byte) (interface{}, error) {
	parsedConfig := Config{}
	return parsedConfig, util.Unmarshal(config, &parsedConfig)

Finally, register your factory with OPA:

func Init() {
    runtime.RegisterPlugin("println_decision_logger", Factory{})

To test your plugin, build a shared object file:

go build -buildmode=plugin main.go

Define an OPA configuration file that will use your plugin:


  plugin: println_decision_logger
    stderr: false

Start OPA with the plugin directory and configuration file:

opa --plugin-dir $PWD run --server --config-file config.yaml

Exercise the plugin via the OPA API:

curl localhost:8181/v1/data

If everything worked you will see the Go struct representation of the decision log event written to stdout.

The source code for this example can be found here.

If there is a mask policy set (see Decision Logger for details) the Event received by the demo plugin will potentially be different than the example documented.