> ## Documentation Index
> Fetch the complete documentation index at: https://docs.octav.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Automations

> Production-ready automation workflows — scheduled airdrop scanning, transaction monitoring, nightly exports, portfolio reports, and credit management

Real-world automation scripts that run on a schedule and notify you when something matters. Each workflow includes the full script, scheduler setup (cron + macOS launchd), realistic output, and customization tips.

<Info>
  All scripts assume you've already authenticated with `octav auth set-key YOUR_API_KEY`. See [Authentication](/cli/overview#authentication) for setup. For basic CLI usage and one-liners, see [Examples](/cli/examples).
</Info>

***

## 1. Daily Airdrop Scanner

Checks a Solana wallet for unclaimed airdrops every day at 2pm. Sends a macOS desktop notification when new airdrops are found and logs results for history.

```bash theme={null}
#!/bin/bash
# airdrop-scanner.sh — Daily airdrop check with desktop notifications

ADDR="7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
LOG_DIR="$HOME/.octav/logs"
LOG_FILE="$LOG_DIR/airdrops.log"

mkdir -p "$LOG_DIR"

RESULT=$(octav airdrop --address "$ADDR" --raw 2>&1)
if [ $? -ne 0 ]; then
  echo "[$(date)] ERROR: $RESULT" >> "$LOG_FILE"
  exit 1
fi

# Count unclaimed airdrops
UNCLAIMED=$(echo "$RESULT" | jq '[.airdrops[] | select(.eligible == true)] | length')
TOTAL_VALUE=$(echo "$RESULT" | jq '[.airdrops[] | select(.eligible == true) | .value_usd] | add // 0')

# Log the check
echo "[$(date)] Checked $ADDR — $UNCLAIMED unclaimed airdrop(s), \$$TOTAL_VALUE total" >> "$LOG_FILE"

# Send macOS notification if airdrops found
if [ "$UNCLAIMED" -gt 0 ]; then
  DETAILS=$(echo "$RESULT" | jq -r '
    [.airdrops[] | select(.eligible == true) | "\(.program): \(.amount) \(.token) (~$\(.value_usd))"]
    | join(", ")
  ')

  osascript -e "display notification \"$DETAILS\" with title \"Octav Airdrop Alert\" subtitle \"$UNCLAIMED unclaimed — \$$TOTAL_VALUE total\""

  echo "[$(date)] NOTIFIED: $DETAILS" >> "$LOG_FILE"
fi
```

**Sample log output:**

```text theme={null}
[Mon Jan 15 14:00:01 2025] Checked 7xKXtg...gAsU — 2 unclaimed airdrop(s), $601 total
[Mon Jan 15 14:00:01 2025] NOTIFIED: Jupiter (JUP): 847 JUP (~$512), Parcl (PRCL): 340 PRCL (~$89)
[Tue Jan 16 14:00:01 2025] Checked 7xKXtg...gAsU — 2 unclaimed airdrop(s), $601 total
[Wed Jan 17 14:00:02 2025] Checked 7xKXtg...gAsU — 1 unclaimed airdrop(s), $89 total
```

<Tabs>
  <Tab title="cron">
    ```bash theme={null}
    # Run daily at 2pm
    0 14 * * * /path/to/airdrop-scanner.sh
    ```

    Add with `crontab -e` or pipe it in:

    ```bash theme={null}
    (crontab -l 2>/dev/null; echo "0 14 * * * $HOME/scripts/airdrop-scanner.sh") | crontab -
    ```
  </Tab>

  <Tab title="launchd (macOS)">
    Save as `~/Library/LaunchAgents/fi.octav.airdrop-scanner.plist`:

    ```xml theme={null}
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>Label</key>
      <string>fi.octav.airdrop-scanner</string>
      <key>ProgramArguments</key>
      <array>
        <string>/bin/bash</string>
        <string>/Users/YOU/scripts/airdrop-scanner.sh</string>
      </array>
      <key>StartCalendarInterval</key>
      <dict>
        <key>Hour</key>
        <integer>14</integer>
        <key>Minute</key>
        <integer>0</integer>
      </dict>
      <key>StandardOutPath</key>
      <string>/tmp/airdrop-scanner.stdout</string>
      <key>StandardErrorPath</key>
      <string>/tmp/airdrop-scanner.stderr</string>
    </dict>
    </plist>
    ```

    Load it:

    ```bash theme={null}
    launchctl load ~/Library/LaunchAgents/fi.octav.airdrop-scanner.plist
    ```
  </Tab>
</Tabs>

<Accordion title="Customization tips">
  * **Multiple wallets**: Loop over an array of addresses and check each one
  * **Slack instead of macOS notifications**: Replace the `osascript` line with a `curl` to a Slack webhook (see the Transaction Watchdog below for an example)
  * **Minimum value filter**: Add `| select(.value_usd > 50)` to the jq filter to ignore dust airdrops
</Accordion>

***

## 2. Transaction Watchdog

Monitors wallets every 10 minutes for new activity. Tracks state to avoid duplicate alerts and supports both macOS notifications and Slack webhooks.

```bash theme={null}
#!/bin/bash
# tx-watchdog.sh — Detect new transactions and send alerts

ADDR="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"
STATE_DIR="$HOME/.octav/state"
STATE_FILE="$STATE_DIR/tx-watchdog-last-tx.txt"
LOG_FILE="$HOME/.octav/logs/tx-watchdog.log"

# Optional: set to your Slack webhook URL, or leave empty for macOS notifications only
SLACK_WEBHOOK=""

mkdir -p "$STATE_DIR" "$(dirname "$LOG_FILE")"

# Get the last known transaction hash
LAST_SEEN=""
[ -f "$STATE_FILE" ] && LAST_SEEN=$(cat "$STATE_FILE")

# Fetch recent transactions
RESULT=$(octav transactions get --addresses "$ADDR" --limit 10 --raw 2>&1)
if [ $? -ne 0 ]; then
  echo "[$(date)] ERROR: $RESULT" >> "$LOG_FILE"
  exit 1
fi

# Get the latest transaction hash
LATEST_TX=$(echo "$RESULT" | jq -r '.transactions[0].hash // empty')

if [ -z "$LATEST_TX" ]; then
  echo "[$(date)] No transactions found" >> "$LOG_FILE"
  exit 0
fi

# Compare with last seen
if [ "$LATEST_TX" = "$LAST_SEEN" ]; then
  echo "[$(date)] No new transactions" >> "$LOG_FILE"
  exit 0
fi

# New transactions found — collect all new ones
NEW_TXS=$(echo "$RESULT" | jq -r --arg last "$LAST_SEEN" '
  .transactions
  | if $last == "" then .[:5] else [.[] | select(.hash != $last)] end
  | .[]
  | "\(.type) \(.amount) \(.token_symbol) ($\(.value_usd)) on \(.chain) — \(.date)"
')

NEW_COUNT=$(echo "$RESULT" | jq --arg last "$LAST_SEEN" '
  .transactions
  | if $last == "" then .[:5] else [.[] | select(.hash != $last)] end
  | length
')

# Update state
echo "$LATEST_TX" > "$STATE_FILE"

# Log
echo "[$(date)] $NEW_COUNT new transaction(s) detected:" >> "$LOG_FILE"
echo "$NEW_TXS" >> "$LOG_FILE"

# Send macOS notification
osascript -e "display notification \"$NEW_COUNT new transaction(s) on ${ADDR:0:6}...${ADDR: -4}\" with title \"Octav Transaction Alert\""

# Send Slack notification if webhook is configured
if [ -n "$SLACK_WEBHOOK" ]; then
  SLACK_MSG=$(echo "$NEW_TXS" | sed 's/"/\\"/g' | paste -sd '\n' -)
  curl -s -X POST "$SLACK_WEBHOOK" \
    -H 'Content-Type: application/json' \
    -d "{\"text\": \"*Transaction Alert* — ${ADDR:0:6}...${ADDR: -4}\n\`\`\`$SLACK_MSG\`\`\`\"}" \
    > /dev/null
fi
```

**Sample alert output:**

```text theme={null}
[2025-01-15 14:30:01] 2 new transaction(s) detected:
swap 2.5 ETH ($5875.00) on ethereum — 2025-01-15
transfer 1000.0 USDC ($1000.00) on ethereum — 2025-01-15
```

<Tabs>
  <Tab title="cron">
    ```bash theme={null}
    # Run every 10 minutes
    */10 * * * * /path/to/tx-watchdog.sh
    ```
  </Tab>

  <Tab title="launchd (macOS)">
    Save as `~/Library/LaunchAgents/fi.octav.tx-watchdog.plist`:

    ```xml theme={null}
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>Label</key>
      <string>fi.octav.tx-watchdog</string>
      <key>ProgramArguments</key>
      <array>
        <string>/bin/bash</string>
        <string>/Users/YOU/scripts/tx-watchdog.sh</string>
      </array>
      <key>StartInterval</key>
      <integer>600</integer>
      <key>StandardOutPath</key>
      <string>/tmp/tx-watchdog.stdout</string>
      <key>StandardErrorPath</key>
      <string>/tmp/tx-watchdog.stderr</string>
    </dict>
    </plist>
    ```

    Load it:

    ```bash theme={null}
    launchctl load ~/Library/LaunchAgents/fi.octav.tx-watchdog.plist
    ```
  </Tab>
</Tabs>

<Accordion title="Customization tips">
  * **Multiple wallets**: Create separate state files per address — `tx-watchdog-${ADDR:0:8}.txt`
  * **Value filter**: Add `| select(.value_usd > 1000)` to only alert on large transactions
  * **Slack setup**: Create a webhook at [api.slack.com/messaging/webhooks](https://api.slack.com/messaging/webhooks) and paste the URL into `SLACK_WEBHOOK`
</Accordion>

***

## 3. Nightly Transaction Export

Runs at midnight, exports the day's transactions to a monthly CSV file. Handles pagination for wallets with high daily activity and appends without duplicates.

```bash theme={null}
#!/bin/bash
# nightly-export.sh — Export today's transactions to a monthly CSV

ADDR="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"
EXPORT_DIR="$HOME/.octav/exports"
TODAY=$(date +%Y-%m-%d)
MONTH_FILE="$EXPORT_DIR/transactions-$(date +%Y-%m).csv"

mkdir -p "$EXPORT_DIR"

# Initialize CSV with header if it doesn't exist
if [ ! -f "$MONTH_FILE" ]; then
  echo "date,type,chain,token,amount,value_usd,hash" > "$MONTH_FILE"
fi

# Fetch today's transactions with pagination
OFFSET=0
LIMIT=250
TOTAL_EXPORTED=0

while true; do
  RESULT=$(octav transactions get \
    --addresses "$ADDR" \
    --start-date "$TODAY" \
    --end-date "$TODAY" \
    --limit "$LIMIT" \
    --offset "$OFFSET" \
    --raw 2>&1)

  if [ $? -ne 0 ]; then
    echo "[$(date)] ERROR: $RESULT" >&2
    exit 1
  fi

  # Extract transaction count
  COUNT=$(echo "$RESULT" | jq '.transactions | length')

  if [ "$COUNT" -eq 0 ]; then
    break
  fi

  # Append to CSV (skip duplicates by checking hash)
  EXISTING_HASHES=""
  [ -f "$MONTH_FILE" ] && EXISTING_HASHES=$(cut -d',' -f7 "$MONTH_FILE" | tail -n +2)

  echo "$RESULT" | jq -r --arg existing "$EXISTING_HASHES" '
    .transactions[] |
    select(.hash as $h | ($existing | split("\n") | index($h)) | not) |
    [.date, .type, .chain, .token_symbol, .amount, .value_usd, .hash] | @csv
  ' >> "$MONTH_FILE"

  NEW_ROWS=$(echo "$RESULT" | jq --arg existing "$EXISTING_HASHES" '
    [.transactions[] | select(.hash as $h | ($existing | split("\n") | index($h)) | not)] | length
  ')
  TOTAL_EXPORTED=$((TOTAL_EXPORTED + NEW_ROWS))

  # If we got fewer than the limit, we've reached the end
  if [ "$COUNT" -lt "$LIMIT" ]; then
    break
  fi

  OFFSET=$((OFFSET + LIMIT))
done

echo "[$(date)] Exported $TOTAL_EXPORTED transaction(s) for $TODAY to $MONTH_FILE"
```

**Sample CSV output (`transactions-2025-01.csv`):**

```csv theme={null}
date,type,chain,token,amount,value_usd,hash
"2025-01-01","swap","ethereum","ETH","2.5","5875.00","0x1a2b3c..."
"2025-01-01","swap","ethereum","USDC","-5875.00","-5875.00","0x1a2b3c..."
"2025-01-03","transfer","arbitrum","ARB","500.0","475.00","0x4d5e6f..."
"2025-01-07","transfer","ethereum","ETH","1.0","2340.00","0x7g8h9i..."
```

**File structure:**

```text theme={null}
~/.octav/exports/
  transactions-2025-01.csv
  transactions-2025-02.csv
  transactions-2025-03.csv
```

<Tabs>
  <Tab title="cron">
    ```bash theme={null}
    # Run at midnight every day
    0 0 * * * /path/to/nightly-export.sh
    ```
  </Tab>

  <Tab title="launchd (macOS)">
    Save as `~/Library/LaunchAgents/fi.octav.nightly-export.plist`:

    ```xml theme={null}
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>Label</key>
      <string>fi.octav.nightly-export</string>
      <key>ProgramArguments</key>
      <array>
        <string>/bin/bash</string>
        <string>/Users/YOU/scripts/nightly-export.sh</string>
      </array>
      <key>StartCalendarInterval</key>
      <dict>
        <key>Hour</key>
        <integer>0</integer>
        <key>Minute</key>
        <integer>0</integer>
      </dict>
      <key>StandardOutPath</key>
      <string>/tmp/nightly-export.stdout</string>
      <key>StandardErrorPath</key>
      <string>/tmp/nightly-export.stderr</string>
    </dict>
    </plist>
    ```
  </Tab>
</Tabs>

<Accordion title="Customization tips">
  * **Multiple wallets**: Loop over addresses and write to separate files or add an `address` column
  * **Weekly instead of monthly files**: Change the filename to `transactions-$(date +%Y-W%V).csv`
  * **Compression**: Add `gzip "$MONTH_FILE.bak"` at the end of each month to archive old files
</Accordion>

***

## 4. Portfolio Tracker with Weekly Report

Two scripts that work together: a daily NAV snapshot recorder and a weekly report generator that runs every Sunday.

### Daily snapshot

```bash theme={null}
#!/bin/bash
# portfolio-tracker.sh — Record daily NAV snapshot

ADDR="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"
DATA_DIR="$HOME/.octav/portfolio"
NAV_FILE="$DATA_DIR/nav-history.csv"

mkdir -p "$DATA_DIR"

# Initialize CSV if it doesn't exist
[ ! -f "$NAV_FILE" ] && echo "date,nav" > "$NAV_FILE"

# Fetch current NAV
RESULT=$(octav portfolio nav --addresses "$ADDR" --raw 2>&1)
if [ $? -ne 0 ]; then
  echo "[$(date)] ERROR: $RESULT" >&2
  exit 1
fi

NAV=$(echo "$RESULT" | jq -r '.nav')
TODAY=$(date +%Y-%m-%d)

# Skip if already recorded today
if grep -q "^$TODAY," "$NAV_FILE" 2>/dev/null; then
  echo "[$(date)] Already recorded NAV for $TODAY"
  exit 0
fi

echo "$TODAY,$NAV" >> "$NAV_FILE"
echo "[$(date)] Recorded NAV: \$$NAV"
```

### Weekly report (Sundays)

```bash theme={null}
#!/bin/bash
# weekly-report.sh — Generate weekly portfolio summary

DATA_DIR="$HOME/.octav/portfolio"
NAV_FILE="$DATA_DIR/nav-history.csv"
ADDR="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"

# Optional: Slack webhook for posting the report
SLACK_WEBHOOK=""

# Get this week's data (last 7 days)
WEEK_START=$(date -v-6d +%Y-%m-%d 2>/dev/null || date -d '6 days ago' +%Y-%m-%d)
WEEK_END=$(date +%Y-%m-%d)

# Extract this week's NAV values
WEEK_DATA=$(awk -F',' -v start="$WEEK_START" -v end="$WEEK_END" '
  NR > 1 && $1 >= start && $1 <= end { print $2 }
' "$NAV_FILE")

if [ -z "$WEEK_DATA" ]; then
  echo "No data for this week"
  exit 0
fi

# Calculate stats
OPEN=$(echo "$WEEK_DATA" | head -1)
CLOSE=$(echo "$WEEK_DATA" | tail -1)
HIGH=$(echo "$WEEK_DATA" | sort -rn | head -1)
LOW=$(echo "$WEEK_DATA" | sort -n | head -1)
CHANGE=$(echo "scale=2; (($CLOSE - $OPEN) / $OPEN) * 100" | bc)
SIGN=$([ "$(echo "$CHANGE >= 0" | bc)" -eq 1 ] && echo "+" || echo "")

# Fetch current top holdings for the report
TOP_HOLDINGS=$(octav portfolio get --addresses "$ADDR" --raw 2>/dev/null | jq -r '
  [.tokens | sort_by(-.value) | .[:3][] | "  \(.symbol): $\(.value | round) (\(.chain))"]
  | join("\n")
')

# Build report
REPORT="Portfolio Weekly Report
$WEEK_START to $WEEK_END
================================
Opening NAV:  \$$OPEN
Closing NAV:  \$$CLOSE
Weekly High:  \$$HIGH
Weekly Low:   \$$LOW
Change:       ${SIGN}${CHANGE}%
================================
Top Holdings:
$TOP_HOLDINGS
================================"

echo "$REPORT"

# Save report
REPORT_FILE="$DATA_DIR/report-$(date +%Y-W%V).txt"
echo "$REPORT" > "$REPORT_FILE"

# Post to Slack if configured
if [ -n "$SLACK_WEBHOOK" ]; then
  SLACK_TEXT=$(echo "$REPORT" | sed 's/"/\\"/g')
  curl -s -X POST "$SLACK_WEBHOOK" \
    -H 'Content-Type: application/json' \
    -d "{\"text\": \"\`\`\`$SLACK_TEXT\`\`\`\"}" \
    > /dev/null
  echo "Posted to Slack."
fi
```

**Sample weekly report:**

```text theme={null}
Portfolio Weekly Report
2025-01-13 to 2025-01-19
================================
Opening NAV:  $181043.22
Closing NAV:  $184230.41
Weekly High:  $186102.88
Weekly Low:   $178934.10
Change:       +1.76%
================================
Top Holdings:
  ETH: $82903 (ethereum)
  USDC: $38088 (ethereum)
  WBTC: $27634 (ethereum)
================================
```

**Scheduler setup:**

```bash theme={null}
# Daily NAV snapshot at 9am
0 9 * * * /path/to/portfolio-tracker.sh

# Weekly report every Sunday at 10am
0 10 * * 0 /path/to/weekly-report.sh
```

<Accordion title="Customization tips">
  * **Multiple wallets**: Track each wallet in a separate CSV and combine totals in the weekly report
  * **Monthly reports**: Add a similar script triggered on the 1st of each month with `0 10 1 * *`
  * **Historical chart**: Feed `nav-history.csv` into a plotting tool like `gnuplot` or a Google Sheet for visual trends
</Accordion>

***

## 5. Credit Usage Guardian

A wrapper script that checks remaining credits before running any CLI command. Tracks daily consumption and alerts when credits drop below a configurable threshold.

```bash theme={null}
#!/bin/bash
# octav-safe — Credit-aware CLI wrapper
# Usage: octav-safe portfolio get --addresses 0x...
# Drop-in replacement for `octav` that prevents accidental credit burn

CREDIT_THRESHOLD=100       # Warn below this many credits
LOG_DIR="$HOME/.octav/logs"
USAGE_LOG="$LOG_DIR/credit-usage.log"

mkdir -p "$LOG_DIR"

# Check current credits
CREDITS_RESULT=$(octav credits --raw 2>&1)
if [ $? -ne 0 ]; then
  echo "ERROR: Could not check credits — $CREDITS_RESULT" >&2
  exit 1
fi

CREDITS=$(echo "$CREDITS_RESULT" | jq -r '.credits')

# Log usage
echo "[$(date)] Credits remaining: $CREDITS | Command: octav $*" >> "$USAGE_LOG"

# Block if below threshold
if [ "$CREDITS" -lt "$CREDIT_THRESHOLD" ]; then
  echo "CREDIT GUARD: Only $CREDITS credits remaining (threshold: $CREDIT_THRESHOLD)"
  echo ""
  echo "  Command blocked: octav $*"
  echo ""
  echo "  To override, run the command directly with \`octav\` instead of \`octav-safe\`."
  echo "  To adjust the threshold, edit CREDIT_THRESHOLD in $(realpath "$0")"
  exit 1
fi

# Show credit count and proceed
echo "[octav-safe] Credits: $CREDITS — running: octav $*"
octav "$@"
```

**Installation:**

```bash theme={null}
# Make it executable and add to PATH
chmod +x /path/to/octav-safe
ln -s /path/to/octav-safe /usr/local/bin/octav-safe

# Now use it as a drop-in replacement
octav-safe portfolio nav --addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68
```

**Sample output — normal operation:**

```text theme={null}
[octav-safe] Credits: 4872 — running: octav portfolio nav --addresses 0x742d...
{
  "nav": 184230.41,
  "currency": "USD",
  "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"
}
```

**Sample output — credit guard triggered:**

```text theme={null}
CREDIT GUARD: Only 47 credits remaining (threshold: 100)

  Command blocked: octav portfolio nav --addresses 0x742d...

  To override, run the command directly with `octav` instead of `octav-safe`.
  To adjust the threshold, edit CREDIT_THRESHOLD in /Users/you/scripts/octav-safe
```

**Sample daily usage log (`credit-usage.log`):**

```text theme={null}
[Mon Jan 15 09:00:01 2025] Credits remaining: 4872 | Command: octav portfolio nav --addresses 0x742d...
[Mon Jan 15 09:00:05 2025] Credits remaining: 4871 | Command: octav transactions get --addresses 0x742d...
[Mon Jan 15 14:00:01 2025] Credits remaining: 4870 | Command: octav airdrop --address 7xKXtg...
[Mon Jan 15 14:10:01 2025] Credits remaining: 4869 | Command: octav transactions get --addresses 0x742d...
```

<Accordion title="Customization tips">
  * **Shell alias**: Add `alias octav="octav-safe"` to your `~/.bashrc` or `~/.zshrc` to protect all commands by default
  * **Daily summary**: Add a cron job that parses the usage log and reports daily consumption — `grep "$(date +%Y-%m-%d)" "$USAGE_LOG" | wc -l` gives you the day's command count
  * **Tiered warnings**: Add a second threshold (e.g., 500) that prints a warning but still allows the command to run
</Accordion>

***

## Setting Up launchd on macOS

macOS uses `launchd` instead of cron for scheduled tasks. While cron works on macOS, `launchd` is the native scheduler and handles sleep/wake correctly — your task runs after waking up even if the Mac was asleep at the scheduled time.

### Step 1: Create the plist file

Each scheduled task needs a `.plist` file in `~/Library/LaunchAgents/`. Use reverse-DNS naming:

```xml theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>fi.octav.your-script-name</string>

  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>/Users/YOU/scripts/your-script.sh</string>
  </array>

  <!-- Option A: Run at a specific time (daily at 2pm) -->
  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key>
    <integer>14</integer>
    <key>Minute</key>
    <integer>0</integer>
  </dict>

  <!-- Option B: Run at a fixed interval (every 600 seconds = 10 min) -->
  <!-- <key>StartInterval</key> -->
  <!-- <integer>600</integer> -->

  <!-- Log output for debugging -->
  <key>StandardOutPath</key>
  <string>/tmp/your-script.stdout</string>
  <key>StandardErrorPath</key>
  <string>/tmp/your-script.stderr</string>
</dict>
</plist>
```

### Step 2: Load and manage

```bash theme={null}
# Load (start scheduling)
launchctl load ~/Library/LaunchAgents/fi.octav.your-script-name.plist

# Unload (stop scheduling)
launchctl unload ~/Library/LaunchAgents/fi.octav.your-script-name.plist

# Check if it's running
launchctl list | grep fi.octav

# Run immediately (for testing)
launchctl start fi.octav.your-script-name
```

### Step 3: Debug

If your script isn't running, check the logs:

```bash theme={null}
# Check launchd's own logs
log show --predicate 'subsystem == "com.apple.xpc.launchd"' --last 1h | grep fi.octav

# Check your script's output
cat /tmp/your-script.stdout
cat /tmp/your-script.stderr
```

<Warning>
  **Common pitfall:** launchd doesn't source your shell profile. If `octav` isn't found, use the full path (`/usr/local/bin/octav` or wherever it's installed). Find it with `which octav`.
</Warning>

***

## Next Steps

<CardGroup cols={3}>
  <Card title="CLI Overview" icon="terminal" href="/cli/overview">
    Installation, authentication, and full command reference
  </Card>

  <Card title="CLI Examples" icon="code" href="/cli/examples">
    One-liners, scripts, and jq patterns
  </Card>

  <Card title="MCP Server" icon="robot" href="/mcp/overview">
    Connect the Octav API directly to AI assistants
  </Card>
</CardGroup>
