← Back to blog

From Jupyter Notebooks to Operational Runbooks

· 5 min read · Stew Team
jupyterrunbookmigrationdevops

Jupyter notebooks are fantastic for data science. But if you’re using them for operational runbooks, there’s a better way.

This guide helps you understand when Jupyter fits and when markdown runbooks are the better choice. For tool comparisons, see our interactive runbook tools comparison.

Why Teams Start with Jupyter

Jupyter’s appeal for ops is understandable:

  • Execution built in: Click to run cells
  • Output captured: See results inline
  • State preserved: Variables persist between cells
  • Familiar: Many engineers have used it

A typical ops notebook looks like:

# Cell 1: Setup
import subprocess
import json

def run(cmd):
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    print(result.stdout)
    if result.returncode != 0:
        print(f"Error: {result.stderr}")
    return result

# Cell 2: Check pods
run("kubectl get pods")

# Cell 3: Check logs
run("kubectl logs -l app=api --tail=50")

It works. But there are hidden costs.

The Hidden Costs of Jupyter for Ops

Cost 1: The Python Wrapper Tax

Every shell command needs a Python wrapper:

# What you want to run
kubectl get pods

# What you actually write
import subprocess
result = subprocess.run(["kubectl", "get", "pods"], capture_output=True, text=True)
print(result.stdout)

This adds:

  • Boilerplate code
  • Escaping complexity
  • Debugging friction

Cost 2: Environment Complexity

To run a Jupyter notebook:

# Requirements
python3 -m venv venv
source venv/bin/activate
pip install jupyter
pip install your-dependencies

# Start
jupyter notebook

During an incident, this setup time matters.

Cost 3: The JSON Problem

Notebook files are JSON:

{
  "cells": [{
    "cell_type": "code",
    "source": ["run(\"kubectl get pods\")"]
  }]
}

This means:

  • Git diffs are noisy
  • Merge conflicts are painful
  • Raw files are unreadable

Cost 4: Output Bloat

Notebooks store outputs:

{
  "outputs": [{
    "output_type": "stream",
    "text": ["NAME                   READY   STATUS    RESTARTS   AGE\n",
             "api-7d4f8b6c9-x2k4j   1/1     Running   0          2d\n",
             "... 50 more lines ..."]
  }]
}

Your repo fills with stale command output.

Cost 5: Collaboration Challenges

Two people edit the same notebook:

<<<<<<< HEAD
    "execution_count": 15,
=======
    "execution_count": 23,
>>>>>>> feature-branch

Merge conflicts on execution counts, outputs, and metadata.

When Jupyter Is Right

Keep Jupyter for:

Use CaseWhy Jupyter Fits
Data analysisRich visualizations, dataframes
Machine learningModel training, metrics tracking
API explorationInteractive testing with state
PrototypingQuick iteration with outputs

When Runbooks Are Better

Switch to markdown runbooks for:

Use CaseWhy Markdown Fits
Incident responseReadability, no setup time
Standard proceduresGit-friendly, reviewable
Team documentationUniversal access
Automated executionClean parsing

Migration Path

Step 1: Identify Runbook Candidates

Look for notebooks that are:

  • Primarily shell commands
  • Used by multiple team members
  • Referenced during incidents
  • Mostly linear execution

Step 2: Extract the Commands

From this notebook:

# Check service health
run("kubectl get pods -l app=api")

# Check recent logs
run("kubectl logs -l app=api --tail=100 | grep -i error")

# Restart if needed
run("kubectl rollout restart deployment/api")

To this markdown:

# API Service Health Check

## Check service health
​```bash
kubectl get pods -l app=api
​```

## Check recent logs
​```bash
kubectl logs -l app=api --tail=100 | grep -i error
​```

## Restart if needed
​```bash
kubectl rollout restart deployment/api
​```

Step 3: Add Context

Notebooks often lack context because the author “just knew”:

# API Service Health Check

Use this runbook when the API health alert fires.

## Prerequisites
- kubectl configured for production
- Access to production namespace

## Check service health

First, verify pods are running:

​```bash
kubectl get pods -l app=api
​```

**Healthy**: All pods show Running, READY 1/1
**Unhealthy**: Pods in CrashLoopBackOff or not Ready

## Check recent logs

Look for error patterns:

​```bash
kubectl logs -l app=api --tail=100 | grep -i error
​```

**If you see** "connection refused": Database may be down
**If you see** "OOM killed": Memory limits too low

## Restart if needed

Only restart after identifying the issue:

​```bash
kubectl rollout restart deployment/api
​```

Wait 60 seconds, then verify with the health check above.

Step 4: Choose Execution Method

Options for making markdown executable:

MethodTradeoff
Copy-pasteUniversal but manual
Runme (VS Code)Easy but requires VS Code
StewWeb + terminal, built for this

Keeping What Works

You can keep Jupyter for what it’s good at while using markdown runbooks for ops:

docs/
├── runbooks/           # Markdown runbooks
│   ├── api-health.md
│   └── db-recovery.md
├── notebooks/          # Jupyter for analysis
│   ├── metrics-analysis.ipynb
│   └── incident-review.ipynb

Conversion Script

Automate simple conversions:

#!/usr/bin/env python3
"""Convert Jupyter notebook shell commands to markdown runbook."""

import json
import sys
import re

def convert(notebook_path):
    with open(notebook_path) as f:
        nb = json.load(f)
    
    output = []
    for cell in nb['cells']:
        if cell['cell_type'] == 'markdown':
            output.append(''.join(cell['source']))
            output.append('')
        elif cell['cell_type'] == 'code':
            source = ''.join(cell['source'])
            # Extract shell commands from run() calls
            matches = re.findall(r'run\(["\'](.+?)["\']\)', source)
            for cmd in matches:
                output.append('```bash')
                output.append(cmd)
                output.append('```')
                output.append('')
    
    return '\n'.join(output)

if __name__ == '__main__':
    print(convert(sys.argv[1]))

Stew: Built for Runbooks

Stew is purpose-built for operational runbooks:

  • Markdown-native: No format conversion needed
  • Shell-first: Bash commands run directly
  • Git-friendly: Clean diffs, easy reviews
  • Team-ready: Share without environment setup

Your team gets execution without Jupyter’s overhead.

Join the waitlist and simplify your operational docs.