Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Title

Gopherbot
DevOps Chatbot

Gophers+bot by Renee French, cropped, cc3.0

By David Parsley, parsley@linuxjedi.org

Gophers + Robot by Renee French (cropped) licensed under Creative Commons License 3.0

Status

Gopherbot is in the middle of the v3 transition. The important point for users is that the current engine behavior is real and usable now, even though some long-tail features and connectors are still evolving.

The docs in this book were reorganized in March 2026 around the workflow the project actually supports today:

  • develop and run the engine locally on a Linux workstation
  • keep robot configuration in git
  • prefer built-in interpreters and current v3 config structure
  • deploy the same robot cleanly to long-running environments

What is current

  • The startup and configuration model documented here matches the current v3 engine defaults and robot skeleton.
  • The SSH connector is the default local-development connector.
  • Slack remains the primary production team-chat connector.
  • Multi-protocol runtime support, PrimaryProtocol, SecondaryProtocols, and username-authoritative identity are part of the current model.
  • BasicMarkdown is the default outgoing message format unless you set another one explicitly.

What is still moving

  • Some included plugins and older integrations are still catching up to v3 expectations.
  • Connector coverage is still uneven across platforms; this manual focuses on what is supported and maintained today.
  • The engine still evolves faster than the user manual at times, so the shipped defaults and example repositories remain useful companion references.

How to read this book

If you are new to Gopherbot, start with Part I and build a local robot first.

If you already run older robots, jump to Configuration Reference and Upgrading Existing Robots.

If you are writing automation, Part III is the modern starting point after you are comfortable with the local workflow.

Foreword

My work with ChatOps began with Hubot around 2012 when I was working as an systems engineer for a small hosting provider. The owner had sent me a link to Hubot, asking me to have a look. I took to the concept immediately, and started automating all kinds of tasks that our support team could easily access from our team chat. Soon they were using our robot to troubleshoot email routing and DNS issues, migrate mailboxes, and build new cPanel server instances.

Not being a very talented (or motivated) Javascript/NodeJS programmer, my Hubot commands invariably followed the same pattern: write it in Javascript if it was trivially easy to do so, otherwise shell out to bash and return the results. This was productive and gave results, but it was ugly and limited in functionality.

When I began teaching myself Go, I needed a good project to learn with. After my experience with Hubot, I decided to write a robot that was more approachable for Systems and DevOps engineers like myself - tasked with providing functionality most easily accessible from e.g. bash or python scripts. Towards that end, Gopherbot’s design:

  • Is CGI-like in operation: the compiled server process spawns scripts which can then use a simple API for interacting with the user / chat service
  • Supports any number of scripting languages by using a simple json-over-http localhost interface
  • Uses a multi-process design with method calls that block

Ultimately, Gopherbot gives me a strong alternative to writing Yet Another Web Application to deliver some kind of reporting, security, or management functionality to managers and technical users. It’s a good meet-in-the-middle solution that’s nearly as easy to use as a web application, with some added benefits:

  • The chat application gives you a single pane of glass for access to a wide range of functionality
  • The shared-view nature of channels gives an added measure of security thanks to visibility, and also a simple means of training users to interact with a given application
  • Like a CGI, applications can focus on functionality, with security and access control being configured in the server process

It is my hope that this design will appeal to other engineers like myself, and that somewhere, somebody will exclaim “Wait, what? I can write chat bot plugins in BASH?!?”

David Parsley, March 2017 / September 2019

#!/bin/bash

# echo.sh - trivial shell plugin example for Gopherbot

# START Boilerplate
[ -z "$GOPHER_INSTALLDIR" ] && { echo "GOPHER_INSTALLDIR not set" >&2; exit 1; }
source $GOPHER_INSTALLDIR/lib/gopherbot_v1.sh

command=$1
shift
# END Boilerplate

configure(){
	cat <<"EOF"
---
Commands:
- Command: "repeat"
  Regex: '(?i:repeat( me)?)'
  Usage: "repeat (me)"
  Summary: "prompt for and trivially repeat a phrase"
  Examples:
  - "(bot) repeat me"
  Keywords: [ "repeat" ]
EOF
}

case "$command" in
# NOTE: only "configure" should print anything to stdout
	"configure")
		configure
		;;
	"repeat")
		REPEAT=$(PromptForReply SimpleString "What do you want me to repeat?")
		RETVAL=$?
		if [ $RETVAL -ne $GBRET_Ok ]
		then
			Reply "Sorry, I had a problem getting your reply: $RETVAL"
		else
			Reply "$REPEAT"
		fi
		;;
esac

Introduction

Gopherbot DevOps Chatbot is a tool for teams of developers, operators, infrastructure engineers and support personnel - primarily for those that are already using Slack or another team chat platform for day-to-day communication. It belongs to and integrates well with a larger family of tools including Ansible, git and ssh, and is able to perform many tasks similar to Jenkins or TravisCI; all of this functionality is made available to your team via the chat platform you’re already using.

To help give you an idea of the kinds of tasks you can accomplish, here are a few of the things my teams have done with Gopherbot over the years:

  • Generating and destroying AWS instances on demand
  • Running software build, test and deploy pipelines, triggered by git service integration with team chat
  • Updating service status on the department website
  • Allowing support personnel to search and query user attributes
  • Running scheduled backups to gather artifacts over ssh and publish them to an artifact service
  • Occasionally generating silly memes

The primary strengths of Gopherbot stem from its simplicity and flexibility. In v3, the preferred workflow is to run the engine directly on a Linux workstation while you build and test your robot locally, then deploy that same robot where it belongs later. Gopherbot still bootstraps cleanly onto servers and containers, but local development is now the default story rather than a special case.

Simple command plugins can still be written in bash, python or ruby, but v3 leans much harder on built-in interpreters and Go-based extensions so robots depend on fewer external tools. Like any user, the robot can also have its own encrypted ssh key for performing remote work and interfacing with git services.

The philosophy underlying Gopherbot is the idea of solving the most problems with the smallest set of general purpose tools, accomplishing a wide variety of tasks reasonably well. The interface is much closer to a CLI than a web GUI, but it’s remarkable what can be accomplished with a shared CLI for your team’s infrastructure.

The major design goals for Gopherbot are reliability and portability, leaning heavily on “configuration as code”. Ideally, custom add-on plugins and jobs that work for a robot instance in Slack should work just as well if your team moves, say, to Rocket.Chat. This goal ends up being a trade-off with supporting specialized features of a given platform, though the Gopherbot API enables platform-specific customizations if desired.

Secondary but important design goals are configurability and security. Individual commands can be constrained to a subset of channels and/or users, require external authorization or elevation plugins, and administrators can customize help and command matching patterns for stock plugins. Gopherbot has been built with security considerations in mind from the start, employing strong encryption, privilege separation, and a host of other measures to make your robot a difficult target for potential attackers.

Modern Gopherbot still assumes that serious robots keep configuration in git, and this manual continues to focus on that model. The big difference is that the day-to-day authoring loop now starts on your workstation: run the engine locally, edit the robot repo locally, reload quickly, and only then hand that robot off to a server, VM, or container.

That’s it for the “marketing” portion of this manual - by now you should have an idea whether Gopherbot would be a good addition to your DevOps tool set.

Terminology

This section is the vocabulary for the rest of the manual.

  • Gopherbot: the engine installation or source tree that contains the binary, built-in defaults, libraries, stock plugins, and robot skeleton.
  • robot: a configured running instance of Gopherbot for one team or one purpose.
  • Robot: the API object passed to plugin, job, and task handlers.
  • install tree: the directory that contains the gopherbot executable plus shipped conf/, lib/, resources/, plugins/, jobs/, tasks/, and robot.skel/.
  • robot home: the working directory where you launch the engine for one robot. In practice this is the directory that may contain .env, .setup-state, workspace/, and custom/.
  • custom config: the robot-specific overlay under custom/. Installed defaults load first, then custom/conf/... overrides them.
  • default robot: the built-in demo robot you get when no custom repository or custom config has been loaded yet. By default this robot is named floyd.
  • bootstrap: the startup path where Gopherbot has enough environment to know which robot repository it should clone, but does not have the robot’s config on disk yet.
  • plugin: interactive automation triggered by chat messages.
  • job: scheduled or triggered automation that usually assembles a pipeline of tasks.
  • task: a reusable unit of work inside a pipeline. Some tasks are stock and included with the engine; others are custom.
  • pipeline: the ordered execution chain built by a plugin or job.
  • primary protocol: the connector that provides the robot’s main runtime identity and default message context.
  • secondary protocol: any additional connector enabled alongside the primary one.
  • UserRoster: the global user directory for the robot. Identity and authorization decisions are username-based.
  • ProtocolConfig: the connector-local configuration block loaded from conf/protocols/<protocol>.yaml.
  • BasicMarkdown: the default cross-protocol outgoing message format in v3.
  • parameter: a name/value setting attached to a task, plugin, job, or namespace. Secure parameters may need to be retrieved through the Robot API instead of plain environment variables.
  • namespace: a reusable group of parameters shared across related automation.
  • environment file: .env or private/environment, loaded early at startup. This is where deployment-specific settings such as GOPHER_ENVIRONMENT, GOPHER_CUSTOM_REPOSITORY, or secrets usually live.

You will also see these names throughout the docs:

  • Floyd: the name of the shipped default robot.
  • Clu: the long-running development robot used while building v3.
  • robot.skel: the standard starting point for a new robot repository.

Build and Install

The v3 installation story is simple:

  1. Install the engine once on a Linux machine you control.
  2. Create one or more robot working directories.
  3. Run the engine from those directories while you develop locally.
  4. When the robot is ready, deploy the same robot configuration to a VM, server, or container.

Two supported ways to install the engine

  • Build from source and keep the checkout intact.
  • Unpack a release archive into a stable install location such as /opt/gopherbot.

In both cases, the gopherbot binary expects the rest of the install tree to be nearby. Treat the installation as a directory tree, not as a single standalone executable.

  • Engine install tree: /opt/gopherbot or a source checkout you build in place
  • Robot home: one directory per robot, for example /srv/robots/acme or ~/robots/acme

You run the binary from the install tree while your current working directory is the robot home. That gives the engine access to both worlds:

  • installed defaults from the engine tree
  • writable custom config from the robot home

Continue with Requirements if you are starting fresh, or skip ahead to Create Your First Robot if you already have a working install.

Requirements

Runtime requirements

  • Linux. Gopherbot is developed and tested as a Linux-first tool.
  • A shell account where you can run long-lived processes and keep a working directory for each robot.
  • git and ssh if you want bootstrap, update, or repository handoff workflows.
  • A team-chat platform or local connector setup appropriate for your robot.

Build requirements

If you are building from source instead of using a release archive:

  • Go 1.24 or newer
  • make
  • standard build utilities such as tar and gzip if you want packaged archives

What you do not need for the default v3 workflow

  • A dev container just to write plugins
  • An extra wrapper script just to start a development robot
  • External interpreters for every language up front

You can still use Bash, Python, or Ruby extensions, but the modern path is to start with the built-in interpreters and Go support already included in the engine.

Build from Source

If you want the latest engine changes, build Gopherbot from a source checkout and keep that checkout as your install tree.

Build steps

git clone https://github.com/lnxjedi/gopherbot.git
cd gopherbot
make gopherbot

That leaves you with a gopherbot binary in the repository root.

Important detail

Do not copy just the binary somewhere else and throw the checkout away. The executable looks for shipped defaults and resources relative to its own location. A working source-based install tree includes at least:

  • gopherbot
  • conf/
  • lib/
  • resources/
  • plugins/
  • jobs/
  • tasks/
  • robot.skel/

Typical source-based workflow

git clone https://github.com/lnxjedi/gopherbot.git ~/src/gopherbot
cd ~/src/gopherbot
make gopherbot

mkdir -p ~/robots/acme
cd ~/robots/acme
~/src/gopherbot/gopherbot

In that example:

  • ~/src/gopherbot is the engine install tree
  • ~/robots/acme is the robot home

That split is the normal v3 authoring workflow.

Install the Engine

If you prefer a cleaner separation between the engine and your robot homes, unpack a release archive into a stable location such as /opt/gopherbot.

Suggested layout

/opt/gopherbot/
  gopherbot
  conf/
  lib/
  resources/
  plugins/
  jobs/
  tasks/
  robot.skel/

Running a robot from that install

Create a robot home somewhere writable:

mkdir -p ~/robots/acme
cd ~/robots/acme
/opt/gopherbot/gopherbot

That command uses:

  • the install tree at /opt/gopherbot for built-in assets
  • the current directory for writable robot state and custom/

Sample SSH key for the default robot

The shipped default robot includes sample SSH users and keys so you can connect locally without doing any setup first. For the alice demo user:

chmod 600 /opt/gopherbot/resources/ssh-default/alice.key
ssh -i /opt/gopherbot/resources/ssh-default/alice.key -p 4221 alice@localhost

That is the fastest way to kick the tires on a fresh install.

Create Your First Robot

The easiest way to understand Gopherbot is to treat the engine and the robot as two separate things:

  • the engine is installed once
  • a robot is a working directory plus git-managed custom config

In v3, you should usually build the engine once, create a fresh robot home, run the default robot locally, and let the onboarding flow scaffold custom/ for you.

mkdir -p ~/robots/acme
cd ~/robots/acme
/opt/gopherbot/gopherbot

If you built from source instead of installing under /opt/gopherbot, use the path to that build tree’s gopherbot binary instead.

On first run, no robot-specific config exists yet, so Gopherbot starts the default robot. By default it listens on the local SSH connector at localhost:4221.

From another terminal:

ssh -i /opt/gopherbot/resources/ssh-default/alice.key -p 4221 alice@localhost

Once connected:

  • run help
  • run info
  • start the onboarding flow with ;new robot

The new robot flow scaffolds a custom/ tree, captures your initial SSH identity, and can later help with repository handoff so the robot is ready to bootstrap elsewhere.

The next few pages walk through that workflow in more detail.

Local Workstation Workflow

This is the workflow the v3 docs assume:

  1. Keep one engine install tree on your workstation.
  2. Keep one working directory per robot.
  3. Run the engine locally from the robot’s working directory.
  4. Edit the robot’s custom/ files directly.
  5. Use reload to tighten the edit-test loop.

Why this workflow works well

Gopherbot is moving toward using its built-in interpreters and internal helpers more aggressively. That makes local authoring much smoother:

  • fewer external runtime dependencies
  • fast reload cycles
  • easier debugging of config and scripts
  • less friction for DevOps engineers who already live in a shell and editor

A good minimum setup

  • one shell running the engine from the robot home
  • one shell or editor for changing files under custom/
  • one local SSH session into the robot for interactive testing

That gives you a tight loop without adding extra orchestration around the engine itself.

Run the Default Robot

When you start Gopherbot in an empty robot home, it does not fail. It starts the shipped default robot instead.

That default is meant to be useful, not just decorative:

  • it starts the local SSH connector by default
  • it includes built-in help and admin commands
  • it can scaffold a new robot with new robot

Start it

mkdir -p ~/robots/demo
cd ~/robots/demo
/opt/gopherbot/gopherbot

Connect to it

chmod 600 /opt/gopherbot/resources/ssh-default/alice.key
ssh -i /opt/gopherbot/resources/ssh-default/alice.key -p 4221 alice@localhost

First commands to try

  • help
  • commands
  • info
  • whoami
  • ;new thread
  • ;new robot

What happens next

Until a real robot repository is configured, you are talking to the default robot named floyd. Once custom/ exists and your robot is configured, that local working directory becomes the robot.

If you stop in the middle of onboarding, the engine keeps state in .setup-state and the new robot resume and new robot repo commands pick up where you left off.

Robot Repository Layout

The robot’s writable home directory is where you launch the engine. The important subdirectory inside it is usually custom/.

Typical robot home

acme-bot/
  .env
  .setup-state
  custom/
    conf/
      robot.yaml
      environments/
      protocols/
      brains/
      history/
      plugins/
      jobs/
    plugins/
    jobs/
    tasks/
    lib/
  workspace/

What lives where

  • .env: deployment-specific environment variables and secrets
  • custom/conf/robot.yaml: robot-wide policy and environment include point
  • custom/conf/environments/*.yaml: environment-specific runtime defaults
  • custom/conf/protocols/*.yaml: connector-local ProtocolConfig
  • custom/conf/brains/*.yaml: selected brain provider settings
  • custom/conf/history/*.yaml: selected history provider settings
  • custom/conf/plugins/*.yaml: plugin matchers, help, and local config
  • custom/conf/jobs/*.yaml: job schedules and local job config
  • custom/plugins/, custom/jobs/, custom/tasks/: your automation code
  • custom/lib/: shared helper code for external languages
  • workspace/: working files generated by jobs or tasks

Installed defaults versus custom files

Gopherbot always loads installed defaults first and then overlays your custom files on top. That means your custom robot should usually contain only the local delta:

  • enable or disable shipped features
  • provide secrets and parameters
  • add new plugins, jobs, or tasks
  • intentionally override behavior

Avoid copying the engine’s whole default config into custom/ unless you truly want to fork that behavior.

Connector Credentials

Connector credentials belong in connector-local files under custom/conf/protocols/.

That is a deliberate v3 design choice:

  • global robot policy stays in custom/conf/robot.yaml
  • connector identity and transport details stay in each connector’s ProtocolConfig

Local development

For local development you often need no third-party credentials at all. The SSH connector is the default local-development connector, and the default robot ships with sample SSH users and keys so you can get started immediately.

Production connectors

Today the most common production setup is:

  • SSH for local development and recovery access
  • Slack as the primary team-chat connector

Credential handling rules

  • Keep secrets encrypted in config when possible with {{ decrypt "..." }}.
  • Keep deployment-specific values in .env when they are environment-dependent.
  • Keep connector user mappings inside the connector config, not in top-level robot config.

The Slack page on the next step shows the current Socket Mode shape.

Slack Socket Mode

Slack is configured in custom/conf/protocols/slack.yaml.

Minimal shape:

ProtocolConfig:
  AppToken: xapp-{{ decrypt "<slack-app-token>" }}
  BotToken: xoxb-{{ decrypt "<slack-bot-token>" }}
  HearSelf: false
  UserMap:
    alice: U12345678

Notes

  • UserMap is connector-local in v3. It does not belong in robot.yaml.
  • The engine still makes authorization decisions by canonical username, not by Slack user ID.
  • Slash commands arrive as hidden bot-addressed messages, so hidden commands still have to be explicitly allowed by the plugin.

If Slack is your primary protocol, set it in custom/conf/environments/<environment>.yaml or directly in custom/conf/robot.yaml, depending on how you structure your environments.

Run and Deploy Your Robot

The same robot can move cleanly through these stages:

  1. local workstation development
  2. shared team development or review
  3. long-running deployment on a server, VM, or container

The big v3 improvement is that you do not need a special container-only development environment to get there.

Local runs

For day-to-day development, launch the engine from the robot home and keep editing custom/:

cd ~/robots/acme
/opt/gopherbot/gopherbot

Long-running deployments

For long-lived robots, the most common approaches are:

  • systemd on a VM or server
  • a container in Kubernetes or another orchestrator

In those environments the robot typically boots from:

  • a checked-out robot home already on disk, or
  • GOPHER_CUSTOM_REPOSITORY plus GOPHER_DEPLOY_KEY so the engine can bootstrap itself

Use the next pages for the concrete deployment knobs.

Deployment Environment Variables

These are the environment variables you are most likely to care about when moving a robot between local development and deployment.

Usually required in deployed environments

  • GOPHER_ENCRYPTION_KEY
    • Unlocks encrypted config values and encrypted robot keys.
  • GOPHER_CUSTOM_REPOSITORY
    • The git URL for the robot repository when bootstrapping from an empty directory.
  • GOPHER_DEPLOY_KEY
    • A deploy key the robot can use to clone GOPHER_CUSTOM_REPOSITORY.

Commonly useful

  • GOPHER_ENVIRONMENT
    • Selects custom/conf/environments/<environment>.yaml.
    • Defaults to development for scaffolded robots when not set.
  • GOPHER_CUSTOM_BRANCH
    • Overrides the branch used for custom config checkout and update flows.
  • GOPHER_SSH_PORT
    • Overrides the SSH connector listen port.
  • GOPHER_LOGDEST
    • Overrides the log destination, for example stdout or robot.log.
  • GOPHER_LOGLEVEL
    • Sets the runtime log level.
  • GOPHER_MESSAGE_FORMAT
    • Overrides the default outgoing format. If unset, v3 defaults to BasicMarkdown.

Variables that matter mostly during local development

  • GOPHER_ENVIRONMENT=development
    • Explicitly pins the development environment if you maintain several.
  • GOPHER_SSH_PORT
    • Helpful when you run more than one local robot.
  • GOPHER_DEFAULT_PROTOCOL
    • Useful only for special multi-protocol routing cases.

Practical advice

  • Keep .env mode-restricted and out of casual reach.
  • Prefer putting deployment-specific values in .env and stable robot behavior in custom/conf/.
  • Do not depend on old top-level GOPHER_PROTOCOL habits as your main environment selector; v3 expects GOPHER_ENVIRONMENT to drive environment-specific config.

Run with systemd

systemd is the easiest long-running deployment target for most Linux robots.

  1. Install the engine in a stable location such as /opt/gopherbot.
  2. Create a dedicated robot home such as /srv/robots/acme.
  3. Create a dedicated Unix user for that robot.
  4. Put the robot’s .env in the robot home with restrictive permissions.
  5. Copy and adapt resources/robot.service or resources/user-robot.service.

Minimal service pattern

[Service]
Type=simple
WorkingDirectory=/srv/robots/acme
ExecStart=/opt/gopherbot/gopherbot -plainlog
Restart=on-failure
TimeoutStopSec=600

Notes

  • Keep the engine install tree and the robot home separate.
  • The robot home should be writable by the robot user.
  • TimeoutStopSec=600 is intentional; Gopherbot tries to finish in-flight pipelines during graceful shutdown.
  • Prefer configuring protocol and behavior in custom/conf/ and .env instead of stuffing everything into Environment= lines.

Bring it up

sudo systemctl daemon-reload
sudo systemctl enable acme-bot
sudo systemctl start acme-bot
sudo systemctl status acme-bot

Container Deployment

Containers are a valid deployment target for Gopherbot.

Good use cases

  • you already operate Kubernetes or another container platform
  • you want immutable engine images and environment-driven robot bootstrap
  • your team already manages secrets and restart policy through an orchestrator

What to keep in mind

  • Build and test the robot locally first.
  • Treat the container as a deployment wrapper around the same robot, not as a separate authoring environment.
  • Provide the same critical environment values you would keep in .env on a VM.
  • Avoid running multiple production instances of the same robot unless you have explicitly designed for that.

Typical bootstrap model

An empty working directory inside the container is fine as long as you provide:

  • GOPHER_ENCRYPTION_KEY
  • GOPHER_CUSTOM_REPOSITORY
  • GOPHER_DEPLOY_KEY
  • any environment selector or connector overrides your robot depends on

On startup, Gopherbot will clone the robot repo, load config, and continue as a normal robot.

Docker Example

A minimal container run looks like this:

docker container run --name acme-bot \
  --restart unless-stopped \
  --env-file .env \
  -e HOSTNAME="$(hostname)" \
  ghcr.io/lnxjedi/gopherbot:latest

Notes

  • Build and validate the robot locally before you do this.
  • Keep .env out of image layers and source control.
  • If your robot needs extra operating-system tools, build a derived image that adds them.
  • podman works fine for the same pattern.

Kubernetes Example

Kubernetes is a good fit when:

  • your team already deploys operational tooling in-cluster
  • you want secrets and restart behavior managed by the platform
  • the robot’s external dependencies fit cleanly into a container image

The repository ships an example manifest at resources/deploy-gopherbot.yaml and Helm material under resources/helm-gopherbot/.

Minimum checklist

  • store the robot’s environment values as a Kubernetes secret
  • mount or inject those values into the container
  • run only one production instance for a given robot unless you have planned for coordination
  • ship any extra system dependencies in the image instead of assuming they exist in the cluster

Daily Usage

Gopherbot listens the way a teammate does: it sees messages coming from users, channels, threads, and direct-message contexts, then routes them through command, ambient-message, and job-trigger logic.

In practice, most users mainly need to know four things:

  • how to address the robot
  • where a command is available
  • how to discover commands with help
  • how threads and direct messages affect replies

This section covers those user-facing behaviors rather than the internal implementation details.

Addressing the Robot

Every robot has both:

  • a regular name, such as floyd
  • a one-character alias, such as ;

Both are valid ways to address a command.

Common patterns

;ping
floyd, ping
floyd: ping
ping, floyd

ping is the canonical first command because it is usually available everywhere the robot is present.

A useful routing nuance

If you send what looks like a command but forget to address the robot, many connectors support the follow-up pattern of sending just the bot name or alias as your next message. That tells the robot to interpret the previous message as if it had been addressed.

Hidden commands

Some connectors support hidden messages, such as Slack slash commands or SSH hidden messages. Hidden messages are not a free bypass:

  • the command still has to be explicitly allowed as hidden by the plugin
  • the message still has to be treated as addressed to the robot

Command Matching

Gopherbot commands are matched by regular expressions, not fuzzy AI intent matching. That is a strength: the behavior is predictable, testable, and safe for automation.

What happens when a command does not match

When you address the robot directly but no command matches, the robot usually replies with a message that points you back to help.

That failure can mean one of three things:

  • you mistyped the command
  • the command exists but is not available in the current channel or DM context
  • the command is not visible to you because of policy or authorization

v3 quality-of-life behavior

If a command is clearly valid somewhere else, the engine can often tell you that it belongs in another channel or in DM. That happens before generic catch-all behavior, so users get a more useful answer than a plain “no command matched”.

Best next step

Use help <keyword> instead of guessing. The help system understands command metadata and current visibility better than trial and error.

Channels, DMs, and Threads

Availability is a first-class part of Gopherbot command design.

Channels

Many commands are intentionally limited to specific channels. That is normal for operational robots. For example:

  • build or deploy commands may only belong in a job channel
  • support commands may only belong in a support channel
  • noisy or playful commands may be kept out of primary work channels

Direct messages

Some commands are better in DM, especially when they:

  • prompt for follow-up input
  • reveal sensitive data
  • would create too much channel noise

Threads

Threads matter in Gopherbot. The engine tracks thread context and many plugins use it to keep long workflows contained. On connectors that support threads well, replying in-thread is usually the cleanest way to run multi-step operations.

Practical takeaway

If a command works somewhere else but not here, it is often a channel-placement issue rather than a typo. Use help <keyword> or commands in the current context to see what is actually available.

Finding Commands with Help

The built-in help plugin is the fastest way to discover what a robot can do in your current context.

Most useful commands

  • help
  • help <keyword>
  • commands
  • help-all
  • info

What makes v3 help better

Help in v3 is tied more closely to command metadata:

  • Usage
  • Summary
  • Examples
  • Keywords

That means users see help that is closer to the actual command surface rather than a detached block of prose.

Context-aware behavior

Help is filtered by the current channel or DM context, and where possible by what the user should actually be allowed to see. If a command exists somewhere else, help can point you there instead of pretending the command does not exist.

Advice for users

  • Use commands when you want to browse.
  • Use help <keyword> when you know roughly what you want.
  • Use info when you need operational details about the robot itself.

Built-in Commands

Every robot ships with a small set of foundational commands, and many robots also keep some stock plugins enabled.

Core commands most users will see

  • ping: check that the robot is listening
  • help, commands, help-all: discover commands
  • info: inspect runtime and admin-facing basics
  • whoami: see how the robot identifies you

Commonly enabled stock plugins

  • links: store and search shared bookmarks
  • lists: keep simple shared lists
  • onboarding helpers on the default robot, such as new robot

Why these matter

These commands are more than demos:

  • info helps confirm which robot, branch, and environment you are talking to
  • whoami helps debug identity mapping and UserRoster issues
  • links and lists are small but real examples of persistent memory usage

Message Context

Gopherbot keeps track of more than just message text. Context can include:

  • the user
  • the protocol
  • the channel
  • the thread
  • labeled values captured by command matchers

That context is what lets plugins do useful follow-up behavior without inventing their own ad-hoc state machine for every conversation.

Examples

  • prompting a user and waiting for the reply in the same channel or thread
  • remembering the current list name or item in a short workflow
  • continuing a threaded conversation without polluting the main channel

Context is intentionally scoped. In v3, thread-scoped memory includes protocol context so different connectors do not collide with each other.

Administration and Day 2 Operations

Once a robot is in use, most operational work falls into a short list:

  • update config from git
  • reload the running robot
  • inspect active protocols and branches
  • pause or resume jobs
  • inspect logs when something goes wrong

The sections below focus on those recurring tasks rather than one-time setup.

Updating and Reloading

There are three related admin actions you will use a lot:

  • reload
  • update
  • switch-branch <branch> or default-branch

reload

Use reload when you have already changed files on disk and want the running robot to re-read configuration.

update

Use update when the running robot should git pull its custom repository and then reload.

This is the common production flow:

  1. make and test changes locally
  2. commit and push them
  3. tell the production robot to update

Branch-aware workflows

switch-branch <branch> and default-branch are useful when you want a fast test-and-rollback cycle on a live robot without manually logging into the host and editing its checkout.

Good habit

Prefer testing significant changes on a local robot first. update is great for safe rollouts, not for discovering syntax mistakes in front of your whole team.

Administrative Commands

The built-in admin plugin provides the core operational command set.

Most important commands

  • reload
  • update
  • switch-branch <branch>
  • default-branch
  • git-info
  • protocol-list
  • protocol-start <name>
  • protocol-stop <name>
  • protocol-restart <name>
  • ps
  • kill-pipeline <wid>
  • pause-job <job>
  • resume-job <job>
  • paused-jobs
  • quit
  • restart
  • abort

What these tell you

  • branch commands answer “what config am I running?”
  • protocol commands answer “which connectors are up right now?”
  • process commands answer “what work is currently in flight?”

These commands are intentionally admin-only. They are part of the day-2 operational surface, not the normal user command set.

Logging

Gopherbot logs can go to:

  • stdout
  • stderr
  • a log file such as robot.log
  • local development: stdout is usually fine unless the connector needs a clean terminal
  • terminal-style local interaction: let the connector move logs to robot.log when needed
  • production under systemd: plain logs to stdout/stderr are usually easiest to collect

Useful debug knobs

  • GOPHER_LOGLEVEL
  • GOPHER_LOGDEST
  • GOPHER_HTTP_DEBUG

Be careful with GOPHER_HTTP_DEBUG; it is for low-level troubleshooting and may expose sensitive request or response data.

General Concepts

Since Gopherbot is designed for ChatOps with the idea of being an ‘Enterprise Sudo’, it is important to discuss security-related issues. It is expected that as team chat services and therefore ChatOps becomes more prevalent in mainstream IT, understanding of ChatOps security issues will improve and mature. Laid out here are a few general considerations along with some of Gopherbot’s specific security-related features.

Plugin (non-)Separation

Gopherbot’s design is intended to allow eventual support for a strong separation between external plugins, so that e.g. internally developed plugins can (more) safely coexist with 3rd-party external plugins. This is not yet fully implemented, however the API design should accommodate it. Currently the robot and all external plugins run as the robot user; mainly this means that all external plugins can read whatever files the main gopherbot process can read, including the file-based brain.

Trusted (internally-developed) and Untrusted (third party) Plugins

Gopherbot is designed with an eye towards future proliferation of third party plugins - from managing cloud provider infrastructure, to ordering pizza, to generating memes or spitting out random facts about cats and Chuck Norris. Currently there are only a small number of plugins available, but it’s still important to discuss and consider these aspects of ChatOps security.

When running Gopherbot for doing real, privileged, security-sensitive work, best practices would dictate:

  • Never run both trusted and untrusted plugins in the same instance of Gopherbot
  • Never invite a bot running untrusted plugins into a channel where a bot with trusted plugins is running

The reasoning here is that plugins have the ability to listen and respond to everything said, and a user might not always be certain of what plugin they’re interacting with. That being said, many plugins (such as weather) are very short and easy to read/audit for malicious code.

Communication between robots

Visibility

Each plugin can specify one or more of Users, Channels, AllChannels, RequireAdmin, AllowDirect and DirectOnly that will limit who a plugin is visible to, and whether it can be accessed in a given channel or via direct message. For instance, you could allow certain security-sensitive plugins to be visible only in a few invite-only private channels. Note that if a given plugin is available to a user only in certain channels, help <keyword> will list the channels where a plugin is available.

Additionally, being able to restrict based on channels means that potentially security-sensitive operations will always be performed in the view of other members of the team, adding another level of protection.

Authorization

If a plugin is available to the user, the robot will then check authorization, if configured. Instead of creating a pluggable interface for e.g. group membership, or other authorization primitives, Gopherbot uses the notion of an “Authorizer” plugin that gets called with the command authorize, and these arguments: the name of the plugin being authorized, an optional group/role name, followed by the command and any arguments to the command. The plugin can perform look-ups or optionally interact with the user, and is expected to exit with bot.Success if the user is authorized, bot.Fail if the user isn’t authorized, or bot.MechanismFail / bot.ConfigurationError if e.g. LDAP or some other central service couldn’t be reached or is misconfigured. Note that the libraries define constants for these values.

Authorization is useful for all kinds of cases where a given plugin may be available in several channels, but uses different resources based on the channel and simply limiting visibility isn’t sufficient. It’s also useful for implementing e.g. group security. The main upside is that it gives the bot administrator the ability to implement arbitrary logic for determining authorization, but that’s also the main downside - it may require scripting to configure certain types of authorization.

Elevation

Finally, if the user passes the authorization check, the robot will then check for elevation if a given command is listed in ElevatedCommands or ElevateImmediateCommands. Elevation behaves similarly to sudo, in that the user may be required to supply a second form of authentication (mfa / 2fa) before an action is allowed. Individual elevation plugins may be configurable with a timeout for ElevatedCommands, such that a user can continue to perform elevated operations for a period of time before re-authentication is required. As the name suggests, ElevateImmediateCommands will always require mfa, and should therefore be used sparingly, especially if the mfa method is onerous (e.g. totp).

Some elevators use a human approval flow instead of personal MFA. The built-in builtin-userapproval elevator lets a configured approver approve or deny a sensitive command.

Hardened Design

Privilege Separation

Gopherbot 2.0 introduced the ability to use privilege separation where the robot executable is installed setuid, and run by the robot ‘user’. Thus, the main process will run as another user; in the Docker containers and Ansible role, this defaults to ‘bin’. The starting environment file .env should only be readable by this privileged user, as this is where the robot obtains e.g. it’s encryption key or Slack token. Externally executed plugin, job and task scripts (but not Go plugins) all run as the non-privileged user that started the process.

Encryption

Gopherbot 2.0 also adds AES-256 / GCM encryption at it’s core, which is required for storing secrets and parameters in the robot’s brain, and can optionally be used to fully encrypt the contents of the brain.

Memory Protection

Preliminary / incomplete has been added for storing the robot’s internal encryption key in protected memory. The current implementation provides minimal additional hardening, and needs further thought and development.

User Approval Elevation

builtin-userapproval is an elevation plugin for commands that should require a human approval step instead of a personal MFA challenge. It is useful when a sensitive action should be approved by another trusted operator, such as a deploy, account change, firewall update, or credential rotation.

Use it when the question is:

  • “Is this action important enough that another person should approve it?”

Do not use it as a substitute for base authorization. Authorization still decides whether the requester is allowed to ask for the action. Elevation adds a second confirmation step after that authorization succeeds.

How the Flow Works

When a command requires elevation and uses builtin-userapproval:

  1. Gopherbot finds the effective approver list for the current plugin.
  2. In strict mode, the requester is removed from that list.
  3. The requester chooses one remaining approver from a short lettered menu.
  4. The selected approver receives a direct yes/no prompt.
  5. If the approver replies yes or y, the protected command runs.

If the selected approver replies no, does not reply before the prompt times out, or there are no eligible approvers, elevation fails and the protected command does not run.

Strict Mode

Strict mode prevents self-approval. It is enabled by default.

With strict mode enabled, this configuration:

Config:
  FallbackApprovers: [ alice, bob ]

means:

  • if alice requests elevation, only bob can approve
  • if bob requests elevation, only alice can approve
  • if alice is the only listed approver, Alice cannot approve herself and elevation fails

This is the safest default for most production robots.

Non-Strict Mode

Non-strict mode allows a listed approver to approve their own elevation automatically. This can be useful for small teams, break-glass commands, personal robots, development environments, or actions where the approver list is being used as a lightweight operator allow-list.

To disable strict mode globally for this elevator:

Config:
  DefaultStrict: false
  FallbackApprovers: [ alice, bob ]

With that configuration, if alice requests an elevated command and alice is in the effective approver list, Gopherbot approves the elevation immediately.

If the requester is not in the effective approver list, the normal choose-an-approver flow still runs.

Configuration

Enable the elevator by setting it as the robot default:

DefaultElevator: builtin-userapproval

Then configure the built-in plugin in conf/plugins/builtin-userapproval.yaml:

ReplyMatchers:
- Label: approvalChoice
  Regex: '[a-z]'

Config:
  DefaultStrict: true
  FallbackApprovers: [ david ]
  PluginApprovers:
    deploy:
      Approvers: [ alice, bob, david ]
      Strict: false
    wireguard: [ alice, bob ]

FallbackApprovers is used when the current plugin is not listed under PluginApprovers.

PluginApprovers can use either form:

  • list form: wireguard: [ alice, bob ]
  • object form with a strict-mode override:
PluginApprovers:
  deploy:
    Approvers: [ alice, bob, david ]
    Strict: false

The list form uses DefaultStrict. The object form can override strict mode for that plugin.

Protecting Commands

In the target plugin’s config, list the commands that require elevation:

ElevatedCommands:
- deploy
- rollback

Commands:
- Command: deploy
  SimpleMatcher: "deploy <service:ident>"
- Command: rollback
  SimpleMatcher: "rollback <service:ident>"

If the robot has DefaultElevator: builtin-userapproval, those commands use user approval automatically.

To use this elevator for one plugin only, set Elevator in that plugin config:

Elevator: builtin-userapproval
ElevatedCommands:
- rotate-key

Choosing Approvers

Approvers are canonical Gopherbot usernames, not Slack IDs, email addresses, SSH usernames, or connector-local IDs. The connector maps transport identity to the canonical username before authorization and elevation run.

Use names from UserRoster and AdminUsers, such as:

UserRoster:
- UserName: alice
  UserID: U123
- UserName: bob
  UserID: U456

Approver names are normalized by trimming whitespace and lowercasing before comparison.

Operational Notes

Keep approver lists small and intentional. The requester sees a single-letter menu, and the implementation supports up to 26 eligible approvers.

Prefer strict mode for production operations where a second person should really be involved. Use non-strict mode only when self-approval is an intentional policy choice.

For commands that return sensitive data, combine elevation with private-command policy. User approval controls whether the command may run; it does not automatically move the response into a direct message.

Extending Your Robot

Gopherbot is most useful when your team adds its own automation. In v3, the recommended extension path is:

  1. start with the built-in interpreters or Go
  2. keep extension config in custom/conf/
  3. keep extension code in custom/plugins/, custom/jobs/, or custom/tasks/
  4. test locally and reload often

Choose the right extension type

  • plugin: a user-facing command or ambient matcher
  • job: scheduled or triggered automation that assembles work
  • task: a reusable unit of work inside pipelines

Language guidance

The current project preference is:

  1. Lua for approachable built-in scripting
  2. Go for safety and performance
  3. JavaScript where it fits well

Bash, Python, and Ruby are still supported, especially when they are the most practical fit for an existing integration.

Your First Plugin

For a first plugin in v3, use Lua. It gives you a fast local loop without requiring an external runtime.

1. Register the plugin in custom/conf/robot.yaml

ExternalPlugins:
  "hello":
    Description: Example hello-world plugin
    Path: plugins/hello.lua

2. Add matcher and help metadata in custom/conf/plugins/hello.yaml

Commands:
- Command: hello
  Regex: '(?i:hello)$'
  Usage: "hello"
  Summary: "reply with a friendly greeting"
  Examples:
  - "(alias) hello"
  Keywords: [ "hello", "example" ]

3. Create custom/plugins/hello.lua

local gopherbot = require "gopherbot_v1"
local Robot = gopherbot.Robot
local task = gopherbot.task

local cmd = arg and arg[1] or ""

if cmd == "init" then
  return task.Normal
end

if cmd == "hello" then
  local bot = Robot:new()
  bot:Reply("Hello from Lua")
  return task.Normal
end

return task.Fail

4. Reload and test

;reload
;hello

Why this layout matters

  • command metadata lives in config, not buried in code
  • the plugin code stays small and focused
  • help output stays aligned with the actual command surface

Configuration Style Guide

These conventions make robots easier to upgrade and easier for teammates to understand.

Prefer small custom deltas

Let the engine’s shipped defaults stay authoritative. In custom config, override only what your robot truly needs:

  • enable or disable features
  • add local parameters and secrets
  • add local commands
  • intentionally redefine behavior

Keep transport details local to connectors

Put connector-specific identity and credential data in custom/conf/protocols/<protocol>.yaml, not in top-level robot config.

Treat usernames as canonical

The engine’s security and policy decisions are username-based. Make connector mappings resolve to the same canonical usernames that exist in UserRoster.

Prefer environments over ad-hoc branching logic

Use custom/conf/environments/<environment>.yaml and GOPHER_ENVIRONMENT to express development, staging, and production differences cleanly.

Writing Good Help Text

Help in v3 is command-linked, so good help starts with good command metadata.

  • Usage: the command body only
  • Summary: one short sentence
  • Examples: a few realistic examples
  • Keywords: the search terms users will actually try

Example

Commands:
- Command: deploy
  Regex: '(?i:deploy ([A-Za-z0-9._/-]+))'
  Usage: "deploy <branch>"
  Summary: "deploy the named git branch to the selected environment"
  Examples:
  - "(alias) deploy main"
  - "(alias) deploy release/2026-03-13"
  Keywords: [ "deploy", "release", "ship" ]

Good habits

  • Use placeholders in examples, not your real bot name.
  • Keep Usage short and command-line-like.
  • Put the detailed explanation in follow-up help or normal prose, not inside Usage.
  • If a command has a common invalid form, handle it deliberately and reply with the correct syntax.

Local Authoring Workflow

The normal v3 authoring loop looks like this:

  1. run the engine from the robot home
  2. connect over the local SSH connector
  3. edit files under custom/
  4. issue reload
  5. test the command again

Example loop

cd ~/robots/acme
/opt/gopherbot/gopherbot

In another terminal:

ssh -i /opt/gopherbot/resources/ssh-default/alice.key -p 4221 alice@localhost

Then:

  • edit custom/conf/plugins/hello.yaml
  • edit custom/plugins/hello.lua
  • run ;reload
  • run ;hello

That is the workflow the rest of this manual assumes.

CLI Tools for Authors

The gopherbot binary is also a CLI utility. The most useful commands for authors and operators are:

  • gopherbot --help
  • gopherbot encrypt
  • gopherbot decrypt
  • gopherbot validate <path>
  • gopherbot dump installed <path>
  • gopherbot dump configured <path>
  • gopherbot list
  • gopherbot fetch <key>
  • gopherbot version

Examples

Encrypt a secret:

gopherbot encrypt MyLousyPassword

Validate a robot repository:

gopherbot validate ~/robots/acme/custom

Dump the final merged robot config:

gopherbot dump configured robot.yaml

These commands are especially useful when configuration reload fails and you want to understand what the engine thinks the world looks like.

Secrets and Parameters

Gopherbot gives you two main ways to supply sensitive values:

  • encrypted values embedded in config with {{ decrypt "..." }}
  • parameters passed to plugins, jobs, tasks, or namespaces

Encrypt a value

gopherbot encrypt foobarbaz

Then use the returned blob in config:

Parameters:
- Name: API_TOKEN
  Value: {{ decrypt "<encrypted-value>" }}

Secure parameter handling

When SecureParameters: true is enabled, tasks may need to fetch secrets with GetParameter(...) instead of reading them directly from the process environment.

That is the safer v3 posture and the one new robots should expect.

Pipelines, Jobs, and Tasks

Pipelines are the execution model behind most non-trivial Gopherbot automation.

Whenever a plugin or job starts work, the engine manages up to three related stages:

  • the primary stage for the main work
  • the final stage for cleanup that should always run
  • the fail stage for failure-specific handling

Mental model

  • plugins are the interactive entry points
  • jobs are scheduled or triggered entry points
  • tasks are the reusable worker units inside those flows

The Robot API lets plugins and jobs build pipelines dynamically, which is why Gopherbot can express CI/CD-like behavior without forcing you into a giant static YAML pipeline language.

Primary Pipelines

The primary stage is where the real work happens.

Key behaviors

  • tasks run in sequence
  • a failure stops normal primary-stage execution
  • plugins often begin as a single task and then add more work dynamically

Important API calls

  • AddTask
  • AddJob
  • AddCommand
  • SetParameter
  • SetWorkingDirectory

v3 nuance

  • AddJob starts a child pipeline
  • AddCommand stays inside the current pipeline and is not a fake new inbound chat message
  • tasks added during execution can run before later originally queued tasks, which is how setup tasks can expand into more detailed work on the fly

Final Pipelines

Final tasks always run after the primary stage, whether the primary stage succeeded or failed.

Ordering

Final tasks run in reverse order of addition. Think of them as a cleanup stack.

That behavior is intentional because it lets you pair setup and teardown naturally:

  • start an ssh-agent
  • later stop that same agent in a final task

Good use cases

  • cleanup
  • closing sessions
  • deleting temporary state
  • sending a final summary after work is complete

Fail Pipelines

Fail tasks run only when the primary stage ends in failure.

Ordering

Unlike final tasks, fail tasks run in the order they were added.

Good use cases

  • send an explicit failure notification
  • gather debugging information
  • alert another system
  • leave breadcrumbs for an operator before cleanup runs

Task Environment

Every task sees a constructed environment, not the raw shell environment of the engine process.

Sources of task values

From lower priority to higher priority, values can come from:

  • namespaces
  • task, plugin, or job parameters
  • pipeline parameters
  • SetParameter(...) during pipeline execution

Practical rules

  • jobs seed pipeline parameters naturally
  • plugins usually have to publish values into the pipeline explicitly with SetParameter(...)
  • secure parameters may need to be read with GetParameter(...)

The safe mental model is: tasks run with the environment the engine intentionally assembles for them, not whatever happened to be exported in the parent shell.

Included Tasks

Gopherbot ships with a small set of useful stock tasks. Common examples include:

  • send-message
  • restart-robot
  • robot-quit
  • pause
  • pause-brain
  • resume-brain
  • rotate-log
  • tail-log
  • ssh-agent
  • ssh-git-helper
  • git-command
  • email-log

These tasks are useful both directly and as examples of how the pipeline model is meant to be used.

Configuration Reference

Gopherbot configuration is built from two layers:

  • installed defaults from the engine
  • custom overrides from your robot

The modern v3 layout is intentionally more structured than older versions:

  • custom/conf/robot.yaml for robot-wide policy
  • custom/conf/environments/ for environment-specific defaults
  • custom/conf/protocols/ for connector-local config
  • custom/conf/brains/ and custom/conf/history/ for provider settings
  • custom/conf/plugins/ and custom/conf/jobs/ for plugin and job metadata

This structure is the backbone of the v3 manual because it is also the backbone of the runtime.

Start with the robot.yaml reference when you need to know where a top-level option belongs or what a robot-wide setting does.

Configuration Overview

Gopherbot configuration is layered. The installed engine ships working defaults, and your robot repository supplies the small set of changes that make the robot yours.

The most important rule is:

  • robot.yaml selects robot-wide behavior and declares extensions
  • dedicated files hold detailed connector, provider, plugin, and job configuration
  • custom files override shipped defaults

File Layout

For a custom robot, configuration normally lives under custom/conf/:

custom/conf/
  robot.yaml
  environments/
    development.yaml
    production.yaml
  protocols/
    ssh.yaml
    slack.yaml
  brains/
    file.yaml
  history/
    file.yaml
  queues/
    gcloud.yaml
  plugins/
    hello.yaml
  jobs/
    nightly-report.yaml
  variables/
    common.yaml
    production.yaml

What Goes Where

Robot-wide selectors and policy live in robot.yaml.

Examples:

  • PrimaryProtocol
  • DefaultProtocol
  • SecondaryProtocols
  • Brain
  • HistoryProvider
  • QueueProviders
  • DefaultMessageFormat
  • AdminUsers
  • UserRoster

See the robot.yaml reference for the full list of top-level options.

Detailed config lives in dedicated files:

  • connector config: conf/protocols/<protocol>.yaml
  • brain config: conf/brains/<provider>.yaml
  • history config: conf/history/<provider>.yaml
  • queue config: conf/queues/<provider>.yaml
  • plugin config: conf/plugins/<plugin>.yaml
  • job config: conf/jobs/<job>.yaml

See the plugin config reference for conf/plugins/<plugin>.yaml.

This split keeps boundaries clean:

  • transport concerns stay with connectors
  • provider concerns stay with providers
  • command and help metadata stays with the plugin that owns it
  • robot-wide authorization and identity policy stays visible at the top level

Loading Order

Gopherbot loads most configuration in two layers:

  1. installed defaults from the engine tree
  2. custom overrides from the robot home

For example, when Gopherbot loads conf/plugins/ping.yaml, it first loads the installed conf/plugins/ping.yaml, then overlays custom/conf/plugins/ping.yaml when that custom file exists.

This same pattern applies to robot.yaml, protocols, providers, plugins, and jobs.

Merge Behavior

  • maps merge recursively
  • scalar values override
  • arrays replace by default
  • arrays append only when the custom key uses the Append* prefix

That merge model is why some connector-local identity data uses lists instead of maps: lists replace cleanly, which prevents accidental inheritance of unwanted defaults.

Appending to Arrays

By default, a custom array replaces the shipped default array:

UserRoster:
- UserName: alice
  Email: alice@example.com

If you want to append to an existing array instead, prefix the key with Append:

AppendUserRoster:
- UserName: bob
  Email: bob@example.com

During merge, Gopherbot strips the Append prefix and appends the new array entries to the existing UserRoster.

This works for array-valued keys throughout the config tree, such as:

  • AppendUserRoster
  • AppendAdminUsers
  • AppendCommands
  • AppendAllowedPrivateCommands
  • AppendScheduledJobs

Use Append* deliberately. Replacing arrays is often safer for security-sensitive lists because it prevents accidentally inheriting default users, channels, commands, or provider entries.

Implementation detail that matters for troubleshooting: Append only changes merge behavior when the existing value and the new value are both arrays. For maps, nested maps merge recursively whether or not the key starts with Append; for scalar values, the custom value replaces the default.

Template Expansion

Config files are Go text templates before they are parsed as YAML. This applies to robot.yaml, included environment files, protocol files, provider files, plugin files, and job files.

See Config Templates for the helper reference, examples, and secret/variable guidance.

Example

{{ $environment := env "GOPHER_ENVIRONMENT" | default "development" }}
{{ printf "environments/%s.yaml" $environment | .Include }}

That is the standard v3 pattern for selecting environment-specific defaults in a scaffolded robot.

robot.yaml Reference

robot.yaml is the top-level configuration file for a robot. It chooses the robot’s global runtime behavior: which connectors start, which brain and history providers are used, who the robot knows about, who can administer it, and which plugins, jobs, tasks, namespaces, and parameter sets are available.

Most detailed configuration does not belong in robot.yaml. In v3, robot.yaml normally contains selectors and robot-wide policy, while detailed provider and extension settings live in dedicated files:

  • connector settings: conf/protocols/<protocol>.yaml
  • brain settings: conf/brains/<provider>.yaml
  • history settings: conf/history/<provider>.yaml
  • queue settings: conf/queues/<provider>.yaml
  • plugin settings: conf/plugins/<plugin>.yaml
  • job settings: conf/jobs/<job>.yaml

For a custom robot, the file is usually custom/conf/robot.yaml inside the robot repository. Gopherbot first loads the installed defaults from the engine’s conf/robot.yaml, then merges the custom robot’s conf/robot.yaml over those defaults.

Quick Example

This is the shape of a small custom robot.yaml:

IgnoreUnlistedUsers: true
SecureParameters: true

{{ $environment := env "GOPHER_ENVIRONMENT" | default "production" }}
{{ printf "environments/%s.yaml" $environment | .Include }}

BotInfo:
  UserName: floyd
  Email: floyd@example.com
  FullName: Floyd Gopherbot
  FirstName: Floyd
  LastName: Gopherbot

Alias: ";"

DefaultMessageFormat: BasicMarkdown
DefaultJobChannel: general
HistoryProvider: file

TimeOuts:
  Plugin:
    Warn: 7m
    Kill: 14m
  Job:
    Warn: 1h
    Kill: 2h

AdminUsers:
- alice

UserRoster:
- UserName: alice
  Email: alice@example.com
  FullName: Alice Admin

ScheduledJobs:
- Name: pause-notifies
  Schedule: "0 0 8 * * *"

The included environment file commonly sets PrimaryProtocol, DefaultProtocol, Brain, and logging for a specific environment.

Loading and Merge Rules

Like other Gopherbot config files, robot.yaml is expanded as a template before it is parsed as YAML. See Config Templates for the common helper reference, include behavior, and secret/variable guidance.

See Configuration Overview for the full loading, merge, override, and Append* behavior.

Unknown top-level keys are startup errors. BrainConfig, HistoryConfig, and QueueConfig are also rejected in robot.yaml; put those in their dedicated provider files.

Protocols and Routing

PrimaryProtocol

Required.

PrimaryProtocol is the main connector protocol for the robot.

PrimaryProtocol: ssh

The engine normalizes protocol names by trimming whitespace and lowercasing them. The primary protocol must have a matching connector config file at conf/protocols/<protocol>.yaml, and that file must contain a ProtocolConfig section.

Common values include:

  • ssh
  • slack
  • googlechat
  • terminal
  • test
  • nullconn

The terminal connector is useful for local work, but it is not supported as a secondary protocol.

During reload, changing PrimaryProtocol is ignored once a connector runtime is already active. Restart the robot to change the primary protocol.

DefaultProtocol

Optional. Defaults to PrimaryProtocol.

DefaultProtocol is used when the robot sends an outbound message without an incoming message context, such as plugin initialization or scheduled work.

DefaultProtocol: ssh

If set, it must be either the primary protocol or one of SecondaryProtocols. If it is not, Gopherbot logs a warning and falls back to the primary protocol.

SecondaryProtocols

Optional.

SecondaryProtocols lists additional connector protocols to initialize and run alongside the primary protocol.

SecondaryProtocols:
- slack
- googlechat

Each secondary protocol should have conf/protocols/<protocol>.yaml. Secondary connector startup failures are logged, but they do not stop the primary connector from running.

Rules:

  • duplicates are ignored
  • the primary protocol is ignored if repeated here
  • terminal is ignored as a secondary protocol
  • protocol names are normalized internally

ChannelRoster

Optional.

ChannelRoster maps channel names to protocol channel IDs when a connector cannot provide a stable human-readable name, or when you want configuration to refer to a friendly channel name.

ChannelRoster:
- ChannelName: general
  ChannelID: C123456

Fields:

  • ChannelName: name used in robot config
  • ChannelID: connector’s internal channel ID

Top-level ChannelRoster entries apply to the primary protocol. Protocol files can also provide ChannelRoster; those entries are tagged with that protocol as they load. Primary-protocol entries from robot.yaml are appended after the primary protocol file so robot-specific channel mappings can override protocol-specific lookup maps.

JoinChannels

Optional.

JoinChannels lists channels the robot should try to join during startup.

JoinChannels:
- general
- operations

Gopherbot also tries to join channels from DefaultChannels and DefaultJobChannel. Not every connector supports joining channels programmatically.

DefaultChannels

Optional.

DefaultChannels defines the channels where plugins are available when the plugin’s own config does not specify Channels and does not set AllChannels.

DefaultChannels:
- general
- random

Plugin-specific channel configuration belongs in conf/plugins/<plugin>.yaml.

Robot Identity and Addressing

BotInfo

Recommended.

BotInfo defines the robot’s canonical identity. Connectors receive this information during initialization, and plugin APIs use it for bot attributes.

BotInfo:
  UserName: floyd
  Email: floyd@example.com
  FullName: Floyd Gopherbot
  FirstName: Floyd
  LastName: Gopherbot

Fields:

  • UserName: the name users type when addressing the robot, such as floyd, ping
  • Email: used by email-sending APIs as the sender address
  • FullName: human-readable robot name
  • FirstName: optional first name attribute
  • LastName: optional last name attribute

GetBotAttribute("name") returns BotInfo.UserName. GetBotAttribute("email") returns BotInfo.Email. GetBotAttribute("fullName") returns BotInfo.FullName.

Alias

Optional.

Alias is a one-character shortcut for addressing the robot.

Alias: ";"

With this example, users can say ;ping instead of floyd, ping.

Allowed alias characters are:

* + ^ $ ? \ [ ] { } & ! ; : - % # @ ~ < >

Only the first rune of the string is used. If the alias is invalid, startup fails.

HearSelf

Optional. Defaults to true.

HearSelf controls whether the engine processes messages that a connector marks as originating from the robot itself.

HearSelf: false

Connectors still own the decision to mark a message as a self-message. This option only controls whether the engine ignores those marked messages.

Users and Access Policy

UserRoster

Optional, but strongly recommended.

UserRoster is the global user directory. Gopherbot’s authorization and policy decisions are based on canonical usernames from this directory, not on connector-specific transport IDs.

UserRoster:
- UserName: alice
  Email: alice@example.com
  FullName: Alice Admin
  FirstName: Alice
  LastName: Admin
  Phone: "+1-555-0100"
- UserName: buildbot
  BotUser: true

Fields:

  • UserName: canonical username used in config, authorization, memory ownership, and policy checks
  • Email: user email returned by attribute APIs
  • Phone: user phone returned by attribute APIs
  • FullName: full display name returned by attribute APIs
  • FirstName: first name returned by attribute APIs
  • LastName: last name returned by attribute APIs
  • BotUser: marks an automated user; bot users are not checked against ambient message matchers and do not fall through to catch-all plugins
  • UserID: legacy parse-only field; accepted so older configs can load, but ignored by the engine

UserName must be lowercase. Entries with an empty username or uppercase letters are ignored.

Connector-specific user IDs do not belong in top-level UserRoster. Put protocol-local identity mapping in the connector’s config file, such as conf/protocols/slack.yaml, conf/protocols/googlechat.yaml, or conf/protocols/ssh.yaml.

IgnoreUnlistedUsers

Optional. Defaults to false unless set by custom config.

When IgnoreUnlistedUsers is true, Gopherbot drops incoming messages unless the connector has validated the transport user and resolved it to a canonical username listed in UserRoster.

IgnoreUnlistedUsers: true

This is the large security switch for roster-based deployments. It is enforced before any worker or pipeline is created.

IgnoreUsers

Optional.

IgnoreUsers lists canonical usernames the robot should never respond to.

IgnoreUsers:
- otherbot
- slackbot

The comparison is case-insensitive and happens before any pipeline is created.

AdminUsers

Optional. Defaults to an empty list if not configured.

AdminUsers lists canonical usernames with access to built-in administrative commands.

AdminUsers:
- alice
- david

Admin checks are username-based. Connector-provided flags, message content, and user-modifiable runtime state do not make a user an admin.

Scheduled jobs run as automatic tasks and are treated as admin by design because they are scheduled by configuration.

DefaultAuthorizer

Optional.

DefaultAuthorizer names the authorizer plugin used when a plugin or job requires authorization but does not specify its own Authorizer.

DefaultAuthorizer: groups

Plugin and job authorization rules live in conf/plugins/<plugin>.yaml and conf/jobs/<job>.yaml.

DefaultElevator

Optional.

DefaultElevator names the elevation plugin used when a plugin command requires elevation but does not specify its own Elevator.

DefaultElevator: builtin-userapproval

Elevation is checked after base authorization succeeds. For the built-in human approval elevator, see User Approval Elevation.

Brain, History, and Queues

Brain

Optional. Defaults to mem if unset at brain initialization, though installed defaults normally set a value.

Brain selects the provider used for long-term robot memory.

Brain: file

Common values:

  • mem: in-memory brain
  • file: local cached file brain
  • dynamo: DynamoDB-backed remote brain
  • cloudflare: Cloudflare KV-backed remote brain
  • firestore: Firestore-backed remote brain

Provider-specific settings belong in conf/brains/<provider>.yaml under BrainConfig.

For file, Gopherbot uses the v3 local brain cache. If legacy file-brain data is present in the old brain directory and the v3 cache has not been initialized, startup asks you to run gopherbot pull-brain.

BrainCache

Optional.

BrainCache configures the engine-owned local cache used by the v3 brain system.

BrainCache:
  Directory: state/brain-cache

Fields:

  • Directory: directory for local brain cache state

If omitted, the directory defaults to state/brain-cache. Installed defaults normally derive this from GOPHER_STATE_DIRECTORY.

HistoryProvider

Optional. Defaults to mem.

HistoryProvider selects where plugin and job run logs are stored.

HistoryProvider: file

Common values:

  • mem: in-memory history
  • file: file-backed history

Provider-specific settings belong in conf/history/<provider>.yaml under HistoryConfig.

If the configured provider is not registered or returns nil during startup, Gopherbot logs an error and falls back to mem.

QueueProviders

Optional.

QueueProviders lists queue providers that should start after full robot initialization.

QueueProviders:
- gcloud

Provider names are trimmed, lowercased, and deduplicated. Provider-specific settings belong in conf/queues/<provider>.yaml under QueueConfig.

Queue provider startup failures are logged per provider and do not stop the robot from running. Jobs opt in to queue triggering with job-level UUIDTrigger in conf/jobs/<job>.yaml.

Messages and Formatting

DefaultMessageFormat

Optional. Defaults to BasicMarkdown.

DefaultMessageFormat controls how outgoing messages are formatted when a plugin, job, or built-in send path does not choose a format explicitly.

DefaultMessageFormat: BasicMarkdown

Supported values:

  • BasicMarkdown
  • Raw
  • Fixed
  • Variable

BasicMarkdown is the v3 default portable format. Raw preserves protocol-native behavior for older robots that need it. Unknown values log an error and fall back to BasicMarkdown.

DefaultJobChannel

Optional, but recommended.

DefaultJobChannel is the default channel for job status messages and other operator-facing output when a job or alert does not specify a channel.

DefaultJobChannel: general

Jobs without their own Channel and without a DefaultJobChannel can be disabled during config loading because Gopherbot has nowhere to report their activity.

ReadyMessage

Optional.

ReadyMessage sends a channel message after startup readiness opens.

ReadyMessage: "floyd is ready"

If unset or blank, no ready message is sent.

ReadyChannel

Optional. Defaults to DefaultJobChannel.

ReadyChannel chooses where ReadyMessage is sent.

ReadyChannel: general

If ReadyMessage is configured but neither ReadyChannel nor DefaultJobChannel is available, Gopherbot logs an error and skips the message.

Scheduling and Time

ScheduledJobs

Optional.

ScheduledJobs schedules configured jobs.

ScheduledJobs:
- Name: pause-notifies
  Schedule: "0 0 8 * * *"
- Name: install-libs
  Schedule: "@init"
- Name: hello
  Schedule: "@every 30s"
  Arguments:
  - "Hello"
  - "World"

Fields:

  • Name: name of the job to run
  • Schedule: cron expression or descriptor
  • Arguments: optional arguments passed to the job
  • Command: accepted by the shared task spec, mainly useful for plugin-like task specs

Schedules use github.com/robfig/cron/v3. Gopherbot accepts seconds as an optional first field, so both five-field and six-field cron expressions can be used. Descriptors such as @every 30s and @init are supported.

@init jobs run during startup before normal plugin initialization. Scheduled jobs run as automatic tasks and bypass normal user authorization/elevation checks because they are controlled by robot configuration.

Only jobs can be scheduled. Disabled jobs are skipped.

TimeZone

Optional.

TimeZone sets the time zone used for scheduled jobs.

TimeZone: America/New_York

Use an IANA time zone name. If the value cannot be loaded, Gopherbot logs an error and uses the system default time zone.

TimeOuts

Optional.

TimeOuts sets default warning and kill thresholds for plugin and job pipelines.

TimeOuts:
  Plugin:
    Warn: 7m
    Kill: 14m
  Job:
    Warn: 1h
    Kill: 2h

Fields:

  • Plugin.Warn: how long a plugin can run before an operator warning
  • Plugin.Kill: how long a plugin can run before termination
  • Job.Warn: how long a job can run before an operator warning
  • Job.Kill: how long a job can run before termination

Durations should normally be quoted or unquoted Go duration strings such as 30s, 7m, or 1h. Integer values are accepted as nanoseconds.

Rules:

  • negative durations are invalid
  • if both Warn and Kill are set, Kill must be greater than Warn
  • zero disables that threshold

Plugin, job, and task configs can override these defaults with their own TimeOuts block.

Plugins, Jobs, Tasks, Namespaces, and Parameters

robot.yaml declares which extensions exist. Detailed plugin and job behavior should usually live in conf/plugins/<plugin>.yaml and conf/jobs/<job>.yaml.

The root maps in this section all use the map key as the extension or set name:

ExternalPlugins:
  "my-plugin":
    Path: plugins/my-plugin.gsh
    Description: My custom command

Common Declaration Fields

These fields are used in ExternalPlugins, ExternalJobs, ExternalTasks, GoPlugins, GoJobs, GoTasks, NameSpaces, and ParameterSets. Not every field is meaningful for every map.

  • Path: executable or source path for external extensions
  • Description: human-readable description
  • NameSpace: namespace for shared memory and parameters
  • ParameterSets: list of named parameter sets to attach
  • Disabled: disables the entry
  • Homed: runs with the robot home as the working context
  • Privileged: privilege setting for the extension
  • Parameters: fixed parameters made available to the extension

Parameter item shape:

Parameters:
- Name: WEATHER_API_KEY
  Value: '{{ secret "WEATHER_API_KEY" }}'

Security note: with SecureParameters: true, extensions must retrieve parameters through the Robot API instead of receiving them as environment variables.

ExternalPlugins

Optional.

ExternalPlugins declares executable or interpreted plugins that can respond to user commands.

ExternalPlugins:
  "admin":
    Path: plugins/admin.gsh
    Description: Administrative plugin
    Privileged: true

External plugins default to Privileged: false when Privileged is omitted.

Plugin command matchers, channel restrictions, help metadata, authorization, elevation, and plugin-specific config usually belong in conf/plugins/<plugin>.yaml.

GoPlugins

Optional.

GoPlugins declares compiled Go plugins registered with the engine.

GoPlugins:
  "help":
    Description: Help plugin

Go plugins default to Privileged: false when Privileged is omitted.

ExternalJobs

Optional.

ExternalJobs declares executable or interpreted jobs.

ExternalJobs:
  "nightly-report":
    Path: jobs/nightly-report.gsh
    Description: Build the nightly report

External jobs default to Privileged: true when Privileged is omitted.

Job triggers, channel, arguments, quiet/log settings, and job-specific config usually belong in conf/jobs/<job>.yaml.

GoJobs

Optional.

GoJobs declares compiled Go jobs registered with the engine.

GoJobs:
  "go-bootstrap":
    Description: Bootstrap a custom robot repository

Go jobs default to Privileged: true when Privileged is omitted.

ExternalTasks

Optional.

ExternalTasks declares reusable pipeline tasks. Tasks are not command starters by themselves; plugins and jobs can add them to a pipeline.

ExternalTasks:
  "render-report":
    Path: tasks/render-report.gsh
    Description: Render a report artifact

External tasks default to Privileged: false when Privileged is omitted. A privileged task cannot be added to an unprivileged pipeline.

GoTasks

Optional.

GoTasks declares compiled Go tasks registered with the engine.

GoTasks:
  "ssh-agent":
    Description: Prepare SSH agent state

Go task entries pass through even when disabled because compiled tasks are enabled by default and may need an explicit Disabled: true.

NameSpaces

Optional.

NameSpaces declares shared namespaces for memory and parameters.

NameSpaces:
  "deploy":
    Description: Shared deployment parameters
    Parameters:
    - Name: DEPLOY_ENV
      Value: production

An extension can use NameSpace: deploy to share long-term memories and parameters with other extensions using the same namespace.

ParameterSets

Optional.

ParameterSets declares named sets of parameters that can be attached to plugins, jobs, tasks, or identity providers.

ParameterSets:
  "github_oauth":
    Description: GitHub OAuth client credentials
    Parameters:
    - Name: CLIENT_ID
      Value: '{{ variable "GITHUB_CLIENT_ID" }}'
    - Name: CLIENT_SECRET
      Value: '{{ secret "GITHUB_CLIENT_SECRET" }}'

Attach a set to an extension with ParameterSets:

ExternalPlugins:
  "github-link":
    ParameterSets:
    - github_oauth

Identity Providers

IdentityProviders

Optional.

IdentityProviders configures providers used by the GetIdentityCredential Robot API for user-linked credentials and refresh behavior.

IdentityProviders:
  github:
    Type: oauth2
    CredentialParameterSet: github_oauth
    OAuth2:
      TokenEndpointAuthMethod: client_secret_post
      Token:
        URL: https://github.com/login/oauth/access_token
        Headers:
          Accept: application/json

Provider keys are trimmed and lowercased.

Fields:

  • Type: provider type. Current user-linked refresh support is OAuth2-oriented.
  • CredentialParameterSet: name of a ParameterSets entry containing provider credentials such as CLIENT_ID and CLIENT_SECRET
  • OAuth2: OAuth2 refresh configuration

OAuth2 fields:

  • TokenEndpointAuthMethod: token endpoint auth method, such as client_secret_post
  • Token.URL: token endpoint URL
  • Token.Headers: headers sent to the token endpoint
  • Token.Parameters: parameters sent to the token endpoint
  • Token.RefreshParameters: additional parameters used during refresh

If an identity provider references a missing parameter set, Gopherbot logs an error. The provider is still loaded into the registry, but API calls can fail later if required settings are missing.

Secret boundary: identity-provider secrets should be supplied through the referenced ParameterSets. Unprivileged extensions do not get broad access to provider registries or shared secret-bearing configuration.

Logging, HTTP, and Runtime Directories

LocalPort

Optional. Defaults to 0.

LocalPort controls the localhost HTTP/JSON API listener used by external plugins.

LocalPort: 0

0 means choose the first available port. The listener binds to 127.0.0.1:<LocalPort>. CLI-only operations do not start this listener.

HttpDebug

Optional. Defaults to false.

HttpDebug enables debug logging for local HTTP JSON API requests and replies.

HttpDebug: true

Use this only for low-level debugging. It does not scrub secrets. Installed defaults also force debug-friendly logging when GOPHER_HTTP_DEBUG=true.

LogDest

Optional.

LogDest chooses where the robot log is written.

LogDest: robot.log

Common values:

  • stdout
  • stderr
  • a filename such as robot.log

Installed defaults avoid logging terminal-connector UI output to stdout.

LogLevel

Optional.

LogLevel sets the initial log level.

LogLevel: info

Recognized values:

  • trace
  • debug
  • info
  • audit
  • warn
  • error

Unknown values resolve to error. Runtime admin commands can change the log level after startup.

WorkSpace

Optional.

WorkSpace is the read/write directory the robot uses for work.

WorkSpace: workspace

If the directory can be created or opened, Gopherbot uses it as the workspace. If it cannot, Gopherbot logs an error and uses the robot config directory instead.

Email

MailConfig

Optional.

MailConfig configures SMTP delivery for the Robot email APIs.

MailConfig:
  Mailhost: smtp.example.com:25
  Authtype: plain
  User: floyd
  Password: '{{ secret "SMTP_PASSWORD" }}'

Fields:

  • Mailhost: SMTP host, normally with port
  • Authtype: plain for SMTP plain auth; any other value sends without auth
  • User: SMTP username for plain
  • Password: SMTP password for plain

The robot’s sender address comes from BotInfo.Email. User recipient addresses come from UserRoster, protocol-provided attributes, or explicit email addresses supplied by the caller.

AdminContact

Optional.

AdminContact is informational contact text exposed through bot attributes and status/help paths.

AdminContact: "Ops Team <ops@example.com>"

Legacy Email and Name Keys

Email and Name are accepted as top-level keys by the v3 loader for compatibility, but they are not applied to runtime robot identity. Use BotInfo.Email and BotInfo.UserName instead.

Security and Secrets

SecureParameters

Optional. Defaults to false unless set by custom config.

When SecureParameters is true, Gopherbot does not publish configured parameters as environment variables to jobs, plugins, and tasks. Extensions must use the Robot API to retrieve parameters explicitly.

SecureParameters: true

This is recommended for robots that handle secrets.

EncryptionKey

Legacy/compatibility option.

EncryptionKey is an older config path for unlocking the real encryption key from the brain.

EncryptionKey: "a-string-at-least-32-bytes-long"

Modern v3 robots should prefer GOPHER_ENCRYPTION_KEY plus the binary encrypted key file generated by gopherbot genkey or startup initialization. During config dumps, Gopherbot masks this value as XXXXXX.

Variables and Secrets Files

Do not put raw shared secrets directly in robot.yaml. Put encrypted secrets and plaintext variables in:

  • custom/conf/variables/common.yaml
  • custom/conf/variables/<environment>.yaml

Example:

Secrets:
  SMTP_PASSWORD: "<encrypted-base64-value>"
Variables:
  GITHUB_CLIENT_ID: "client-id"

Then reference them from config templates:

Password: '{{ secret "SMTP_PASSWORD" }}'
Value: '{{ variable "GITHUB_CLIENT_ID" }}'

Environment-specific values override common values.

Provider and Connector Config That Must Not Be in robot.yaml

These top-level keys are invalid in robot.yaml:

  • BrainConfig
  • HistoryConfig
  • QueueConfig

Put them in:

  • conf/brains/<provider>.yaml
  • conf/history/<provider>.yaml
  • conf/queues/<provider>.yaml

Connector ProtocolConfig also belongs in conf/protocols/<protocol>.yaml, not in robot.yaml.

Top-level UserMap is specifically rejected from protocol files in v3. Connector-local identity mapping must live under that connector’s ProtocolConfig, using the fields documented for that connector, such as Slack and Google Chat ProtocolConfig.UserMap or SSH ProtocolConfig.UserKeys.

Complete Top-Level Key Index

This index lists the top-level keys accepted by the v3 robot.yaml loader.

KeyPurpose
AdminContactInformational robot administrator contact
AdminUsersCanonical usernames with admin access
AliasOne-character command-addressing shortcut
BotInfoRobot identity
BrainBrain provider selector
BrainCacheLocal v3 brain cache settings
ChannelRosterChannel name/ID mappings
DefaultAuthorizerDefault authorization plugin
DefaultChannelsDefault plugin channels
DefaultElevatorDefault elevation plugin
DefaultJobChannelDefault channel for job/operator output
DefaultMessageFormatDefault outgoing message format
DefaultProtocolProtocol for context-free outbound messages
EmailLegacy accepted key; use BotInfo.Email
EncryptionKeyLegacy encryption unlock setting
ExternalJobsExternal job declarations
ExternalPluginsExternal plugin declarations
ExternalTasksExternal pipeline task declarations
GoJobsCompiled Go job declarations
GoPluginsCompiled Go plugin declarations
GoTasksCompiled Go task declarations
HearSelfProcess or ignore connector-marked self messages
HistoryProviderHistory provider selector
HttpDebugDebug local HTTP API traffic
IdentityProvidersUser-linked identity provider registry
IgnoreUnlistedUsersDrop unvalidated/unlisted users before pipelines
IgnoreUsersUsers the robot never responds to
JoinChannelsChannels to join during startup
LocalPortLocal HTTP/JSON API port
LogDestLog destination
LogLevelInitial log level
MailConfigSMTP settings
NameLegacy accepted key; use BotInfo.UserName
NameSpacesShared memory/parameter namespaces
ParameterSetsReusable named parameter sets
PrimaryProtocolRequired primary connector protocol
QueueProvidersQueue provider selectors
ReadyChannelChannel for startup ready message
ReadyMessageOptional message after startup readiness
ScheduledJobsJob schedules
SecondaryProtocolsAdditional connector protocols
SecureParametersRequire API parameter retrieval instead of env vars
TimeOutsDefault plugin/job timeout thresholds
TimeZoneTime zone for scheduled jobs
UserRosterCanonical user directory
WorkSpaceRobot working directory

plugins/<plugin>.yaml Reference

conf/plugins/<plugin>.yaml configures how a plugin is matched, where it is visible, what policy applies to its commands, and what plugin-specific Config data the plugin can read at runtime.

It does not declare that the plugin exists. Plugin declarations live in robot.yaml under ExternalPlugins or GoPlugins.

ExternalPlugins:
  "hello":
    Description: Example hello-world plugin
    Path: plugins/hello.lua

After a plugin is declared, put command matchers and plugin behavior in:

custom/conf/plugins/hello.yaml

Quick Example

Channels:
- general
- operations

AllowedPrivateCommands:
- status

Commands:
- Command: status
  SimpleMatcher: "service status <service:ident>"
  Usage: "service status <service>"
  Summary: "show current status for a named service"
  Examples:
  - "(alias) service status api"
  Keywords: [ "service", "status", "health" ]

Config:
  DefaultRegion: us-east-1

The plugin handler receives status as the command and api as the first argument.

Loading and Merge Rules

Plugin config is loaded from:

conf/plugins/<plugin>.yaml

Gopherbot loads installed plugin defaults first, then merges custom robot overrides. Maps merge recursively, arrays replace by default, and arrays append only when the key starts with Append.

Plugin config files are templates like the rest of the config tree. See Config Templates.

For compiled Go plugins and interpreter-backed plugins that provide a default configuration through their configure hook, that default is also part of the plugin’s base config before installed and custom YAML overrides are applied.

Unknown top-level keys disable the plugin at load time. Some declaration keys are valid in robot.yaml but intentionally invalid or ignored in conf/plugins/<plugin>.yaml; see What Belongs in robot.yaml.

What Belongs in robot.yaml

Put plugin declaration and execution identity in robot.yaml, not in conf/plugins/<plugin>.yaml.

Use robot.yaml for:

  • Path
  • Description
  • NameSpace
  • ParameterSets
  • Parameters
  • Homed
  • Privileged
  • Disabled when you want to disable a plugin before plugin config loading

Use conf/plugins/<plugin>.yaml for:

  • command matchers
  • channel visibility
  • user/admin/authorization/elevation policy
  • private-command policy
  • catch-all behavior
  • reply matchers
  • timeout overrides
  • plugin-specific Config

Important implementation details:

  • Privileged is illegal in conf/plugins/<plugin>.yaml; set it in robot.yaml.
  • NameSpace and ParameterSets in plugin YAML are logged and ignored; set them in robot.yaml.
  • Path, Description, Parameters, and Homed belong in robot.yaml. They are not useful plugin-YAML overrides.

Command Matching

Commands

Commands are directed commands. They are checked when the user addresses the robot by name, alias, DM, slash/private route, or another connector-owned bot-message path.

Commands:
- Command: ping
  SimpleMatcher: "ping"
  Usage: "ping"
  Summary: "see if the bot is alive"
  Examples:
  - "(alias) ping"
  Keywords: [ "ping" ]

Each command matcher must have:

  • Command: the command token passed to the plugin
  • exactly one of Regex or SimpleMatcher

Command names must not start with _. Leading underscore commands are reserved for engine lifecycle callbacks such as _init, _configure, _authorize, _usergroups, _elevate, _catchall, _subscribed, and _expiresub.

Regex

Regex is a Go regular expression for the command body after the robot addressing prefix has been removed.

Commands:
- Command: deploy
  Regex: '(?i:deploy ([A-Za-z0-9._/-]+))'

For Commands, Gopherbot wraps the regex with leading/trailing whitespace tolerance and anchors it as an exact match. Do not add surrounding ^ and $ unless you specifically intend to anchor inside the command body.

Capture groups become positional arguments to the plugin.

SimpleMatcher

SimpleMatcher is the preferred syntax for common directed commands.

Commands:
- Command: loglevel
  SimpleMatcher: "set log level {to} (level:trace|debug|info|warn|error)"

Simple matchers are case-insensitive, tolerate extra whitespace, and treat spaces in the spec as matching either spaces or dashes in user input.

Common forms:

  • literal text: required non-capturing words
  • {optional noise}: optional non-capturing words
  • /start|begin/: required non-capturing synonyms
  • (label:a|b|c): required capturing choice
  • [label:a|b|c]: optional capturing choice
  • <name:ident>: typed capture
  • [<name:rest>]: optional typed capture

Built-in capture types include ident, token, rest, number, decimal, bool, duration, email, url, ip, ipv4, ipv6, cidr, dnsname, slug, and base64.

SimpleMatcher is supported only for directed Commands. It is rejected for MessageMatchers, ReplyMatchers, and job argument matchers.

Command Matcher Fields

These fields are accepted inside each Commands item:

  • Command: command token passed to the plugin
  • Regex: regular expression matcher
  • SimpleMatcher: simplified matcher syntax
  • Usage: short usage text for help
  • Summary: one-sentence help summary
  • Examples: example invocations
  • Keywords: help search terms
  • ChannelOnly: match only normal channel messages, not threaded messages
  • Contexts: capture-group labels for short-term “it”/context memory

Usage, Summary, Examples, and Keywords drive the v3 help system. They do not change matching behavior.

Contexts

Contexts lets a command remember a captured value for later commands that use a generic reference such as it.

Commands:
- Command: show-ticket
  SimpleMatcher: "show ticket <ticket:slug>"
  Contexts:
  - ticket:it:that
- Command: close-ticket
  SimpleMatcher: "close ticket <ticket:slug>"
  Contexts:
  - ticket:it:that

When a user says show ticket OPS-123, the ticket context can remember OPS-123. If the next command says close ticket it, the engine can substitute the remembered value. If no value is remembered, the user is asked to be more specific.

Ambient Message Matching

MessageMatchers

MessageMatchers listen to messages that were not necessarily addressed to the robot.

MessageMatchers:
- Command: help
  Regex: '^(?i:help)$'

Fields are the same InputMatcher shape as Commands, but with two important differences:

  • SimpleMatcher is not supported
  • Gopherbot does not add anchors around MessageMatchers; include ^ and $ yourself when you want exact matching

MessageMatchers can be powerful and noisy. Use them sparingly.

AmbientMatchCommand

Optional. Defaults to false.

When AmbientMatchCommand is false, MessageMatchers are skipped for messages that were already recognized as directed robot commands.

AmbientMatchCommand: true

Set this only when the plugin should inspect both ambient messages and directed commands.

MatchUnlisted

Optional. Defaults to false.

When MatchUnlisted is false, ambient MessageMatchers are skipped for users that are not listed in the global UserRoster.

MatchUnlisted: true

This affects ambient matching only. It does not bypass IgnoreUnlistedUsers, which is a pre-pipeline gate in robot.yaml.

Channel Visibility

Channels

Channels lists the public channels where the plugin is available.

Channels:
- general
- operations

If Channels is empty, Gopherbot uses DefaultChannels from robot.yaml when that list is configured.

AllChannels

AllChannels: true makes the plugin available in every normal channel where the robot is present.

AllChannels: true

If neither plugin Channels nor robot DefaultChannels are configured, Gopherbot defaults the plugin to all channels unless AllChannels was explicitly set to false.

Channel

Channel is accepted for plugins because plugins can also be scheduled or used in pipeline contexts that need a channel.

Channel: general

For normal command visibility, use Channels or AllChannels.

Direct Messages

Plugins are generally considered available in direct messages for matching purposes. Whether a command can actually run in a private context is controlled by the private-command policy below.

Private Commands

Private contexts include direct messages and connector-supported hidden/ephemeral messages such as slash-command routes.

AllowedPrivateCommands

AllowedPrivateCommands lists commands that may run in private contexts.

AllowedPrivateCommands:
- status
- whoami

Use * to allow every command in the plugin to run privately:

AllowedPrivateCommands:
- "*"

RequiredPrivateCommands

RequiredPrivateCommands lists commands that must run in a private context.

RequiredPrivateCommands:
- dump
- token

If a required-private command is invoked publicly, the engine rejects it before plugin code runs.

RequireAllCommandsPrivate

RequireAllCommandsPrivate: true requires every command in the plugin to run privately.

RequireAllCommandsPrivate: true

RestrictPrivateChannels

RestrictPrivateChannels: true makes configured Channels an access boundary for private-capable commands.

Channels:
- security
RestrictPrivateChannels: true
AllowedPrivateCommands:
- rotate-key

Without this setting, a private-capable command can run in DMs or hidden contexts even if the plugin has public channel restrictions. With this setting:

  • DMs are rejected because a DM does not prove membership in a configured channel
  • hidden commands must come from one of the configured Channels

Use this only when channel membership is part of the access policy. For normal user authorization, prefer Users, RequireAdmin, Authorizer, or elevation.

User and Admin Policy

Users

Users is an allow-list of canonical usernames.

Users:
- alice
- bob
- ops-*

If Users is empty, all users are allowed by this setting. Entries are matched with filepath-style patterns, so ops-* can match usernames such as ops-alice.

RequireAdmin

RequireAdmin: true restricts the whole plugin to robot administrators.

RequireAdmin: true

Admins come from AdminUsers in robot.yaml, plus scheduled/automatic tasks controlled by robot configuration.

AdminCommands

AdminCommands restricts individual commands to administrators.

AdminCommands:
- clear-cache
- restart-service

Every command listed here must exist in Commands or MessageMatchers, or the plugin is disabled during config loading.

Authorization

Authorization is for policy checks delegated to another plugin, usually for group or role decisions. It runs after admin checks and private-command checks.

AuthorizedCommands

AuthorizedCommands lists commands that require authorization.

AuthorizedCommands:
- deploy
- rollback

Every command listed here must exist in Commands or MessageMatchers, or the plugin is disabled.

AuthorizeAllCommands

AuthorizeAllCommands: true requires authorization for every command in the plugin.

AuthorizeAllCommands: true

Authorizer

Authorizer overrides the robot-wide DefaultAuthorizer for this plugin.

Authorizer: groups

The authorizer must be a plugin. When a command needs authorization, Gopherbot calls the authorizer’s _authorize command with the protected plugin name, AuthRequire, the command name, and the command arguments.

If a plugin configures Authorizer but no commands actually require authorization, Gopherbot treats that as a configuration error.

AuthRequire

AuthRequire is an optional string passed to the authorizer, commonly a group or role name.

AuthRequire: production-deployers

The help system can also use an authorizer’s _usergroups support to hide or show command help based on AuthRequire.

Elevation

Elevation is an additional confirmation step, often MFA-like. It runs after authorization.

For human approval workflows, see User Approval Elevation.

ElevatedCommands

ElevatedCommands lists commands that require elevation.

ElevatedCommands:
- deploy
- restart-service

After a successful elevation, the pipeline remains elevated for its lifetime.

ElevateImmediateCommands

ElevateImmediateCommands lists commands that always require a fresh elevation prompt.

ElevateImmediateCommands:
- rotate-secret

Use this sparingly for especially sensitive commands.

Elevator

Elevator overrides the robot-wide DefaultElevator for this plugin.

Elevator: totp

The elevator must be a plugin that implements the engine _elevate callback.

Catch-All Plugins

CatchAll

CatchAll: true lets a plugin handle unmatched directed commands.

CatchAll: true

The plugin receives engine command _catchall and the full unmatched message text as the first argument.

There should usually be only one generic catch-all plugin in a location. Multiple catch-all matches can make routing ambiguous.

CatchAllModes

CatchAllModes narrows which command-addressing modes a catch-all handles.

CatchAllModes:
- name
- direct

Allowed values:

  • alias: addressed through the robot alias
  • name: addressed by robot name or mention
  • direct: sent in a DM
  • hidden: sent through hidden/ephemeral transport

Invalid values disable the plugin.

Replies and Prompt Matching

ReplyMatchers

ReplyMatchers are shared prompt/reply matchers used by plugin code that asks the user for a reply.

ReplyMatchers:
- Label: confirm
  Regex: '(?i:y(?:es)?|no?)'

Fields:

  • Label: label used by the plugin when waiting for a reply
  • Regex: Go regex used to match the reply
  • ChannelOnly: match only normal channel messages
  • Contexts: optional context labels

Label values that start with a capital letter are reserved for stock reply matchers and disable the plugin.

Plugin-Specific Config

Config

Config is free-form YAML for the plugin itself.

Config:
  DefaultRegion: us-east-1
  MaxItems: 20

Gopherbot stores this raw config and exposes it to the plugin through the Robot API. The engine validates the rest of the plugin file with known fields, but it intentionally does not validate the shape inside Config.

Config is the right place for plugin-owned options. Secrets should normally come from config template secret references or attached parameter sets, not hard-coded plaintext.

Timeouts

TimeOuts

TimeOuts overrides the robot-wide plugin timeout defaults for this plugin.

TimeOuts:
  Warn: 2m
  Kill: 5m

Fields:

  • Warn: how long the plugin can run before an operator warning
  • Kill: how long the plugin can run before termination or manual-intervention alert

Durations are Go duration strings such as 30s, 2m, or 1h. Integer values are accepted as nanoseconds. A value of 0 disables that threshold. If both thresholds are non-zero, Kill must be greater than Warn.

Disabling a Plugin

Disabled

Disabled: true disables the plugin.

Disabled: true

You can disable a plugin in robot.yaml before plugin config is loaded, or in conf/plugins/<plugin>.yaml while loading plugin-specific config.

Invalid or Legacy Keys

These keys are not valid in v3 plugin config:

  • CommandMatchers: use Commands
  • Help: use command-linked Usage, Summary, Examples, and Keywords
  • Helptext: use Summary and Examples
  • Privileged: set this in robot.yaml

These declaration fields belong in robot.yaml, not plugin YAML:

  • Path
  • Description
  • NameSpace
  • ParameterSets
  • Parameters
  • Homed

NameSpace and ParameterSets are explicitly ignored when they appear in plugin YAML. Put them in the plugin declaration in robot.yaml.

Effective Top-Level Key Index

This index lists the top-level keys that have an effect in v3 plugin config.

KeyPurpose
AdminCommandsCommands restricted to robot admins
AllChannelsMake the plugin available in all normal channels
AllowedPrivateCommandsCommands allowed in DMs or hidden/private contexts
AmbientMatchCommandLet MessageMatchers also inspect directed commands
AuthorizeAllCommandsRequire authorization for all plugin commands
AuthorizedCommandsCommands that require authorization
AuthorizerPlugin-specific authorizer override
AuthRequireGroup/role/policy string passed to the authorizer
CatchAllHandle unmatched directed commands
CatchAllModesRestrict catch-all handling by addressing mode
ChannelChannel used for scheduled/pipeline plugin contexts
ChannelsPublic channels where the plugin is visible
CommandsDirected command matchers
ConfigFree-form plugin-owned configuration
DisabledDisable the plugin
ElevatedCommandsCommands that require elevation
ElevateImmediateCommandsCommands that always require fresh elevation
ElevatorPlugin-specific elevator override
MatchUnlistedLet ambient matchers run for unlisted users
MessageMatchersAmbient message matchers
ReplyMatchersPrompt/reply matchers
RequireAdminRestrict the whole plugin to admins
RequireAllCommandsPrivateRequire private context for every command
RequiredPrivateCommandsCommands that must run privately
RestrictPrivateChannelsEnforce channel restrictions for private commands
TimeOutsPer-plugin timeout thresholds
UsersCanonical username allow-list

InputMatcher Field Index

Commands, MessageMatchers, and ReplyMatchers use the same matcher shape, with restrictions noted above.

FieldUsed ByPurpose
CommandCommands, MessageMatchersCommand token passed to plugin
Regexall matcher listsGo regex matcher
SimpleMatcherCommands onlySimplified directed-command matcher
UsageCommands, MessageMatchersHelp usage text
SummaryCommands, MessageMatchersHelp summary
ExamplesCommands, MessageMatchersHelp examples
KeywordsCommands, MessageMatchersHelp search terms
LabelReplyMatchersReply matcher label
ChannelOnlyall matcher listsRestrict match to non-thread channel messages
Contextsall matcher listsLabels for context memory based on captures

Config Templates

Gopherbot configuration files are Go text templates before they are parsed as YAML. This applies across the config tree, including robot.yaml, environment files, protocol files, provider files, plugin files, and job files.

Templates are most often used to:

  • select environment-specific config
  • read deployment environment variables
  • include another config file
  • reference shared variables and encrypted secrets
  • keep custom robot config small while still supporting development, staging, and production differences

Common Pattern

A scaffolded robot usually selects an environment file from robot.yaml:

{{ $environment := env "GOPHER_ENVIRONMENT" | default "production" }}
{{ printf "environments/%s.yaml" $environment | .Include }}

With this pattern, GOPHER_ENVIRONMENT=development loads conf/environments/development.yaml; when the variable is unset, the robot loads conf/environments/production.yaml.

Template Helpers

env

env reads an environment variable.

PrimaryProtocol: {{ env "GOPHER_PROTOCOL" | default "ssh" }}

If the variable is unset or empty, env returns an empty string.

default

default returns a fallback value when the piped input is empty.

LogLevel: {{ env "GOPHER_LOGLEVEL" | default "info" }}

.Include

.Include includes another config file from the same config tree.

{{ printf "environments/%s.yaml" $environment | .Include }}

When used from installed defaults, includes are resolved from the installed config tree. When used from custom config, includes are resolved from the custom robot config tree.

secret

secret decrypts a named encrypted secret from the custom variables files.

Password: '{{ secret "SMTP_PASSWORD" }}'

Secrets are loaded from:

  • custom/conf/variables/common.yaml
  • custom/conf/variables/<environment>.yaml

Environment-specific secrets override common secrets.

Example variables file:

Secrets:
  SMTP_PASSWORD: "<encrypted-base64-value>"

variable

variable reads a named plaintext value from the custom variables files.

Value: '{{ variable "GITHUB_CLIENT_ID" }}'

Variables are loaded from:

  • custom/conf/variables/common.yaml
  • custom/conf/variables/<environment>.yaml

Environment-specific variables override common variables.

Example variables file:

Variables:
  GITHUB_CLIENT_ID: "client-id"

GetStartupMode

GetStartupMode returns the current startup mode.

Possible values:

  • demo: no custom robot repository is configured
  • bootstrap: a custom repository is configured but local config is not present yet
  • test-dev: local development or integration-test config is being used
  • cli: a CLI command is running instead of a full robot
  • production: normal configured robot startup

Installed defaults use this helper to choose safe demo, bootstrap, test, and production behavior. Custom robot config usually does not need it; prefer GOPHER_ENVIRONMENT and environment files for normal robot differences.

SetEnv

SetEnv overrides an environment variable during template expansion.

{{ SetEnv "GOPHER_PROTOCOL" "ssh" }}

This is mainly useful in installed defaults because defaults load before custom config. Use it sparingly in custom robot config; ordinary variables and environment files are usually clearer.

IsTestBuild

IsTestBuild is true when Gopherbot was compiled with the test build tag.

This helper is intended for Gopherbot’s own test and default configuration. Most custom robots should not need it.

Secrets and the Removed decrypt Helper

The old decrypt helper has been removed in v3. If a legacy config uses:

Token: '{{ decrypt "..." }}'

move the encrypted value into a variables file:

Secrets:
  TOKEN: "<encrypted-base64-value>"

then reference it with:

Token: '{{ secret "TOKEN" }}'

This keeps encrypted values in one predictable place and makes it easier to separate development, staging, and production secrets.

Quoting Template Values

YAML parsing happens after template expansion. Quote template expressions when the expanded value may contain punctuation, spaces, colons, braces, or other YAML-sensitive characters.

Recommended:

Password: '{{ secret "SMTP_PASSWORD" }}'

Usually fine for simple values:

PrimaryProtocol: {{ env "GOPHER_PROTOCOL" | default "ssh" }}

When in doubt, quote the template expression.

Merge Timing

Each config file is expanded before it is merged. Installed defaults are expanded and loaded first. Custom robot config is expanded and merged afterward.

That means custom templates can use custom variables files, and custom scalar values replace installed defaults after expansion.

Environment Variables

Environment variables influence both startup and task execution.

Startup values you will use most often

  • GOPHER_ENCRYPTION_KEY
  • GOPHER_CUSTOM_REPOSITORY
  • GOPHER_DEPLOY_KEY
  • GOPHER_CUSTOM_BRANCH
  • GOPHER_ENVIRONMENT
  • GOPHER_LOGDEST
  • GOPHER_LOGLEVEL
  • GOPHER_SSH_PORT
  • GOPHER_MESSAGE_FORMAT

Task and pipeline environment

Tasks receive a cleaned environment plus pipeline-specific values. Common examples include:

  • GOPHER_PROTOCOL
  • GOPHER_USER
  • GOPHER_CHANNEL
  • GOPHER_THREAD_ID
  • GOPHER_MESSAGE_ID
  • GOPHER_PIPE_NAME
  • GOPHER_TASK_NAME
  • GOPHER_START_PROTOCOL
  • GOPHER_START_CHANNEL
  • GOPHER_START_THREAD_ID
  • GOPHER_START_USER

For task authors, the practical rule is simple: prefer the Robot API for important values, and use the environment for integration with external scripts and tools.

Troubleshooting

When config behaves strangely, start with the engine’s own view of the world.

Best tools

  • gopherbot validate <path>
  • gopherbot dump installed robot.yaml
  • gopherbot dump configured robot.yaml
  • gopherbot dump configured protocols/ssh.yaml

Common causes of trouble

  • a value moved from robot.yaml into a provider or protocol file in v3
  • an old top-level UserMap still exists somewhere
  • ProtocolConfig is missing for the active primary protocol
  • a list was overridden when you meant to append, or appended when you meant to replace
  • .env is missing required deployment values for bootstrap mode

Operational advice

If reload fails, treat that as a config problem first, not a plugin bug. The merge and validation tools will usually point you to the real issue faster than trial-and-error editing.

BasicMarkdown Reference

BasicMarkdown is the default outgoing message format in Gopherbot v3. It is a small, portability-first formatting language intended to let one plugin produce readable output across Slack, SSH, and other connectors without relying on connector-specific markup.

For send methods and format-selection helpers such as MessageFormat(...), Fixed(), and Direct(), see Message Sending and Formatting.

Design goals

  • keep shared automation portable
  • support the formatting operators most teams actually need
  • degrade cleanly on simpler connectors
  • avoid visibly broken connector-specific syntax

Supported syntax

Paragraphs and line breaks

A blank line starts a new paragraph. A single newline is a line break.

Hello team,

The deploy has started.
Investigating now.

Bold

Use double asterisks.

**important**

Italic

Use single asterisks.

*emphasis*

Inline code

Use backticks.

Use `kubectl get pods` to inspect the namespace.

Fenced code blocks

Use triple backticks. An optional language hint is allowed.

```yaml
apiVersion: v1
kind: Pod
```

Block quotes

Use > at the start of the line.

> deploy paused pending approval

Unordered lists

Use - at the start of a line.

- build
- deploy
- verify

Only a single list level is part of the current contract.

Use standard Markdown link syntax.

[runbook](https://example.com/runbook)

If a connector cannot render labeled links natively, it should degrade to readable text such as runbook (https://example.com/runbook).

Mentions

BasicMarkdown supports connector-resolved mention tokens:

@alice please confirm the production deploy.

Connectors should turn that into a native mention when they can resolve the user safely. If they cannot, the literal @alice text should remain visible.

Mentions are not parsed inside inline code or fenced code blocks.

Emoji

Both shortcode emoji and raw Unicode emoji are allowed:

:white_check_mark: deploy complete
Deploy complete ✅

Connectors may render shortcode emoji natively when supported. If a shortcode is unknown, the literal text should remain visible.

Escaping

Use backslash escapes when you want literal formatting characters in BasicMarkdown text:

\*not italic\*
\`not code\`
\@literal-user
\[literal link text\]

Connector expectations

Connectors are responsible for translating BasicMarkdown into native rendering on the target platform.

The contract is:

  • preserve meaning first
  • prefer faithful rendering where possible
  • degrade to readable plain text where necessary
  • never emit obviously broken formatting

For connector-specific rendering notes, check the connector appendices such as Slack.

Robot API Overview

Every plugin, job, and task works through the Robot API.

Main capability groups

  • identity and attributes
  • message sending and formatting
  • prompting for replies
  • memory and datum storage
  • pipeline assembly
  • admin and utility helpers

API chapters

Practical notes

  • The current first-class extension languages in this manual are Go, Lua, JavaScript, Bash, Python, and Ruby.
  • Not every language binding exposes every public Robot method. The detailed pages call out gaps where they matter.
  • Connector-specific rendering details live in the connector appendices. For message formatting behavior that depends on Slack or another protocol, start with the API chapter, then check the relevant appendix.

Important v3 behavior notes

  • AddCommand composes work into the current pipeline; it does not fake a new user message.
  • AddJob starts a child pipeline and does not automatically inherit every parent parameter.
  • Prompt timeouts are longer on interactive local connectors such as SSH when using built-in interpreters or Go.

Language Choices

Gopherbot supports several extension languages, but they are not all equally recommended for new work.

  1. Lua
    • built in
    • fast local loop
    • good fit for readable operational automation
  2. Go
    • strongest type safety
    • best for heavier logic and long-lived maintained plugins
  3. JavaScript
    • built in
    • useful when your team already thinks in JS

Still supported

  • Bash
  • Python
  • Ruby

These are valuable for existing robots and for quick integrations, but they depend more on external tooling and are not the main design direction for v3.

One practical rule

If you can write the extension cleanly in a built-in interpreter or Go, do that first. Reach for external runtimes when they are clearly the right fit, not by habit.

Getting Information About Users and the Robot

The attribute methods give plugins and jobs access to directory-style metadata about the current sender, another user, or the robot itself.

Methods

  • GetSenderAttribute(attr)
  • GetUserAttribute(user, attr)
  • GetBotAttribute(attr)

All three methods return both a value and a return code. In Go that is represented by *robot.AttrRet. In the scripting bindings, the object usually stringifies to the attribute value while still exposing the return code.

Common user attributes

  • name
  • fullName
  • email
  • firstName
  • lastName
  • phone
  • internalID

Common bot attributes

  • name
  • alias
  • fullName or realName
  • contact, admin, or adminContact
  • email
  • protocol
  • internalID

Most bot attributes come from robot configuration. User attributes come from the roster plus connector-local identity mapping.

Examples

Go

func handler(r robot.Robot, args ...string) robot.TaskRetVal {
    sender := r.GetSenderAttribute("email")
    if sender.RetVal != robot.Ok {
        r.Say("I could not find your email address.")
        return robot.Normal
    }

    proto := r.GetBotAttribute("protocol")
    r.Say("I know you as %s, and I am speaking on %s.", sender.Attribute, proto.Attribute)
    return robot.Normal
}

Lua

local gopherbot = require("gopherbot_v1")
local ret = gopherbot.ret
local Robot = gopherbot.Robot

local bot = Robot:new()
local email, rv = bot:GetSenderAttribute("email")
if rv ~= ret.Ok then
  bot:Say("I could not find your email address.")
else
  local proto = bot:GetBotAttribute("protocol")
  bot:Say("I know you as " .. email .. " on " .. tostring(proto))
end

JavaScript

const { Robot, ret } = require("gopherbot_v1")();

const bot = new Robot();
const email = bot.GetSenderAttribute("email");
if (email.retVal !== ret.Ok) {
  bot.Say("I could not find your email address.");
} else {
  const proto = bot.GetBotAttribute("protocol");
  bot.Say(`I know you as ${email.attribute} on ${proto.attribute}.`);
}

Bash

USEREMAIL=$(GetSenderAttribute email)
if [ $? -ne $GBRET_Ok ]
then
  Say "I could not find your email address."
else
  PROTOCOL=$(GetBotAttribute protocol)
  Say "I know you as $USEREMAIL on $PROTOCOL."
fi

Python

email = bot.GetSenderAttribute("email")
if email.ret != Robot.Ok:
    bot.Say("I could not find your email address.")
else:
    proto = bot.GetBotAttribute("protocol")
    bot.Say("I know you as %s on %s." % (email, proto))

Ruby

email = bot.GetSenderAttribute("email")
if email.ret != Robot::Ok
  bot.Say("I could not find your email address.")
else
  proto = bot.GetBotAttribute("protocol")
  bot.Say("I know you as #{email} on #{proto}.")
end

Sending Messages and Formatting

The Robot API separates message destination from message format. That makes it possible to keep most extensions protocol-agnostic while still sending readable output to Slack, SSH, and other connectors.

Message formats

  • BasicMarkdown
    • the v3 default outgoing format
    • intended to be the portable, cross-protocol choice for most normal replies
  • Variable
    • literal variable-width text
    • useful when you want normal text without connector-driven formatting surprises
  • Fixed
    • preformatted output for code, logs, and tabular text
  • Raw
    • connector-native pass-through
    • use sparingly, because it gives up portability

For the actual BasicMarkdown syntax contract, see BasicMarkdown Reference.

Default and overrides

Outgoing format is chosen in this order:

  1. an explicit format on the send call or helper Robot
  2. GOPHER_MESSAGE_FORMAT if set
  3. DefaultMessageFormat
  4. the v3 default of BasicMarkdown

The usual helpers are:

  • MessageFormat(...)
  • Fixed()
  • Direct()
  • Threaded()

Main message-sending methods

  • Say(...) and Reply(...)
    • send in the current context
  • SayThread(...) and ReplyThread(...)
    • force thread-aware delivery
  • SendChannelMessage(...) and SendChannelThreadMessage(...)
    • send to a specific channel
  • SendUserMessage(...)
    • send a direct message
  • SendUserChannelMessage(...) and SendUserChannelThreadMessage(...)
    • direct a message to a user in a channel
  • SendProtocolUserChannelMessage(...)
    • choose the connector explicitly for cross-protocol sends

Platform-specific rendering

The API guarantees the intent of the format, not identical pixel-perfect rendering across every connector. For connector-specific notes such as Slack formatting behavior, check the relevant connector appendix after reading this page.

Examples

Go

func handler(r robot.Robot, args ...string) robot.TaskRetVal {
    r.Say("Build started for `%s`", args[0])
    r.Fixed().Say("service   status\napi       ok\nworker    ok")
    r.Direct().Reply("I will DM you when the deploy finishes.")
    _ = r.SendProtocolUserChannelMessage("slack", "", "deployments", "*Deploy complete*")
    return robot.Normal
}

Lua

local gopherbot = require("gopherbot_v1")
local fmt = gopherbot.fmt
local Robot = gopherbot.Robot

local bot = Robot:new()
bot:MessageFormat(fmt.BasicMarkdown):Say("Deploying `payments`")
bot:Fixed():Say("service   status\napi       ok\nworker    ok")
bot:Direct():Reply("I will DM you when the deploy finishes.")
bot:SendProtocolUserChannelMessage("slack", "", "deployments", "*Deploy complete*", fmt.BasicMarkdown)

JavaScript

const { Robot, fmt } = require("gopherbot_v1")();

const bot = new Robot();
bot.MessageFormat(fmt.BasicMarkdown).Say("Deploying `payments`");
bot.Fixed().Say("service   status\napi       ok\nworker    ok");
bot.Direct().Reply("I will DM you when the deploy finishes.");
bot.SendProtocolUserChannelMessage("slack", "", "deployments", "*Deploy complete*", fmt.BasicMarkdown);

Bash

Say -m 'Deploying `payments`'
Say -f $'service   status\napi       ok\nworker    ok'
SendUserMessage -m "$GOPHER_USER" "I will DM you when the deploy finishes."
SendProtocolUserChannelMessage -m "slack" "" "deployments" "*Deploy complete*"

Python

bot.MessageFormat("BasicMarkdown").Say("Deploying `payments`")
bot.MessageFormat("Fixed").Say("service   status\napi       ok\nworker    ok")
bot.Direct().Reply("I will DM you when the deploy finishes.")
bot.SendProtocolUserChannelMessage("slack", "", "deployments", "*Deploy complete*", "BasicMarkdown")

Ruby

bot.MessageFormat("BasicMarkdown").Say("Deploying `payments`")
bot.MessageFormat("Fixed").Say("service   status\napi       ok\nworker    ok")
bot.Direct().Reply("I will DM you when the deploy finishes.")
bot.SendProtocolUserChannelMessage("slack", "", "deployments", "*Deploy complete*", "BasicMarkdown")

Prompting for Replies

The Prompt*ForReply family supports interactive, multi-step commands where the robot needs more input before it can continue.

Prompting methods

  • PromptForReply(regexID, prompt)
  • PromptThreadForReply(regexID, prompt)
  • PromptUserForReply(regexID, user, prompt)
  • PromptUserChannelForReply(regexID, user, channel, prompt)
  • PromptUserChannelThreadForReply(regexID, user, channel, thread, prompt)

The method you choose depends on who should receive the prompt and whether the exchange should stay inside a thread.

Matching replies

regexID can refer to a custom reply matcher from plugin or job configuration, or one of the stock matchers:

  • Email
  • Domain
  • OTP
  • IPAddr
  • SimpleString
  • YesNo

Return values

In Go, the methods return (reply, retVal).

In the scripting bindings, the result is usually an object that includes the matched reply plus the return code.

Common return codes:

  • Ok
  • UserNotFound
  • ChannelNotFound
  • MatcherNotFound
  • Interrupted
  • TimeoutExpired
  • UseDefaultValue
  • ReplyNotMatched

Examples

Go

func handler(r robot.Robot, args ...string) robot.TaskRetVal {
    answer, rv := r.PromptForReply("YesNo", "Do you want to continue?")
    if rv != robot.Ok {
        r.Say("No reply received.")
        return robot.Normal
    }
    r.Say("You answered: %s", answer)
    return robot.Normal
}

Lua

local gopherbot = require("gopherbot_v1")
local ret = gopherbot.ret
local Robot = gopherbot.Robot

local bot = Robot:new()
local answer, rv = bot:PromptForReply("YesNo", "Do you want to continue?")
if rv ~= ret.Ok then
  bot:Say("No reply received.")
else
  bot:Say("You answered: " .. answer)
end

JavaScript

const { Robot, ret } = require("gopherbot_v1")();

const bot = new Robot();
const answer = bot.PromptForReply("YesNo", "Do you want to continue?");
if (answer.retVal !== ret.Ok) {
  bot.Say("No reply received.");
} else {
  bot.Say(`You answered: ${answer.reply}`);
}

Bash

ANSWER=$(PromptForReply "YesNo" "Do you want to continue?")
if [ $? -ne $GBRET_Ok ]
then
  Say "No reply received."
else
  Say "You answered: $ANSWER"
fi

Python

answer = bot.PromptForReply("YesNo", "Do you want to continue?")
if answer.ret != Robot.Ok:
    bot.Say("No reply received.")
else:
    bot.Say("You answered: %s" % answer)

Ruby

answer = bot.PromptForReply("YesNo", "Do you want to continue?")
if answer.ret != Robot::Ok
  bot.Say("No reply received.")
else
  bot.Say("You answered: #{answer}")
end

Memory and Brain Methods

The Robot API has two different memory layers:

  • long-term data stored in the configured brain provider
  • short-term contextual memory stored in the running engine

Memory scoping

Long-term data is scoped by key and namespace. Short-term memory is scoped to the current user and channel, and optionally to a thread.

Long-term memory methods

  • CheckoutDatum(key, rw)
  • UpdateDatum(...)
  • CheckinDatum(...)
  • DeleteDatum(key)

Use long-term memory for persistent state such as deployment records, saved lists, or structured workflow state. Read-write checkouts are intentionally short-lived. If you need to coordinate longer work, combine long-term memory with Exclusive(...).

Long-term memory helpers are not available in the Bash binding.

Go

type DeployState struct {
    Version string `json:"version"`
}

var state DeployState
token, exists, rv := r.CheckoutDatum("deploy-state", &state, true)
if rv != robot.Ok {
    r.Say("Unable to load deploy state.")
    return robot.Normal
}
if !exists {
    state.Version = "initial"
}
state.Version = "2026.03.17"
if rv := r.UpdateDatum("deploy-state", token, &state); rv != robot.Ok {
    r.Say("Unable to update deploy state.")
}

Lua

local gopherbot = require("gopherbot_v1")
local ret = gopherbot.ret
local Robot = gopherbot.Robot

local bot = Robot:new()
local memory, rv = bot:CheckoutDatum("deploy-state", true)
if rv ~= ret.Ok then
  bot:Say("Unable to load deploy state.")
elseif not memory.exists then
  memory.datum = { version = "initial" }
end

memory.datum.version = "2026.03.17"
if bot:UpdateDatum(memory) ~= ret.Ok then
  bot:Say("Unable to update deploy state.")
end

JavaScript

const { Robot, ret } = require("gopherbot_v1")();

const bot = new Robot();
const state = bot.CheckoutDatum("deploy-state", true);
if (state.retVal !== ret.Ok) {
  bot.Say("Unable to load deploy state.");
} else {
  if (!state.exists) {
    state.datum = { version: "initial" };
  }
  state.datum.version = "2026.03.17";
  if (bot.UpdateDatum(state) !== ret.Ok) {
    bot.Say("Unable to update deploy state.");
  }
}

Python

memory = bot.CheckoutDatum("deploy-state", True)
if memory.ret != Robot.Ok:
    bot.Say("Unable to load deploy state.")
else:
    if not memory.exists:
        memory.datum = {"version": "initial"}
    memory.datum["version"] = "2026.03.17"
    if bot.UpdateDatum(memory) != Robot.Ok:
        bot.Say("Unable to update deploy state.")

Ruby

memory = bot.CheckoutDatum("deploy-state", true)
if memory.ret != Robot::Ok
  bot.Say("Unable to load deploy state.")
else
  memory.datum = { "version" => "initial" } unless memory.exists
  memory.datum["version"] = "2026.03.17"
  if bot.UpdateDatum(memory) != Robot::Ok
    bot.Say("Unable to update deploy state.")
  end
end

Short-term memory methods

  • Remember(key, value, shared)
  • RememberThread(key, value, shared)
  • RememberContext(context, value)
  • RememberContextThread(context, value)
  • Recall(key, shared)
  • DeleteMemory(key, shared)

Short-term memory is best for conversational context such as “the current server” or “the thing we were just talking about.”

Go

r.RememberContext("server", "api-01")
r.Say("Next time you say 'it', I will treat that as api-01.")

Lua

local gopherbot = require("gopherbot_v1")
local Robot = gopherbot.Robot

local bot = Robot:new()
bot:RememberContext("server", "api-01")
bot:Say("Next time you say 'it', I will treat that as api-01.")

JavaScript

const { Robot } = require("gopherbot_v1")();

const bot = new Robot();
bot.RememberContext("server", "api-01");
bot.Say("Next time you say 'it', I will treat that as api-01.");

Bash

RememberContext "server" "api-01"
Say "Next time you say 'it', I will treat that as api-01."

Python

bot.RememberContext("server", "api-01")
bot.Say("Next time you say 'it', I will treat that as api-01.")

Ruby

bot.RememberContext("server", "api-01")
bot.Say("Next time you say 'it', I will treat that as api-01.")

Pipeline Methods

Gopherbot pipelines are built in code rather than declared in a separate pipeline language. That is why the Robot API includes methods for adding tasks, jobs, commands, cleanup steps, and failure handlers while a pipeline is being assembled.

Pipeline-building methods

  • GetParameter(name)
  • SetParameter(name, value)
  • AddTask(name, args...)
  • FinalTask(name, args...)
  • FailTask(name, args...)
  • AddJob(name, args...)
  • SpawnJob(name, args...)
  • AddCommand(plugin, command)
  • FinalCommand(plugin, command)
  • FailCommand(plugin, command)
  • Exclusive(tag, queueTask)

Important behavior notes

  • AddCommand(...) composes another plugin command into the current pipeline. It is not the same thing as injecting a fresh user message.
  • FinalTask(...) runs during cleanup even if the main pipeline fails.
  • FailTask(...) and FailCommand(...) only run when the primary pipeline fails.
  • SpawnJob(...) starts work in parallel, while AddJob(...) adds a child job into the current pipeline flow.
  • Exclusive(...) is the advisory lock for protecting shared resources such as worktrees or deployment targets.

SetWorkingDirectory(...) exists in the public Go API, but it is currently omitted from this user-facing reference until that area is revisited.

Examples

Go

if !r.Exclusive("deploy-prod", false) {
    r.Say("Another production deploy is already running.")
    return robot.Normal
}

r.SetParameter("TARGET_ENV", "production")
r.AddTask("build-artifact", "payments")
r.AddTask("deploy-service", "payments")
r.FinalTask("cleanup-worktree")
r.FailTask("notify-deploy-failure")

Lua

local gopherbot = require("gopherbot_v1")
local task = gopherbot.task
local Robot = gopherbot.Robot

local bot = Robot:new()
if not bot:Exclusive("deploy-prod", false) then
  bot:Say("Another production deploy is already running.")
  return task.Normal
end

bot:SetParameter("TARGET_ENV", "production")
bot:AddTask("build-artifact", "payments")
bot:AddTask("deploy-service", "payments")
bot:FinalTask("cleanup-worktree")
bot:FailTask("notify-deploy-failure")

JavaScript

const { Robot, task } = require("gopherbot_v1")();

const bot = new Robot();
if (!bot.Exclusive("deploy-prod", false)) {
  bot.Say("Another production deploy is already running.");
  return task.Normal;
}

bot.SetParameter("TARGET_ENV", "production");
bot.AddTask("build-artifact", "payments");
bot.AddTask("deploy-service", "payments");
bot.FinalTask("cleanup-worktree");
bot.FailTask("notify-deploy-failure");

Bash

if ! Exclusive "deploy-prod" false
then
  Say "Another production deploy is already running."
  exit 0
fi

SetParameter "TARGET_ENV" "production"
AddTask "build-artifact" "payments"
AddTask "deploy-service" "payments"
FinalTask "cleanup-worktree"
FailTask "notify-deploy-failure"

Python

if not bot.Exclusive("deploy-prod", False):
    bot.Say("Another production deploy is already running.")
else:
    bot.SetParameter("TARGET_ENV", "production")
    bot.AddTask("build-artifact", ["payments"])
    bot.AddTask("deploy-service", ["payments"])
    bot.FinalTask("cleanup-worktree", [])
    bot.FailTask("notify-deploy-failure", [])

Ruby

if !bot.Exclusive("deploy-prod", false)
  bot.Say("Another production deploy is already running.")
else
  bot.SetParameter("TARGET_ENV", "production")
  bot.AddTask("build-artifact", ["payments"])
  bot.AddTask("deploy-service", ["payments"])
  bot.FinalTask("cleanup-worktree", [])
  bot.FailTask("notify-deploy-failure", [])
end

Utility Methods

This chapter covers the core convenience methods that most plugin and job authors use regularly.

Methods

  • CheckAdmin()
  • Elevate(immediate)
  • GetTaskConfig(...)
  • Log(level, message, ...)
  • Pause(seconds)

Notes

  • CheckAdmin() is useful when one plugin has a mix of admin-only and non-admin commands.
  • Elevate(...) requests stronger proof of identity, usually for sensitive operations.
  • GetTaskConfig(...) is how compiled Go, Lua, and JavaScript extensions read structured task configuration. Python and Ruby also expose it. Bash does not.
  • Log(...) writes to the robot log with the normal log levels.

Log levels

  • Trace
  • Debug
  • Info
  • Audit
  • Warn
  • Error
  • Fatal

Examples

Go

type config struct {
    AllowedEnv string `json:"allowedEnv"`
}

var cfg config
if rv := r.GetTaskConfig(&cfg); rv != robot.Ok {
    r.Log(robot.Error, "missing config")
    return robot.Normal
}
if !r.CheckAdmin() {
    r.Say("This command is admin-only.")
    return robot.Normal
}
r.Log(robot.Info, "running admin task for %s", cfg.AllowedEnv)

Lua

local gopherbot = require("gopherbot_v1")
local ret = gopherbot.ret
local task = gopherbot.task
local log = gopherbot.log
local Robot = gopherbot.Robot

local bot = Robot:new()
local cfg, rv = bot:GetTaskConfig()
if rv ~= ret.Ok then
  bot:Log(log.Error, "missing config")
  return task.Normal
end
if not bot:CheckAdmin() then
  bot:Say("This command is admin-only.")
  return task.Normal
end
bot:Log(log.Info, "running admin task")

JavaScript

const { Robot, ret, log } = require("gopherbot_v1")();

const bot = new Robot();
const cfg = bot.GetTaskConfig();
if (cfg.retVal !== ret.Ok) {
  bot.Log(log.Error, "missing config");
} else if (!bot.CheckAdmin()) {
  bot.Say("This command is admin-only.");
} else {
  bot.Log(log.Info, "running admin task");
}

Bash

if ! CheckAdmin
then
  Say "This command is admin-only."
  exit 0
fi

Log "Info" "running admin task"
Pause 1
Say "Done."

Python

if not bot.CheckAdmin():
    bot.Say("This command is admin-only.")
else:
    bot.Log("Info", "running admin task")
    bot.Pause(1)
    bot.Say("Done.")

Ruby

if !bot.CheckAdmin()
  bot.Say("This command is admin-only.")
else
  bot.Log("Info", "running admin task")
  bot.Pause(1)
  bot.Say("Done.")
end

Miscellaneous Methods

This page covers public Robot methods that do not fit cleanly into the main attribute, messaging, memory, pipeline, or utility chapters.

Thread subscriptions

These methods let a plugin stay attached to an active thread so later messages in that thread continue to route back to the same plugin logic.

  • Subscribe()
  • Unsubscribe()

Go

func handler(r robot.Robot, args ...string) robot.TaskRetVal {
    if r.Subscribe() {
        r.Say("I will keep watching this thread.")
    }
    return robot.Normal
}

Lua

local gopherbot = require("gopherbot_v1")
local Robot = gopherbot.Robot

local bot = Robot:new()
if bot:Subscribe() then
  bot:Say("I will keep watching this thread.")
end

JavaScript

const { Robot } = require("gopherbot_v1")();

const bot = new Robot();
if (bot.Subscribe()) {
  bot.Say("I will keep watching this thread.");
}

Bash

if Subscribe
then
  Say "I will keep watching this thread."
fi

Python

if bot.Subscribe():
    bot.Say("I will keep watching this thread.")

Ruby

if bot.Subscribe()
  bot.Say("I will keep watching this thread.")
end

Random helpers

These are convenience methods for lightweight reply variation.

  • RandomString(...)
  • RandomInt(...)

RandomInt(...) is not exposed consistently in every older scripting binding, so check the binding you are using.

Go

choice := r.RandomString([]string{"Working on it.", "Still running.", "Almost done."})
r.Say(choice)

Lua

local gopherbot = require("gopherbot_v1")
local Robot = gopherbot.Robot

local bot = Robot:new()
local choice = bot:RandomString({"Working on it.", "Still running.", "Almost done."})
bot:Say(choice)

JavaScript

const { Robot } = require("gopherbot_v1")();

const bot = new Robot();
const choice = bot.RandomString(["Working on it.", "Still running.", "Almost done."]);
bot.Say(choice);

Python

choice = bot.RandomString(["Working on it.", "Still running.", "Almost done."])
bot.Say(choice)

Ruby

choice = bot.RandomString(["Working on it.", "Still running.", "Almost done."])
bot.Say(choice)

Compiled Go notes

Two public methods are mostly relevant to compiled Go extensions:

  • GetMessage()
  • RaisePriv(reason)

They are intentionally not expanded into large user-facing chapters here. If you are writing a compiled Go extension and need direct access to message internals or privileged filesystem behavior, read the engine source and current developer docs alongside this manual.

Upgrading Existing Robots

For v3, extension compatibility is a higher priority than configuration compatibility.

That means many older plugins and jobs can keep working, while robot configuration often needs deliberate migration.

Highest-priority migration items

  1. Move to PrimaryProtocol and optional SecondaryProtocols.
  2. Put connector-local identity mapping inside each connector’s ProtocolConfig.
  3. Keep UserRoster as the canonical global user directory.
  4. Move provider-specific settings into conf/brains/<provider>.yaml and conf/history/<provider>.yaml.
  5. Expect DefaultMessageFormat to be BasicMarkdown unless you set something else explicitly.

Identity changes to care about

  • authorization is username-based
  • IgnoreUnlistedUsers is username-based
  • Slack UserMap is connector-local
  • SSH user mapping belongs in ProtocolConfig.UserKeys

Practical upgrade advice

The safest migration path is usually:

  1. scaffold a fresh v3-style robot
  2. compare it with your existing robot
  3. port your custom config and extensions into the new layout
  4. test locally before touching production

That approach usually goes faster than trying to rescue a heavily mutated old config tree in place.

Appendix

Reference material for Gopherbot administrators and developers.

Appendix A: Protocols

Gopherbot communicates with users via different protocols, and extensions can modify their behavior and provide protocol-specific functionality based on values provided to the extension. For Go extensions, this is provided in the robot.Message struct provided by the GetMessage() method. For external scripts, this is provided in the GOPHER_PROTOCOL environment variable.

The design of Gopherbot is meant for mainly protocol-agnostic functionality. Running jobs and querying infrastructure should operate in much the same way whether the protocol is Slack or Terminal. However, some teams may wish to create protocol-specific extensions, and accept the risk of a more difficult transition should the team switch their primary chat platform.

A.1 Slack

The first and best-supported protocol is Slack. Developers wishing to support new protocols should consider Slack the “gold standard”. Gopherbot uses the slack-go/slack library.

The Message struct for Slack will have an .Protocol value of robot.Slack, and .Incoming pointer to a robot.ConnectorMessage struct:

  • Protocol: slack
  • MessageObject: *slack.MessageEvent
  • Client: *slack.Client

Outgoing message formats

Slack is the most fully developed team-chat connector, but exact rendering can still vary a bit between Slack clients, notifications, and previews.

  • BasicMarkdown
    • the normal v3 default for portable formatted output
  • Variable
    • sent as literal variable-width text
  • Fixed
    • sent as preformatted Slack content for code or aligned text
  • Raw
    • passed through with connector-native interpretation

If you are trying to understand a formatting difference on Slack, start with the API formatting chapter and then use this appendix for Slack-specific expectations.