Previous notes:
- 001 - Designing my Workflow Engine
- 002 - Kubernetized
- 003 - New UI, Deadlocks, and AI Overengineering
- 004 - Cloud-Native Graceful Shutdowns
- 005 - Quotas and Limits
- 006 - Github Integration
Earlier this week I was contemplating on how to add support for Conditional Jobs to Runbook. I already have some sort of conditional execution; you can specify a list of steps upon the current step depends_on, and the current step will only get executed after the steps it depends on have finished successfully. In case one of them fails, the workflow fails as well, pretty straight forward.
But the depends_on is a sort of “static condition”: it only sets the order of execution, and the execution can’t be stopped or started based on some arbitrary checks set by the user. What I wanted with this Conditional execution was something smarter: a way for workflows to make decisions at runtime, not just based on whether the previous step passed or failed.
A CI/CD Example
Consider a CI/CD pipeline where you don’t always want to deploy automatically. Sometimes deployment should only happen:
- After someone manually approves it, AND
- When it’s triggered from specific branches
For example:
name: "Deploy to Vercel"
steps:
# ...
- name: "Deploy"
depends_on:
- "Run Tests"
if: '(branch == "main" or branch == "qa") and approved_by(@aziflaj)'
command: pnpm exec vercel --logs --target=$ENVrunbook.yml
The "Deploy" step always happens if the "Run Tests" step succeeds, but before it runs it has to check some conditions:
- Is the triggering branch
mainorqa? - Has
@aziflajapproved this step?
If either condition fails, the step is skipped (or stopped for approval)
Evaluating booleans
I considered 3 different approaches for evaluating these expressions:
- Embedd a JS interpreter, something like robertkrimen/otto and evaluate expressions written in a language the user (most likely) already knows
- Write my own boolean expression parser using goyacc and some reading on Backus-Naur form
- Remember I’ve already done this last year, and PinguLang can already evaluate boolean expressions, among other things
So I decided to add my old project as a dependency of my current project:

PinguLang lets me inject variables and intrinsic functions in the scope of the execution, like this:
// source -> lexer -> tokens -> parser -> AST -> eval
lxr := lexer.New(string(step.If))
prs := parser.New(lxr)
program := prs.ParseProgram()
scope := object.NewScope()
scope.Set("branch", &object.String(Value: []rune("qa"))) // the triggering branch
scope.Set(
"approved_by",
&object.IntrinsicFunc(Value: func(args ...object.Object) object.Object {
// [redacted implementation]
return &object.Boolean(Value: true)
}),
)
result := eval.Eval(scope, program)
result.IsTruthy() // the evaluated boolean we expectcond.go
Putting It to the Test
After wiring this up, I ran a worƒlow that has both passing and failing conditions:

That "Step 2 Cond F" is grayed out because its condition is evaluated to false. And how it looks like in the yml is:
name: "Testing Docker & Compose"
global:
env:
TEXT: "Hello World"
BOOL_TRUE: true
TRUTHY: true
steps:
- name: "Step 1"
command: printenv
- name: "Step 2 Cond T"
depends_on:
- "Step 1"
if: "$BOOL_TRUE == $TRUTHY" # evaluates to true
env:
TEXT: "Running Step 2 Cond T"
command: printenv
- name: "Step 2 Cond F"
depends_on:
- "Step 1"
env:
TEXT: "Running Step 2 Cond F"
if: "$BOOL_TRUE != $TRUTHY" # evaluates to false
command: printenv
- name: "Run via Docker Compose"
depends_on:
- "Step 1"
env:
BUILDKIT_PROGRESS: "plain"
command: |
repo_cloner
cd runbook-activated
ls -la
docker compose build --quiet
docker compose up -d redis
docker compose run --rm app
docker compose downrunbook.yml
Global Env Vars
As a side quest, I also added support for globally defined environment variables, which can be overridden per step as needed. These globals are also exposed to the if conditions as $ENVVARS, so you can reference and compare them dynamically within your expressions.
Every week, we get one step closer to the first closed beta testers for Runbook.