Custom source
You can extend Botkube functionality by writing custom source plugin. A source allows you to get asynchronous streaming of domain-specific events. For example, streaming Kubernetes or Prometheus events .
Source is a binary that implements the source Protocol Buffers contract.
Goal​
This tutorial shows you how to build a custom ticker source that emits an event at a specified interval.

For a final implementation, see the Botkube template repository.
Prerequisites​
- Basic understanding of the Go language.
- Go at least 1.18.
- See go.mod for the recommended version used by Botkube team.
- GoReleaser at least 1.13.
Develop plugin business logic​
Create a source plugin directory:
mkdir botkube-plugins && cd botkube-pluginsInitialize the Go module:
go mod init botkube-pluginsCreate the
main.gofile for thetickersource with the following template:cat << EOF > main.go
package main
import (
"context"
"fmt"
"time"
"github.com/hashicorp/go-plugin"
"github.com/kubeshop/botkube/pkg/api"
"github.com/kubeshop/botkube/pkg/api/source"
"gopkg.in/yaml.v3"
)
// Config holds source configuration.
type Config struct {
Interval time.Duration
}
// Ticker implements the Botkube source plugin interface.
type Ticker struct{}
func main() {
source.Serve(map[string]plugin.Plugin{
"ticker": &source.Plugin{
Source: &Ticker{},
},
})
}
EOFThis template code imports required packages and registers
Tickeras the gRPC plugin. At this stage, theTickerservice doesn't implement the required Protocol Buffers contract. We will add the required methods in the next steps.Download imported dependencies:
go mod tidyAdd the required
Metadatamethod:// Metadata returns details about the Ticker plugin.
func (Ticker) Metadata(_ context.Context) (api.MetadataOutput, error) {
return api.MetadataOutput{
Version: "0.1.0",
Description: "Emits an event at a specified interval.",
}, nil
}The
Metadatamethod returns basic information about your plugin. This data is used when the plugin index is generated in an automated way. You will learn more about that in the next steps.Ąs a part of the
Metadatamethod, you can define the plugin dependencies. To learn more about them, see the Dependencies document.Add the required
Streammethod:// Stream sends an event after configured time duration.
func (Ticker) Stream(ctx context.Context, in source.StreamInput) (source.StreamOutput, error) {
cfg, err := mergeConfigs(in.Configs)
if err != nil {
return source.StreamOutput{}, err
}
ticker := time.NewTicker(cfg.Interval)
out := source.StreamOutput{
Event: make(chan source.Event),
}
go func() {
for {
select {
case <-ctx.Done():
ticker.Stop()
case <-ticker.C:
out.Event <- source.Event{
Message: api.NewPlaintextMessage("Ticker Event", true),
}
}
}
}()
return out, nil
}
// mergeConfigs merges all input configuration. In our case we don't have complex merge strategy,
// the last one that was specified wins :)
func mergeConfigs(configs []*source.Config) (Config, error) {
// default config
finalCfg := Config{
Interval: time.Minute,
}
for _, inputCfg := range configs {
var cfg Config
err := yaml.Unmarshal(inputCfg.RawYAML, &cfg)
if err != nil {
return Config{}, fmt.Errorf("while unmarshalling YAML config: %w", err)
}
if cfg.Interval != 0 {
finalCfg.Interval = cfg.Interval
}
}
return finalCfg, nil
}The
Streammethod is the heart of your source plugin. This method runs your business logic and push events into theout.Outputchannel. Next, the Botkube core sends the event to a given communication platform.The
Streammethod is called only once. Botkube attaches the list of associated configurations. You will learn more about that in the Passing configuration to your plugin section.Implement
HandleExternalRequestmethod:Plugins can handle external requests from Botkube incoming webhook. Any external system can call the webhook and trigger a given source plugin. By default, the path of the incoming webhook is
http://botkube.botkube.svc.cluster.local:2115/sources/v1/{sourceName}and it supports POST requests in JSON payload format.If you don't want to handle external events from incoming webhook, simply nest the
source.HandleExternalRequestUnimplementedunder your struct:// Ticker implements the Botkube executor plugin interface.
type Ticker struct {
// specify that the source doesn't handle external requests
source.HandleExternalRequestUnimplemented
}To handle such requests, you need to implement the
HandleExternalRequestmethod. In this case, themessageproperty from payload is outputted to the bound communication platforms:
// HandleExternalRequest handles incoming payload and returns an event based on it.
func (Forwarder) HandleExternalRequest(_ context.Context, in source.ExternalRequestInput) (source.ExternalRequestOutput, error) {
var p payload
err := json.Unmarshal(in.Payload, &p)
if err != nil {
return source.ExternalRequestOutput{}, fmt.Errorf("while unmarshaling payload: %w", err)
}
if p.Message == "" {
return source.ExternalRequestOutput{}, fmt.Errorf("message cannot be empty")
}
msg := fmt.Sprintf("*Incoming webhook event:* %s", p.Message)
return source.ExternalRequestOutput{
Event: source.Event{
Message: api.NewPlaintextMessage(msg, true),
},
}, nil
}
Build plugin binaries​
Now it's time to build your plugin. For that purpose we will use GoReleaser. It simplifies building Go binaries for different architectures.
Instead of GoReleaser, you can use another tool of your choice. The important thing is to produce the binaries for the architecture of the host platform where Botkube is running.
Create the GoReleaser configuration file:
cat << EOF > .goreleaser.yaml
project_name: botkube-plugins
before:
hooks:
- go mod download
builds:
- id: ticker
binary: source_ticker_{{ .Os }}_{{ .Arch }}
no_unique_dist_dir: true
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
goarm:
- 7
snapshot:
name_template: 'v{{ .Version }}'
EOFBuild the binaries:
goreleaser build --rm-dist --snapshot
Congrats! You just created your first Botkube source plugin! 🎉
Now it's time to test it locally with Botkube. Once you're done testing, see how to distribute it.
Passing configuration to your plugin​
Sometimes your source plugin requires a configuration specified by the end-user. Botkube supports such requirement and provides an option to specify plugin configuration under config. An example Botkube configuration looks like this:
communications:
"default-group":
slack:
channels:
"default":
name: "all-teams"
bindings:
sources:
- ticker-team-a
- ticker-team-b
sources:
"ticker-team-a":
botkube/ticker:
enabled: true
config:
interval: 1s
"ticker-team-b":
botkube/ticker:
enabled: true
config:
interval: 2m
This means that two different botkube/ticker plugin configurations were bound to the all-teams Slack channel. For each bound configuration Botkube source dispatcher calls Stream method with the configuration specified under the bindings.sources property.