> ## Documentation Index
> Fetch the complete documentation index at: https://hoopdev-feat-new-runbook-parameters.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Runbooks

> Create reusable, parameterized templates for common operations

export const RunbookAnimation = () => {
  const WG = 'var(--warm-gold)';
  const rgba = a => `rgba(var(--sand-100-rgb),${a})`;
  const useTypewriter = (text, active, speed = 65) => {
    const [out, setOut] = useState('');
    const [done, setDone] = useState(false);
    useEffect(() => {
      if (!active) {
        setOut('');
        setDone(false);
        return;
      }
      let i = 0;
      setOut('');
      setDone(false);
      const iv = setInterval(() => {
        i++;
        setOut(text.slice(0, i));
        if (i >= text.length) {
          clearInterval(iv);
          setDone(true);
        }
      }, speed);
      return () => clearInterval(iv);
    }, [text, active, speed]);
    return [out, done];
  };
  const Overline = ({children, style}) => {
    return <div style={{
      fontFamily: 'var(--sans)',
      fontSize: 10,
      fontWeight: 600,
      color: WG,
      textTransform: 'uppercase',
      letterSpacing: '0.1em',
      marginBottom: 10,
      ...style
    }}>
        {children}
      </div>;
  };
  const Check = ({checked, label}) => {
    return <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: 7,
      fontFamily: 'var(--sans)',
      fontSize: 11.5,
      color: checked ? rgba(0.55) : rgba(0.25),
      transition: 'color 0.3s'
    }}>
        <div style={{
      width: 16,
      height: 16,
      borderRadius: 3,
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      background: checked ? 'rgba(var(--warm-gold-rgb),0.12)' : rgba(0.03),
      border: checked ? '1px solid rgba(var(--warm-gold-rgb),0.3)' : `1px solid ${rgba(0.08)}`,
      transition: 'all 0.4s'
    }}>
          {checked ? <svg width="9" height="9" viewBox="0 0 16 16" fill="none"><path d="M3 8.5L6.5 12L13 4" stroke={WG} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" /></svg> : <div style={{
      width: 7,
      height: 7,
      borderRadius: '50%',
      border: `2px solid ${rgba(0.12)}`,
      borderTopColor: 'rgba(var(--warm-gold-rgb),0.4)',
      animation: 'rb-spin 0.8s linear infinite'
    }} />}
        </div>
        {label}
      </div>;
  };
  const Connector = ({visible}) => {
    return <div style={{
      display: 'flex',
      justifyContent: 'center',
      height: visible ? 16 : 0,
      overflow: 'hidden',
      transition: 'height 0.4s cubic-bezier(.4,0,.2,1)'
    }}>
        <div style={{
      width: 1,
      height: '100%',
      background: 'linear-gradient(to bottom, rgba(var(--warm-gold-rgb),0.25), transparent)'
    }} />
      </div>;
  };
  const CODE_LINES = [{
    t: 'comment',
    v: '-- customer-lookup.runbook.sql'
  }, {
    t: 'kw',
    v: 'SELECT'
  }, {
    t: 'f',
    v: '  customer_id, name, email, status'
  }, {
    t: 'kw',
    v: 'FROM customers'
  }, {
    t: 'kw',
    v: 'WHERE'
  }, {
    t: 'param',
    v: '  customer_id = {{ .customer_id | required | type "number" }}'
  }];
  const PHASES = [{
    label: 'Template',
    ms: 3000
  }, {
    label: 'Lens view',
    ms: 5000
  }, {
    label: 'Review',
    ms: 2500
  }, {
    label: 'Results',
    ms: 4000
  }];
  const [phase, setPhase] = useState(0);
  const [checks, setChecks] = useState(0);
  const [lensPos, setLensPos] = useState(0);
  const [typed, typedDone] = useTypewriter('40192', phase >= 1, 75);
  useEffect(() => {
    let t;
    const run = p => {
      t = setTimeout(() => {
        const n = (p + 1) % 4;
        setPhase(n);
        if (n === 0) {
          setChecks(0);
          setLensPos(0);
        }
        run(n);
      }, PHASES[p].ms);
    };
    run(0);
    return () => clearTimeout(t);
  }, []);
  useEffect(() => {
    if (phase !== 1) return;
    setLensPos(0);
    const start = performance.now();
    const duration = 1800;
    let frameId;
    const animate = now => {
      const elapsed = now - start;
      const progress = Math.min(elapsed / duration, 1);
      const eased = 1 - Math.pow(1 - progress, 3);
      setLensPos(eased * 100);
      if (progress < 1) frameId = requestAnimationFrame(animate);
    };
    frameId = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(frameId);
  }, [phase]);
  useEffect(() => {
    if (phase !== 2) {
      setChecks(0);
      return;
    }
    let c = 0;
    const iv = setInterval(() => {
      c++;
      setChecks(c);
      if (c >= 3) clearInterval(iv);
    }, 600);
    return () => clearInterval(iv);
  }, [phase]);
  const lensActive = phase >= 1;
  const lensWidth = lensActive ? lensPos : 0;
  return <div className="rb-root" aria-hidden="true">
      <style>{`
        @keyframes rb-blink { 0%,100%{opacity:1} 50%{opacity:0} }
        @keyframes rb-up { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
        @keyframes rb-spin { to{transform:rotate(360deg)} }
        @keyframes rb-pulse { 0%,100%{opacity:.12} 50%{opacity:.2} }

        .rb-root {
          position: relative;
          width: 100%;
          max-width: 580px;
        }
      `}</style>

      <div style={{
    background: 'linear-gradient(135deg, var(--gradient-dark-start) 0%, var(--gradient-dark-mid) 35%, var(--gradient-dark-end) 70%, var(--bronze) 100%)',
    borderRadius: 14,
    overflow: 'hidden',
    position: 'relative',
    fontFamily: 'var(--sans)'
  }}>
        <div style={{
    position: 'absolute',
    top: '-30%',
    right: '-10%',
    width: '60%',
    height: '160%',
    background: 'radial-gradient(ellipse at center, rgba(var(--warm-gold-rgb),0.12) 0%, transparent 70%)',
    pointerEvents: 'none',
    animation: 'rb-pulse 4s ease-in-out infinite'
  }} />

        {}
        <div style={{
    position: 'relative',
    padding: '14px 22px',
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    borderBottom: `1px solid ${rgba(0.06)}`
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: 8
  }}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="var(--sand-100)">
              <circle cx="7" cy="7" r="3.5" /><circle cx="17" cy="7" r="3.5" />
              <circle cx="7" cy="17" r="3.5" /><circle cx="17" cy="17" r="3.5" />
            </svg>
            <span style={{
    fontSize: 13,
    fontWeight: 700,
    color: 'var(--sand-100)'
  }}>Runbooks</span>
          </div>
          <div style={{
    display: 'flex',
    gap: 6
  }}>
            {PHASES.map((_, i) => <div key={i} style={{
    width: i === phase ? 22 : 7,
    height: 7,
    borderRadius: 4,
    background: i === phase ? WG : i < phase ? 'rgba(var(--warm-gold-rgb),0.4)' : rgba(0.1),
    transition: 'all 0.5s cubic-bezier(.4,0,.2,1)'
  }} />)}
          </div>
          <div style={{
    fontSize: 11,
    fontWeight: 500,
    color: rgba(0.3)
  }}>{PHASES[phase].label}</div>
        </div>

        {}
        <div style={{
    position: 'relative',
    padding: '20px 24px',
    display: 'flex',
    flexDirection: 'column',
    gap: 0,
    minHeight: 480
  }}>

          {}
          <div style={{
    position: 'relative',
    borderRadius: 10,
    overflow: 'hidden',
    minHeight: 250
  }}>

            {}
            <div style={{
    background: rgba(0.04),
    border: `1px solid ${rgba(0.08)}`,
    borderRadius: 10,
    padding: '16px 20px',
    fontFamily: 'var(--mono)',
    fontSize: 12.5,
    lineHeight: 2,
    position: 'relative',
    zIndex: 1
  }}>
              <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginBottom: 8
  }}>
                <Overline style={{
    marginBottom: 0
  }}>Template</Overline>
                <div style={{
    fontFamily: 'var(--mono)',
    fontSize: 9,
    color: rgba(0.2)
  }}>customer-lookup.runbook.sql</div>
              </div>
              {CODE_LINES.map((l, i) => {
    let color = rgba(0.55);
    let bg = 'transparent';
    let weight = 400;
    if (l.t === 'comment') color = 'var(--sand-500)';
    if (l.t === 'kw') color = WG;
    if (l.t === 'param') {
      color = WG;
      bg = 'rgba(var(--warm-gold-rgb),0.08)';
      weight = 500;
    }
    return <div key={i} style={{
      color,
      fontWeight: weight,
      background: bg,
      borderRadius: l.t === 'param' ? 4 : 0,
      padding: l.t === 'param' ? '2px 6px' : 0,
      margin: l.t === 'param' ? '4px -6px 0' : 0,
      border: l.t === 'param' ? '1px solid rgba(var(--warm-gold-rgb),0.15)' : 'none'
    }}>
                    {l.v}
                  </div>;
  })}
            </div>

            {}
            <div style={{
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    width: `${lensWidth}%`,
    overflow: 'hidden',
    zIndex: 2,
    borderRadius: 10,
    transition: phase === 0 ? 'width 0.3s' : 'none'
  }}>
              <div style={{
    background: 'rgba(var(--espresso-rgb),0.95)',
    backdropFilter: 'blur(12px)',
    WebkitBackdropFilter: 'blur(12px)',
    border: '1px solid rgba(var(--sand-100-rgb),0.08)',
    borderRadius: 10,
    padding: '16px 20px',
    width: '100%',
    minWidth: 0,
    height: '100%',
    boxSizing: 'border-box',
    display: 'flex',
    flexDirection: 'column'
  }}>
                <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginBottom: 14
  }}>
                  <Overline style={{
    marginBottom: 0
  }}>Customer Lookup</Overline>
                  <div style={{
    fontFamily: 'var(--sans)',
    fontSize: 9,
    color: rgba(0.25),
    display: 'flex',
    alignItems: 'center',
    gap: 4
  }}>
                    <div style={{
    width: 5,
    height: 5,
    borderRadius: '50%',
    background: WG
  }} />
                    Form view
                  </div>
                </div>

                <div style={{
    marginBottom: 14
  }}>
                  <div style={{
    fontSize: 12,
    fontFamily: 'var(--sans)',
    fontWeight: 500,
    color: rgba(0.5),
    marginBottom: 5
  }}>
                    Customer ID <span style={{
    fontSize: 9,
    color: 'rgba(var(--warm-gold-rgb),0.5)',
    fontStyle: 'italic'
  }}>required · number</span>
                  </div>
                  <div style={{
    background: rgba(0.03),
    border: typedDone ? '1px solid rgba(var(--warm-gold-rgb),0.35)' : `1px solid ${rgba(0.1)}`,
    borderRadius: 6,
    padding: '9px 12px',
    fontFamily: 'var(--mono)',
    fontSize: 13,
    color: 'var(--sand-100)',
    minHeight: 18,
    transition: 'border-color 0.3s',
    display: 'flex',
    alignItems: 'center'
  }}>
                    {typed}
                    {phase === 1 && !typedDone && <span style={{
    display: 'inline-block',
    width: 2,
    height: 15,
    background: WG,
    marginLeft: 1,
    animation: 'rb-blink 1s step-end infinite'
  }} />}
                  </div>
                </div>

                <div style={{
    marginBottom: 14
  }}>
                  <div style={{
    fontSize: 12,
    fontFamily: 'var(--sans)',
    fontWeight: 500,
    color: rgba(0.5),
    marginBottom: 5
  }}>Output columns</div>
                  <div style={{
    display: 'flex',
    gap: 6,
    flexWrap: 'wrap'
  }}>
                    {['customer_id', 'name', 'email', 'status'].map(f => <span key={f} style={{
    background: rgba(0.04),
    border: `1px solid ${rgba(0.08)}`,
    borderRadius: 4,
    padding: '3px 8px',
    fontFamily: 'var(--mono)',
    fontSize: 10,
    color: rgba(0.3)
  }}>{f}</span>)}
                  </div>
                </div>

                {typedDone && <div style={{
    display: 'inline-flex',
    alignItems: 'center',
    gap: 5,
    alignSelf: 'flex-start',
    background: 'rgba(var(--warm-gold-rgb),0.08)',
    border: '1px solid rgba(var(--warm-gold-rgb),0.2)',
    borderRadius: 14,
    padding: '4px 10px',
    fontSize: 10,
    fontWeight: 500,
    color: WG,
    animation: 'rb-up 0.3s ease-out'
  }}>
                    <svg width="10" height="10" viewBox="0 0 16 16" fill="none">
                      <path d="M3 8.5L6.5 12L13 4" stroke={WG} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
                    </svg>
                    Ready to execute
                  </div>}
              </div>

              {lensActive && lensWidth < 100 && <div style={{
    position: 'absolute',
    top: 0,
    right: -1,
    bottom: 0,
    width: 3,
    background: 'linear-gradient(to bottom, rgba(var(--warm-gold-rgb),0.5), rgba(var(--warm-gold-rgb),0.1))',
    borderRadius: 2,
    boxShadow: '0 0 12px rgba(var(--warm-gold-rgb),0.3)'
  }} />}
            </div>
          </div>

          <Connector visible={phase >= 2} />

          {}
          <div style={{
    background: rgba(0.04),
    border: `1px solid ${rgba(0.08)}`,
    borderRadius: 10,
    padding: '14px 18px',
    opacity: phase >= 2 ? 1 : 0,
    maxHeight: phase >= 2 ? 160 : 0,
    overflow: 'hidden',
    transition: 'opacity 0.4s 0.1s, max-height 0.4s cubic-bezier(.4,0,.2,1)'
  }}>
            <Overline>Access Review</Overline>
            <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: 7
  }}>
              <Check checked={checks >= 1} label="Guardrails passed" />
              <Check checked={checks >= 2} label="Data masking enabled" />
              <Check checked={checks >= 3} label="Approved by policy" />
            </div>
          </div>

          <Connector visible={phase >= 3} />

          {}
          <div style={{
    background: rgba(0.04),
    border: `1px solid ${rgba(0.08)}`,
    borderRadius: 10,
    padding: 0,
    overflow: 'hidden',
    opacity: phase >= 3 ? 1 : 0,
    maxHeight: phase >= 3 ? 200 : 0,
    transition: 'opacity 0.4s 0.1s, max-height 0.4s cubic-bezier(.4,0,.2,1)'
  }}>
            <div style={{
    background: rgba(0.03),
    borderBottom: `1px solid ${rgba(0.06)}`,
    padding: '8px 14px',
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center'
  }}>
              <Overline style={{
    marginBottom: 0
  }}>Results</Overline>
              <span style={{
    fontFamily: 'var(--mono)',
    fontSize: 9,
    color: rgba(0.2)
  }}>1 row · 42ms</span>
            </div>
            <div style={{
    padding: '0 14px'
  }}>
              <div style={{
    display: 'grid',
    gridTemplateColumns: '50px 1fr 1fr 60px',
    padding: '6px 0',
    borderBottom: `1px solid ${rgba(0.04)}`,
    fontFamily: 'var(--sans)',
    fontSize: 9,
    fontWeight: 600,
    color: rgba(0.25),
    textTransform: 'uppercase'
  }}>
                <span>ID</span><span>Name</span><span>Email</span><span>Status</span>
              </div>
              <div style={{
    display: 'grid',
    gridTemplateColumns: '50px 1fr 1fr 60px',
    padding: '8px 0',
    fontFamily: 'var(--mono)',
    fontSize: 11,
    color: rgba(0.55),
    animation: phase >= 3 ? 'rb-up 0.4s ease-out' : 'none'
  }}>
                <span>40192</span>
                <span>Acme Corp</span>
                <span style={{
    color: WG,
    fontWeight: 500
  }}>a•••@acme.io</span>
                <span>
                  <span style={{
    background: 'rgba(var(--warm-gold-rgb),0.08)',
    border: '1px solid rgba(var(--warm-gold-rgb),0.2)',
    borderRadius: 10,
    padding: '1px 6px',
    fontSize: 8.5,
    fontFamily: 'var(--sans)',
    fontWeight: 600,
    color: WG,
    textTransform: 'uppercase'
  }}>active</span>
                </span>
              </div>
            </div>
            <div style={{
    borderTop: `1px solid ${rgba(0.04)}`,
    padding: '7px 14px',
    display: 'flex',
    gap: 10,
    fontSize: 9,
    color: rgba(0.18)
  }}>
              <span>Recorded</span><span>·</span><span>Audited</span><span>·</span><span>Masked</span>
            </div>
          </div>
        </div>
      </div>
    </div>;
};

<div style={{background: 'linear-gradient(135deg, #111111 0%, #1A1A1A 35%, #2A2A2A 70%, #3A3A3A 100%)', borderRadius: 14, overflow: 'hidden', padding: 24, height: 420}}>
  <RunbookAnimation />
</div>

## What You'll Accomplish

Runbooks let you create reusable templates for common operations. Instead of typing the same queries or commands repeatedly, you can:

* Create parameterized templates stored in Git
* Share approved procedures across your team
* Enforce input validation and safe defaults
* Maintain an audit trail of every execution

***

## How It Works

<Steps>
  <Step title="Create Template">
    Write a runbook template with placeholders for parameters
  </Step>

  <Step title="Store in Git">
    Commit the template to your Git repository
  </Step>

  <Step title="Configure Hoop">
    Point Hoop to your repository
  </Step>

  <Step title="Execute">
    Users select the runbook, fill in parameters, and run
  </Step>
</Steps>

### Example Workflow

1. **DBA creates** a runbook for looking up customer data
2. **Template stored** in `runbooks/customer-lookup.runbook.sql`
3. **Support team** selects the runbook, enters customer ID
4. **Query executes** with proper validation and audit logging

***

## Quick Start

## Prerequisites

To get the most out of this guide, you will need to:

* Either [create an account in our managed instance](https://use.hoop.dev) or [deploy your own hoop.dev instance](/setup/deployment/overview)
* You must be your account administrator to perform the following actions

- A Git repository for storing runbooks
- Git access credentials (SSH key or token)

### Step 1: Create a Runbook File

Create a file in your repository with the `.runbook.<ext>` extension:

**`runbooks/customer-lookup.runbook.sql`**

```sql theme={null}
SELECT customer_id, name, email, created_at
FROM customers
WHERE customer_id = {{ .customer_id
    | description "Customer ID to look up"
    | required "Customer ID is required"
    | type "number" }}
```

### Step 2: Configure Git Integration

In the Web App:

1. Go to **Manage > Runbooks**
2. Click **Configure Repository**
3. Enter your repository URL and credentials

Or via CLI:

```bash theme={null}
hoop admin create plugin runbooks \
  -c GIT_URL=git@github.com:your-org/runbooks.git \
  -c GIT_SSH_KEY=file://$HOME/.ssh/deploy_key
```

### Step 3: Run the Runbook

1. Go to **Runbooks** in the sidebar
2. Select `customer-lookup`
3. Enter the customer ID
4. Click **Execute**

***

## Template Syntax

Runbooks use Go's `text/template` syntax. Parameters are defined with `{{ .parameter_name }}`.

### Basic Parameter

```sql theme={null}
SELECT * FROM orders WHERE id = {{ .order_id }}
```

### Parameter with Validation

```sql theme={null}
SELECT * FROM users
WHERE email = {{ .email
    | description "User email address"
    | required "Email is required"
    | pattern "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" }}
```

### Parameter Functions

| Function      | Description                                          | Example                             |
| ------------- | ---------------------------------------------------- | ----------------------------------- |
| `description` | Help text for the input                              | `description "Customer ID"`         |
| `required`    | Error message if empty                               | `required "This field is required"` |
| `default`     | Default value if not provided                        | `default "US"`                      |
| `placeholder` | Placeholder text shown inside the input              | `placeholder "e.g. 12345"`          |
| `type`        | Input type for UI                                    | `type "number"`                     |
| `pattern`     | Regex validation                                     | `pattern "^[0-9]+$"`                |
| `options`     | Dropdown options                                     | `options "active" "inactive"`       |
| `order`       | Integer that controls field order in the form        | `order "1"`                         |
| `minlength`   | Minimum character length                             | `minlength "3"`                     |
| `maxlength`   | Maximum character length                             | `maxlength "100"`                   |
| `asenv`       | Pass value as environment variable instead of inline | `asenv "API_KEY"`                   |
| `squote`      | Wrap value in single quotes                          | `squote`                            |
| `dquote`      | Wrap value in double quotes                          | `dquote`                            |
| `quotechar`   | Wrap value with a custom character                   | `quotechar "$"`                     |
| `encodeb64`   | Encode value as base64                               | `encodeb64`                         |
| `decodeb64`   | Decode a base64 value                                | `decodeb64`                         |

### Input Types

| Type       | UI Behavior                             |
| ---------- | --------------------------------------- |
| `text`     | Standard text input                     |
| `number`   | Numeric input                           |
| `email`    | Email validation                        |
| `date`     | Date picker                             |
| `time`     | Time picker                             |
| `tel`      | Phone number input                      |
| `url`      | URL input                               |
| `password` | Masked text input with show/hide toggle |
| `textarea` | Multi-line text input                   |
| `file`     | File upload — value sent as base64      |
| `select`   | Dropdown (use with `options`)           |

***

## Example Runbooks

### SQL: Customer Lookup

```sql theme={null}
-- runbooks/sql/customer-lookup.runbook.sql
SELECT
    customer_id,
    name,
    email,
    status,
    created_at
FROM customers
WHERE customer_id = {{ .customer_id
    | description "Customer ID"
    | required "Customer ID is required"
    | type "number"
    | squote }}
```

### SQL: Update Order Status

```sql theme={null}
-- runbooks/sql/update-order-status.runbook.sql
UPDATE orders
SET
    status = {{ .new_status
        | description "New order status"
        | type "select"
        | options "pending" "processing" "shipped" "delivered"
        | required "Status is required"
        | squote }},
    updated_at = NOW()
WHERE order_id = {{ .order_id
    | description "Order ID to update"
    | required "Order ID is required"
    | type "number" }}
```

### Bash: Service Restart

```bash theme={null}
# runbooks/ops/restart-service.runbook.sh
#!/bin/bash
SERVICE_NAME={{ .service
    | description "Service to restart"
    | type "select"
    | options "nginx" "postgres" "redis" "myapp"
    | required "Service name is required" }}

echo "Restarting $SERVICE_NAME..."
sudo systemctl restart $SERVICE_NAME
sudo systemctl status $SERVICE_NAME
```

### Python: Data Export

```python theme={null}
# runbooks/data/export-report.runbook.py
# {{ .start_date | description "Start date (YYYY-MM-DD)" | required "Start date required" | asenv "START_DATE" }}
# {{ .end_date | description "End date (YYYY-MM-DD)" | required "End date required" | asenv "END_DATE" }}
# {{ .format | description "Export format" | type "select" | options "csv" "json" | default "csv" | asenv "FORMAT" }}

import os
import pandas as pd

start = os.environ['START_DATE']
end = os.environ['END_DATE']
fmt = os.environ['FORMAT']

# Your export logic here
print(f"Exporting data from {start} to {end} as {fmt}")
```

### Kubernetes: Scale Deployment

```bash theme={null}
# runbooks/k8s/scale-deployment.runbook.sh
# {{ .namespace | description "Kubernetes namespace" | default "default" | asenv "NAMESPACE" }}
# {{ .deployment | description "Deployment name" | required "Deployment required" | asenv "DEPLOYMENT" }}
# {{ .replicas | description "Number of replicas" | type "number" | required "Replicas required" | asenv "REPLICAS" }}

kubectl scale deployment/$DEPLOYMENT \
    --replicas=$REPLICAS \
    --namespace=$NAMESPACE

kubectl rollout status deployment/$DEPLOYMENT --namespace=$NAMESPACE
```

***

## Security Features

### Input Validation

Use `pattern` to prevent injection attacks:

```sql theme={null}
-- Safe: Only allows numeric IDs
WHERE id = {{ .id | pattern "^[0-9]+$" }}

-- Unsafe: Allows any input (SQL injection risk)
WHERE id = {{ .id }}
```

### Environment Variables

Use `asenv` to pass values as environment variables instead of inline:

```bash theme={null}
# {{ .password | asenv "DB_PASSWORD" }}
psql -h localhost -U admin -c "SELECT 1"
# Password passed via PGPASSWORD, not visible in command
```

### Quoting

Use `squote` or `dquote` to properly quote string values:

```sql theme={null}
WHERE name = {{ .name | squote }}
-- Produces: WHERE name = 'John'
```

***

## File Organization

Recommended repository structure:

```
runbooks/
├── sql/
│   ├── customer-lookup.runbook.sql
│   ├── order-update.runbook.sql
│   └── report-generation.runbook.sql
├── ops/
│   ├── restart-service.runbook.sh
│   ├── check-logs.runbook.sh
│   └── deploy.runbook.sh
├── k8s/
│   ├── scale-deployment.runbook.sh
│   └── rollback.runbook.sh
└── data/
    ├── export-report.runbook.py
    └── cleanup-old-data.runbook.sql
```

### Naming Convention

Files must end with `.runbook.<extension>`:

* `.runbook.sql` - SQL queries
* `.runbook.sh` - Bash scripts
* `.runbook.py` - Python scripts
* `.runbook.rb` - Ruby scripts

***

## Integration with Other Features

| Feature               | How It Works with Runbooks               |
| --------------------- | ---------------------------------------- |
| **Access Requests**   | Runbook execution can require approval   |
| **Guardrails**        | Rules apply to runbook-generated queries |
| **Live Data Masking** | Results are masked automatically         |
| **Session Recording** | All executions are recorded              |
| **Parallel Mode**     | Run runbooks across multiple connections |

***

## Troubleshooting

### Runbook Not Appearing

**Check:**

1. File ends with `.runbook.<ext>`
2. Git repository is configured correctly
3. Hoop can access the repository (check credentials)
4. Run `hoop admin get runbooks` to verify sync

### Template Parsing Error

**Check:**

1. All `{{ }}` brackets are balanced
2. Function names are spelled correctly
3. Pipes `|` are used correctly

**Test locally:**

```bash theme={null}
hoop runbooks lint path/to/your.runbook.sql
```

### Parameter Validation Failing

If a parameter fails validation:

1. Check the `pattern` regex is correct
2. Test the regex at [regex101.com](https://regex101.com)
3. Ensure `required` values are provided

***

## Best Practices

<CardGroup cols={2}>
  <Card title="Validate Inputs" icon="shield-check">
    Always use `pattern` for user inputs to prevent injection
  </Card>

  <Card title="Add Descriptions" icon="comment">
    Every parameter should have a clear description
  </Card>

  <Card title="Set Defaults" icon="sliders">
    Provide sensible defaults where appropriate
  </Card>

  <Card title="Version Control" icon="code-branch">
    Store runbooks in Git for history and review
  </Card>
</CardGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Configuration Guide" icon="gear" href="/setup/configuration/runbooks-configuration">
    Detailed Git setup and template syntax
  </Card>

  <Card title="Parallel Mode" icon="layer-group" href="/learn/features/parallel-mode">
    Run runbooks across multiple connections
  </Card>

  <Card title="Access Requests" icon="clock" href="/learn/features/access-requests/jit">
    Require approval for runbook execution
  </Card>

  <Card title="Session Recording" icon="video" href="/learn/features/session-recording">
    Audit runbook executions
  </Card>
</CardGroup>
