You are viewing docs for Brigade v2. Click here for v1 docs.

Brigade Docs

Brigade: Event-driven scripting for Kubernetes.

Scripting Guide

Scripting Guide

This guide explains the basics of how to create and structure Brigade scripts, which can be written in either JavaScript (brigade.js) or TypeScript (brigade.ts).

Brigade Scripts, Projects, and Repositories

At the very core, a Brigade script is simply a JavaScript (or TypeScript) file defined by a project, which Brigade executes in the context of a worker to handle events to which the project is subscribed.

Brigade scripts can be stored either in a project definition or in a project’s git repository. We like to think of these as cluster-oriented shell scripts: The script just ties together a bunch of other programs.

This document will walk through the example projects, with the scripts gradually increasing in complexity.


To proceed, you will need to have access to a Brigade server and be logged in using the brig CLI. See the Quickstart if you have not already done so.

Then, each example project can be created like so:

$ brig project create -f examples/<project>/project.yaml

Let’s begin!

A Basic Brigade Script

Here is a basic brigade.js script that just sends a single message to the log:

console.log("Hello, World!");


First let’s create the example project:

$ brig project create --file examples/01-hello-world/project.yaml

Created project "hello-world".

This project subscribes to one event, the exec event generated by the brig event create command. The events that a project subscribes to are configured under the eventSubscriptions section of its definition file (e.g. project.yaml).

Next, let’s trigger execution of the project script by creating an event of this type:

$ brig event create --project hello-world --follow

Created event "261229dc-1140-4f6a-bf91-bd2a69f31721".

Waiting for event's worker to be RUNNING...
2021-09-20T22:15:12.047Z INFO: brigade-worker version: a398ba8-dirty
Hello, World!

This example unconditionally logs “Hello, World!” in response to any event to which the project is subscribed (though, as mentioned, it is only subscribed to one type). It’s more common to incorporate event handlers to handle specific events in different ways.

Brigade Events and Event Handlers

Examples of events would include pushes to GitHub or DockerHub repositories. When a project is subscribed to an event, that project’s worker will load and execute the project’s script. Event handlers in the script define specific logic for handling matching events.

Here we see a script that consists of an event handler specifically for the exec event emitted by the brig CLI. If any other event is generated for a project, no action would occur as there is no logic defined to handle it.

const { events } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  console.log("Hello, World!");

  // Optionally return a string and it will automatically be persisted
  // in the event's summary field.
  return "All done."



There are a few things to note about this script:

  • It imports the events object from @brigadecore/brigadier. Almost all Brigade scripts do this.
  • It declares a single event handler that says “on an exec event, run this function”.
  • We’re not actually utilizing the event object in this script, but we will in a later example
  • The event handler returns a string, which is the optional event summary. This is can be utilized by gateways interested in the results or other context related to the handling of the event. The summary only has meaning to the script and possibly the gateway – otherwise, it is opaque to Brigade itself.
  • Scripts with event handlers defined, such as this one, must invoke events.process() after registering all handlers in order to dispatch the event.

Similarly to our first script, this event handler function displays a message to a log, producing the following output:

$ brig event create --project first-event --follow

Created event "5b0bd00a-4f31-40da-ad01-0d2f62f4d70e".

Waiting for event's worker to be RUNNING...
2021-09-20T22:24:57.655Z INFO: brigade-worker version: a398ba8-dirty
Hello, World!

Note: the success/failure of an event is determined by the exit code of the Worker running the script. If the script fails or throws an error, the Worker will exit non-zero and be considered to have failed.

Since projects may subscribe to multiple types of events (from multiple sources as well), defining multiple event handlers in your script permits different types of events to be handled differently.

For example, we can can expand the example above to also provide a handler for a GitHub push event:

const { events } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  console.log("==> handling an 'exec' event")

events.on("", "push", async event => {
  console.log(" **** I'm a GitHub 'push' handler")

Now if we re-run our brig command, we will see the same output as before:

$ brig event create --project first-event --follow

Created event "6769b7fd-ddd0-4a42-aa17-723c02a2b61a".

Waiting for event's worker to be RUNNING...
2021-09-20T22:29:28.498Z INFO: brigade-worker version: a398ba8-dirty
Hello, World!

Since the event we created was from and of type exec, only the corresponding handler was executed. The handler for events from and of type push was not.

See the Events doc for more info on how events are structured in Brigade.

Jobs and Containers

The Brigade worker’s image is derived from a Node.js image and containers based on that image can execute JavaScript and TypeScript only. In the frequent case that other runtimes, tools, etc. not present on the worker image are required in order to respond to an event, spawning a Job permits aspects of event-handling to be delegated to a more suitable container.

The Brigadier library exposes an API for defining and executing Jobs.

In the previous section, we focused on event handlers which just logged messages. In this section, we’ll update the event handler to create a few jobs.

To start with, let’s create a simple job that doesn’t really do any work:

const { events, Job } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  let job = new Job("do-nothing", "debian", event);


The first thing to note is that we have changed the first line. In addition to importing the events object, we are now also importing Job. Job is the class used for creating all jobs in Brigade.

Next, inside of our exec event handler, we create a new Job, and we give it three pieces of information:

  • a name: The name must be unique to the event handler and should be composed of letters, numbers, and dashes (-).
  • an image: This can be any image that your cluster can find. In the case above, we use the image named debian, which is fetched straight from DockerHub.
  • the event itself that this handler is for

The Job is a crucial part of the Brigade ecosystem. A container is created from a Job’s image, and it’s within this container that we do the “real work”. At the beginning, we explained that we think of Brigade scripts as “shell scripts for your cluster.” When you execute a shell script, it is typically some glue code that manages calling one or more external programs in a specific way.


ps -ef "hello" | grep chrome

The script above really just organizes the way we call two existing programs (ps and grep). Brigade does the same, except instead of executing programs, it executes containers. Each container is expressed as a Job, which is a wrapper that knows how to execute containers.

So in our example above, we create a Job named “do-nothing” that runs a Debian Linux container and (as the name implies) does nothing.

Jobs are created and run in different steps. This way, we can do some preparation on our job (as we will see in a moment) before executing it.

To run a job, we use the Job’s run() method. Behind the scenes, Brigade will start a new debian container (downloading the image if necessary), execute it, and monitor it. When the container is done executing, the run is complete.

It is worth mentioning that run() is asynchronous and actually a request to the Brigade API server to schedule the job for execution. This execution is subject to scheduling constraints, e.g. per configuration like maximum job concurrency, substrate capacity, etc.

Further, note that run() returns immediately after scheduling and the await keyword can (and usually should) be used to, instead, block further script execution until the job has actually been executed and reached a terminal state.

If we run the code above, we’ll get output that looks something like this:

$ b event create --project first-job --follow

Created event "aa8fff14-0b8d-4903-9109-ccadc1d9d3fe".

Waiting for event's worker to be RUNNING...
2021-09-22T18:41:01.787Z INFO: brigade-worker version: 927850b-dirty
2021-09-22T18:41:02.130Z [job: do-nothing] INFO: Creating job do-nothing

Basically, our simple build just created an empty Debian Linux pod which had nothing to do and so exited immediately.

Adding Commands to Jobs

To make our Job do more, we can add a command to it. A command can then be paired with a list of arguments. The command and argument arrays are added to the job’s primaryContainer, which is the container running the image supplied to the Job constructor, e.g. debian in this example. (Every job will have one primaryContainer and zero or more sidecarContainers. We’ll look at an example using sidecarContainers later on.)

const { events, Job } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  let job = new Job("my-first-job", "debian", event);
  job.primaryContainer.command = ["echo"];
  job.primaryContainer.arguments = ["My first job!"];



A command can be anything that is supported by the job’s image. In the example above, our command invokes the echo binary with the arguments supplied.

For multiple commands, a common approach is for the command array to consist of one element such as ["/bin/sh"] or ["/bin/bash"] and then the first element of the arguments array be "-c", followed by entries comprising the shell commands needed.

However, as construction of such commands may soon prove unwieldy due to increased complexity, we recommend writing a shell script and making this available to the script to run (e.g. placing it in the git repository associated with the project or adding it to a custom Worker image).

Let’s run the example:

$ brig event create --project first-job --follow

Created event "046c09cd-76cb-49ea-b40c-d3e0e557de62".

Waiting for event's worker to be RUNNING...
2021-09-22T20:11:10.453Z INFO: brigade-worker version: 927850b-dirty
2021-09-22T20:11:10.909Z [job: my-first-job] INFO: Creating job my-first-job

Now, to see the logs from my-first-job, we issue the following brig command utilizing the generated event ID.

$ brig event logs --id 046c09cd-76cb-49ea-b40c-d3e0e557de62 --job my-first-job

My first job!

Note: will throw an exception if the job fails. Additionally, job success/failure is determined by the exit code of the job’s primaryContainer.

Combining jobs into a pipeline

Now we can take things one more step and create two jobs that each do something.

const { events, Job } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  let job1 = new Job("my-first-job", "debian", event);
  job1.primaryContainer.command = ["echo"];
  job1.primaryContainer.arguments = ["My first job!"];

  let job2 = new Job("my-second-job", "debian", event);
  job2.primaryContainer.command = ["echo"];
  job2.primaryContainer.arguments = ["My second job!"];



In this example we create two jobs (my-first-job and my-second-job). Each starts a Debian container and prints a message, then exits. On account of the use of await, job2 won’t run until job1 completes, so these jobs implicitly run sequentially.

Let’s run the example and then view each job’s logs:

$ brig event create --project simple-pipeline

Created event "58e7d3cf-b7d2-4ab7-98ad-326a99f10a25".

$ brig event logs --id 58e7d3cf-b7d2-4ab7-98ad-326a99f10a25 --job my-first-job

My first job!

$ brig event logs --id 58e7d3cf-b7d2-4ab7-98ad-326a99f10a25 --job my-second-job

My second job!

Serial and Concurrent job groups

Now that we’ve seen an example project that runs multiple jobs, let’s look at the methods we have for specifiying how the jobs run, i.e. sequentially or concurrently – or, as we’re about to see, a combination thereof.

For example, we can run two sequences of jobs concurrently:

const { events, Job } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  let job1 = new Job("my-first-job", "debian", event);
  job1.primaryContainer.command = ["echo"];
  job1.primaryContainer.arguments = ["My first job!"];

  let job2 = new Job("my-second-job", "debian", event);
  job2.primaryContainer.command = ["echo"];
  job2.primaryContainer.arguments = ["My second job!"];
  let jobA = new Job("my-a-job", "debian", event);
  jobA.primaryContainer.command = ["echo"];
  jobA.primaryContainer.arguments = ["My A job!"];

  let jobB = new Job("my-b-job", "debian", event);
  jobB.primaryContainer.command = ["echo"];
  jobB.primaryContainer.arguments = ["My B job!"];

  await Job.concurrent(
    Job.sequence(job1, job2),
    Job.sequence(jobA, jobB)



There are two things to notice in the example above:

  1. Both the concurrent and sequence methods exist on the Job object.
  2. The return type for both methods is a generic “runnable”, i.e. an object that run() can then be called on, just like a standalone job instance.

Here’s a breakdown of each method:

  • Job.sequence() takes an array of runnables (e.g. a job or a group of jobs) and runs them in sequence. A new runnable is started only when the previous one completes. The sequence completes when the last runnable has completed (or when any runnable fails). A sequential group is itself considered a success or failure on the basis of all its jobs completing successfully.
  • Job.concurrent() takes an array of runnables and runs them all concurrently. When run, all runnables are started simultaneously (subject to scheduling constraints). The concurrent group completes when all Runnables have completed. A concurrent group is itself considered a success or failure on the basis of all its jobs completing successfully.

As both of these methods return a runnable, they can be chained. In the example above, job1 and job2 run in sequence, as do jobA and jobB, but both sequences are run concurrently.

This is the way script writers can control the order in which groups of jobs are run.

For example, if using Brigade to implement CI, you might wish to divide checks into those that are resource intensive (e.g. longer-running builds, integration tests) and those that are less so (e.g. linting, unit tests). Both groups can run the jobs within concurrently, but the groups themselves might be run in sequence, such that no resource intensive checks are executed unless/until all of the less resource intensive checks have passed.

Running a script from a Git Repository

Earlier we talked about how a project may have an associated Git repository. Let’s look at one such project now.

kind: Project
  id: git
description: A project with whose script is stored in a git repository
  - source:
    - exec
    # logLevel: DEBUG
    configFilesDirectory: examples/06-git/.brigade
      ref: refs/heads/v2


Notice that there is no embedded script in this project definition. Rather, the project specifies where the Brigade config file directory (configFilesDirectory) should be located in the repository configured under the git section. (If not supplied, the default is to look for the .brigade directory at the git repository’s root).

The config file directory is where the Brigade script is placed. In this example, the brigade.js script is simply a console.log() statement.

console.log("Hello, Git!");

Let’s run the example:

$ brig event create --project git --follow

Created event "5ff386ed-060e-49fa-8292-0bade75f8840".

Waiting for event's worker to be RUNNING...
2021-09-22T21:38:10.667Z INFO: brigade-worker version: 927850b-dirty
Hello, Git!

Here’s what is happening behind the scenes when we create an event for this project: Because the project has a Git repository associated with it, Brigade is automatically fetching a clone of that repository and attaching it to the Worker in charge of running the script.

By default, the repository contents are not automatically mounted to jobs in the project’s Brigade script. However, mounting the contents to a job is easily accomplished via the sourceMountPath configuration on a job’s primaryContainer.

The following example shows how a job can be configured to access the repo in order to run a test target. It also configures workingDirectory with the same value as sourceMountPath so that the job needn’t worry about changing into the appropriate directory before running commands:

const { events, Job } = require("@brigadecore/brigadier");
const localPath = "/workspaces/brigade";

events.on("", "push", async event => {
  let test = new Job("test", "debian", event);
  test.primaryContainer.sourceMountPath = localPath;
  test.primaryContainer.workingDirectory = localPath;
  test.primaryContainer.command = ["bash"];
  test.primaryContainer.arguments = ["-c", "make test"];


Being able to associated a Git repository to a project is a convenient way to provide version-controlled data to our Brigade scripts. For instance, instead of embedding a project’s script and other configuration inside the project definition, these files can be fetched from source control.

Additionally, this functionality makes Brigade a great tool for executing CI pipelines, deployments, packaging tasks, end-to-end tests, and other DevOps tasks for a given repository.

Working with Event and Project Data

As we’ve seen, the event object is always passed into an event handler. This object also includes project data. Let’s look at the data we have access to.

The Brigade Event

From the event, we can find out what triggered the event, what data was sent with the event, the details of the worker in charge of running the event and more.

Here are some notable fields on the event object:

  • id is a unique, per-event ID. Every time a new event is triggered, a new ID will be generated.
  • project is the project that registered the handler for this event. We’ll look at the fields accessible on this object below.
  • source is the event source. The brig event create command, for example, would set source to
  • type is the event type. A GitHub Pull Request event, for example, would set type to pull_request.
  • payload contains any information that the external service sent when triggering the event. For example, a GitHub push request generates a rather large payload. Payload is an unparsed string.
  • worker is the Brigade worker assigned to handle the event. Among other things, git details such as the commit (revision ID) and clone URL can be found on this object.

For a full overview of the event object supplied by the brigadier library, see the Brigadier documentation.

The Project

The project object (event.project) gives us the following fields:

  • id is the project ID.
  • secrets is the key/value map of secrets defined on the project. These are set via brig secret set (see the Secrets Guide for more info).

Using Event and Project Objects

Let’s look at some examples that utilizes event and project data.

The first example extracts the payload from the event object and logs it to the console:

const { events } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  console.log("Hello, " + event.payload + "!");



When we create an event with a payload for the script above, we’ll see output like this:

$ brig event create --project first-payload --payload "Brigade" --follow

Created event "05e31d97-945b-4727-b710-7d983d137d40".

Waiting for event's worker to be RUNNING...
2021-09-23T16:25:29.761Z INFO: brigade-worker version: 927850b-dirty
Hello, Brigade!

We can update the example to print the project ID as well.

const { events } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  console.log("Project: " +;
  console.log("Hello, " + event.payload + "!");


The following output is generated:

$ brig event create --project first-payload --payload "Brigade" --follow

Created event "b45720c4-115c-4c9b-b668-a872479f2210".

Waiting for event's worker to be RUNNING...
2021-09-23T16:30:29.267Z INFO: brigade-worker version: 927850b-dirty
Project: first-payload
Hello, Brigade!

Note that some event and project data should be treated with care. Things like event.project.secrets or event.worker.git.cloneURL might not be the sorts of information you want accidentally displayed. Check out the Secrets guide for examples on how to safely handle project secrets in your scripts.

Worker storage and shared workspace

Brigade offers the ability to set up storage for the worker that can then be shared amongst jobs. This functionality isn’t enabled by default and needs to be configured on the workerTemplate section of the project definiton as well as on each job in the script that requires access to the workspace.

An example demonstrating use of a shared workspace

First, the useWorkspace field on the workerTemplate of the project definition must be set to true:

  useWorkspace: true

Next, for each job to use the shared workspace, provide a value for the workspaceMountPath field on the job’s primaryContainer:

const { events, Job } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  let job1 = new Job("first-job", "debian", event);
  job1.primaryContainer.workspaceMountPath = "/share";
  job1.primaryContainer.command = ["bash"];
  job1.primaryContainer.arguments = ["-c", "echo 'Hello!' > /share/message"];

  let job2 = new Job("second-job", "debian", event);
  job2.primaryContainer.workspaceMountPath = "/share";
  job2.primaryContainer.command = ["cat"];
  job2.primaryContainer.arguments = ["/share/message"];



$ brig event create --project shared-workspace

Created event "2eee9044-4469-49bd-a58b-aa659951a502".

$ brig event logs --id 2eee9044-4469-49bd-a58b-aa659951a502 --job second-job


Note: Shared storage is dependent on the underlying Kubernetes cluster and the availability of the correct type of storage class. See the Storage doc for more information on cluster requirements.

Sidecar containers

Jobs can optionally be configured with one or more sidecar containers, which run alongside the job’s primary container. All sidecar containers will be terminated a short time after the job’s primary container completes. A few additional notes:

  • Regardless of whether sidecar containers are present, job success or failure is still determined solely upon the exit code of its primary container.

  • All the containers are networked together such that processes listening for network connections in any one of them can be addressed by processes running in the others using the local network interface.

  • Brigade does not understand the relationship(s) between your containers and therefore cannot coordinate startup. If, for instance, a process in the primary container should be delayed until some supplementary process in some sidecar container is up and running and listening for connections, then the script author needs to account for that themselves.

As an example, consider an event handler that needs to run tests but also needs to provision a backing database required by the tests. The backing database could run as a sidecar container while the job’s primary container is concerned with running the tests.

Another use case is a job that needs the Docker daemon. The safest way to do this is to run Docker-in-Docker, i.e. starting up the daemon within a container that can then be used as needed. A sidecar container is a perfect application for starting up the daemon whilst the job’s primary container handles the main tasks at hand.

Let’s take a look at this latter example and introduce further configuration needed for this scenario.


Docker-in-Docker (DinD) containers must run as privileged in order to function. This also needs to be allowed at the project level in order for jobs to run as privileged. The default is for this feature to be disallowed.

Here is an example project configuration allowing containers to run in privileged mode:

    allowPrivileged: true

Each job container needing to run in this mode must also add explicit configuration. In the example below, the docker sidecar container has privileged set to true, while the primary container does not:

const { events, Job, Container } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  let job = new Job("dind", "docker:stable-dind", event);
  job.primaryContainer.environment.DOCKER_HOST = "localhost:2375";
  job.primaryContainer.command = ["sh"];
  job.primaryContainer.arguments = [
    // Wait for the Docker daemon to start up
    // And then pull the image
    "sleep 20 && docker pull busybox"

  // Run the Docker daemon in a sidecar container
  job.sidecarContainers = {
    "docker": new Container("docker:stable-dind")
  job.sidecarContainers.docker.privileged = true



Here’s the output from creating an event and then looking at the job logs:

$ brig event create --project dind --follow

Created event "94d0fcd5-61dc-49be-bb81-3e5784e66a4a".

Waiting for event's worker to be RUNNING...
2021-09-23T19:00:08.915Z INFO: brigade-worker version: 927850b-dirty
2021-09-23T19:00:09.428Z [job: dind] INFO: Creating job dind

$ brig event logs --id 94d0fcd5-61dc-49be-bb81-3e5784e66a4a --job dind

Using default tag: latest
latest: Pulling from library/busybox
24fb2886d6f6: Pulling fs layer
24fb2886d6f6: Verifying Checksum
24fb2886d6f6: Download complete
24fb2886d6f6: Pull complete
Digest: sha256:52f73a0a43a16cf37cd0720c90887ce972fe60ee06a687ee71fb93a7ca601df7
Status: Downloaded newer image for busybox:latest

Note we could also take a look at the sidecar container logs on the job like so:

$ brig event logs --id 94d0fcd5-61dc-49be-bb81-3e5784e66a4a --job dind --container docker

time="2021-09-23T19:00:11.230907700Z" level=info msg="Starting up"

Accessing the host Docker socket

For security reasons, it is recommended that you use Docker-in-Docker (DinD) instead of using a host’s Docker socket directly.

However, each job also has the option to mount in a docker socket. When enabled, the docker socket from the Kubernetes Node running the job Pod is mounted to /var/run/docker.sock in the job’s container. This is typically required only for “Docker-out-of-Docker” (“DooD”) scenarios where the container needs to use the host’s Docker daemon. This is strongly discouraged for almost all use cases.

In order for the socket to be mounted, the Brigade project must have the allowDockerSocket field on the jobPolicies section of its worker spec set to true. The default is false, disallowing use of the host docker socket.

Example project configuration enabling this feature:

    allowDockerSocketMount: true

Additionally, a job must declare that it needs a docker socket by setting useHostDockerSocket on its primaryContainer to true:

const { events, Job } = require("@brigadecore/brigadier");

events.on("", "exec", async event => {
  let job = new Job("dood", "docker", event);
  job.primaryContainer.useHostDockerSocket = true;
  job.primaryContainer.command = ["docker"];
  job.primaryContainer.arguments = ["ps"];


Here’s the output when we create an event for the script above:

$ brig event create --project dood --follow

Created event "283be00c-5481-43ae-8634-bd9bd194488b".

Waiting for event's worker to be RUNNING...
2021-09-23T19:50:27.796Z INFO: brigade-worker version: cfa7e5e-dirty
2021-09-23T19:50:27.994Z [job: dood] INFO: Creating job dood

$ brig event logs --id 283be00c-5481-43ae-8634-bd9bd194488b --job dood

CONTAINER ID   IMAGE                         COMMAND                  CREATED          STATUS                  PORTS     NAMES
68ab8fa3f395   docker                        "docker ps"              1 second ago     Up Less than a second             k8s_dood_283be00c-5481-43ae-8634-bd9bd194488b-dood_brigade-2450aa5d-80be-442b-bdd2-425a4d85c3e9_e2bb2688-8362-4824-91a0-797d0396ba04_0

Note: Not all cluster providers use Docker as their container runtime. For example, KinD uses containerd and so the usual Docker socket is not available. Here we mention again that DinD is the preferred route when a Docker socket is necessary.


This guide covers the basics of writing Brigade scripts. Here are some links for further reading:

  • If you’d like more details about the Brigadier JS/TS API, take a look at the Brigadier docs
  • For a more advanced script examples and techniques, see the Advanced Scripting Guide
  • Peruse the other example projects/scripts in the Examples directory. There are example projects using npm, yarn, TypeScript and more.

Happy Scripting!