@clawhub-kjvarga-5f9b7d60c7
Generate detailed daily OpenClaw cost reports by agent, model, and channel, with HTML email formatting and optional automated delivery.
---
name: daily-cost-report
description: Generate daily OpenClaw cost reports with breakdown by agent/model/channel. Use when: daily cron job, on-demand cost analysis, cost auditing. Generates HTML-formatted email reports.
emoji: 📊
requires:
env:
- OPENAI_API_KEY
---
# Daily Cost Report 📊
Generate comprehensive OpenClaw usage and cost reports with breakdowns by agent, model, and channel. Supports both on-demand analysis and automated daily email delivery.
## Quick Start
```bash
# Generate report for yesterday (markdown)
{baseDir}/scripts/daily-cost-report.sh yesterday
# Generate report for a specific date
{baseDir}/scripts/daily-cost-report.sh 2026-03-18
# Generate report for today
{baseDir}/scripts/daily-cost-report.sh today
# Generate HTML-formatted email report
{baseDir}/scripts/daily-cost-report-email.sh yesterday
# Send email report to recipient
{baseDir}/scripts/send-cost-report.sh [email protected] yesterday
```
## What It Does
The daily cost report analyzes OpenClaw session data for a specified date range and generates:
- **Total cost and token usage** across all agents, models, and channels
- **Per-agent breakdown** showing which agents consumed the most resources
- **Per-model breakdown** showing cost distribution across Claude models, Deepseek, GPT, etc.
- **Per-channel breakdown** showing usage from Telegram, CLI, web, etc.
- **Top sessions by cost** identifying the most expensive individual sessions
- **Prompt cache metrics** showing cache write/read tokens and cost savings
Reports use the current pricing for:
- Claude Haiku 4.5
- Claude Sonnet 4.5
- Claude Opus 4.6
- Deepseek V3.2
- OpenAI GPT-4o-mini
Cache pricing (read/write) is factored into cost calculations.
## Scripts
### `daily-cost-report.sh`
Core report generator. Queries OpenClaw sessions via `openclaw sessions --all-agents --json`, filters by date range, calculates costs using model-specific pricing, and generates markdown output.
**Output:** `/tmp/cost-report-YYYY-MM-DD.md`
### `daily-cost-report-email.sh`
Wraps the markdown report in an HTML email template with styled tables, summary metrics, and visual hierarchy.
**Output:** `/tmp/cost-report-YYYY-MM-DD.html`
### `send-cost-report.sh`
Sends the HTML report via email using the `mail` command. Falls back to saving the file if mail delivery fails.
**Usage:** `send-cost-report.sh <recipient-email> [date]`
## Cron Usage
The daily cost report is typically scheduled as a cron job in `~/.openclaw/cron/jobs.json`:
```json
{
"id": "daily-cost-report",
"agentId": "worker",
"name": "main-daily-cost-report",
"enabled": true,
"schedule": {
"kind": "cron",
"expr": "0 8 * * *",
"tz": "America/Vancouver"
},
"sessionTarget": "isolated",
"wakeMode": "now",
"payload": {
"kind": "agentTurn",
"message": "Generate yesterday's cost report and send to [email protected]"
},
"delivery": {
"mode": "announce",
"channel": "telegram",
"to": "7918443630"
}
}
```
The cron job invokes the skill, which then calls the appropriate scripts.
## Manual Invocation from Agent
When Karl asks for a cost report:
```bash
# From any agent with exec access
exec(command: "bash ~/.openclaw/workspace/skills/daily-cost-report/scripts/daily-cost-report.sh yesterday")
# Or to generate and send email
exec(command: "bash ~/.openclaw/workspace/skills/daily-cost-report/scripts/send-cost-report.sh [email protected] yesterday")
```
## Report Format
The report includes:
1. **Summary section** - Total cost, tokens, cache metrics, savings
2. **Cost by Agent** - Which agents are most active/expensive
3. **Cost by Model** - Model-level resource consumption
4. **Cost by Channel** - Usage by Telegram, CLI, web, etc.
5. **Top Sessions** - Highest-cost individual sessions
All monetary values are in USD with 4 decimal precision. Token counts are formatted with thousands separators for readability.
## Date Handling
Scripts accept:
- `yesterday` (default) - Previous calendar day
- `today` - Current calendar day
- `YYYY-MM-DD` - Specific date
Date parsing is compatible with both macOS (`date -v-1d`) and Linux (`date -d "yesterday"`).
## Requirements
- OpenClaw CLI: `openclaw sessions --all-agents --json`
- `jq` for JSON processing
- `awk` for aggregation
- `mail` command (for email delivery)
- bash 4+ (for associative arrays)
## Cost
**This skill costs nothing** — it only reads session metadata that OpenClaw already tracks. No external API calls.
## Example Output
```
# OpenClaw Daily Cost Report 🐈⬛
**Date:** 2026-03-18
**Generated:** 2026-03-19 08:00:00 PDT
---
## Summary
| Metric | Value |
|--------|-------|
| **Total Cost** | $2.4567 |
| **Total Tokens** | 1,234,567 |
| **Input Tokens** | 987,654 |
| **Output Tokens** | 246,913 |
| **Cache Write Tokens** | 123,456 |
| **Cache Read Tokens** | 456,789 |
| **Cache Savings** | $0.3245 |
---
## Cost by Agent
| Agent | Cost | Tokens | Input | Output |
|-------|------|--------|-------|--------|
| main | $1.2345 | 654,321 | 543,210 | 111,111 |
| worker | $0.8901 | 400,000 | 320,000 | 80,000 |
| research | $0.3321 | 180,246 | 124,444 | 55,802 |
...
```
FILE:scripts/daily-cost-report-email.sh
#!/bin/bash
# Daily cost report with HTML email delivery
# Usage: daily-cost-report-email.sh [date]
set -euo pipefail
DATE="-yesterday"
# Handle date format for macOS/Linux compatibility
if date -v-1d +%Y-%m-%d >/dev/null 2>&1; then
# macOS
DATE_STR=$(date -j -f "%Y-%m-%d" "$DATE" "+%Y-%m-%d" 2>/dev/null || date -j -v-1d "+%Y-%m-%d")
else
# Linux
DATE_STR=$(date -d "$DATE" "+%Y-%m-%d" 2>/dev/null || date -d "yesterday" "+%Y-%m-%d")
fi
REPORT_FILE="/tmp/cost-report-DATE_STR.md"
HTML_FILE="/tmp/cost-report-DATE_STR.html"
# Generate the markdown report
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
bash "$SCRIPT_DIR/daily-cost-report.sh" "$DATE" > /dev/null 2>&1 || true
# If report exists, create HTML email
if [ -f "$REPORT_FILE" ]; then
# Extract summary stats
TOTAL_COST=$(grep "Total Cost" "$REPORT_FILE" | head -1 | awk -F'|' '{print $3}' | xargs)
TOTAL_TOKENS=$(grep "Total Tokens" "$REPORT_FILE" | head -1 | awk -F'|' '{print $3}' | xargs)
REPORT_DATE=$(grep "Date:" "$REPORT_FILE" | head -1 | awk -F: '{print $2}' | xargs)
# Create HTML with embedded markdown (formatted as readable HTML)
cat > "$HTML_FILE" << EOF
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
color: #333;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 20px;
}
.header h1 {
font-size: 28px;
margin-bottom: 5px;
color: #000;
}
.header .date {
color: #666;
font-size: 14px;
}
.metrics {
background: #f9f9f9;
padding: 20px;
border-radius: 6px;
margin-bottom: 30px;
}
.metric-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.metric-row:last-child {
border-bottom: none;
}
.metric-label {
color: #666;
font-weight: 500;
}
.metric-value {
color: #0066cc;
font-weight: 600;
}
h2 {
font-size: 18px;
margin-top: 30px;
margin-bottom: 15px;
color: #222;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
font-size: 14px;
}
th {
background: #f0f0f0;
padding: 12px;
text-align: left;
font-weight: 600;
border: 1px solid #ddd;
}
td {
padding: 10px 12px;
border: 1px solid #eee;
}
tr:nth-child(even) {
background: #fafafa;
}
.footer {
text-align: center;
color: #999;
font-size: 12px;
border-top: 1px solid #eee;
padding-top: 20px;
margin-top: 30px;
}
code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: "Monaco", "Courier New", monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🐈⬛ OpenClaw Daily Cost Report</h1>
<p class="date">$(date '+%A, %B %d, %Y')</p>
</div>
<div class="metrics">
<div class="metric-row">
<span class="metric-label">Total Cost</span>
<span class="metric-value">TOTAL_COST</span>
</div>
<div class="metric-row">
<span class="metric-label">Total Tokens</span>
<span class="metric-value">TOTAL_TOKENS</span>
</div>
</div>
<h2>Report Summary</h2>
<p style="color: #666; line-height: 1.6; margin-bottom: 20px;">
This report shows your OpenClaw usage for <strong>$(date -j -f "%Y-%m-%d" "$DATE_STR" "+%B %d, %Y" 2>/dev/null || date -d "$DATE_STR" "+%B %d, %Y")</strong>.
Includes breakdown by agent, model, and channel.
</p>
<h2>Full Report</h2>
<pre style="background: #f5f5f5; padding: 15px; border-radius: 6px; overflow-x: auto; font-size: 13px; line-height: 1.4;">$(cat "$REPORT_FILE")</pre>
<div class="footer">
<p>📊 Report generated by OpenClaw at $(date '+%I:%M %p %Z')</p>
<p>This is an automated message. Do not reply to this email.</p>
</div>
</div>
</body>
</html>
EOF
echo "✅ Report: $REPORT_FILE"
echo "📧 HTML Email: $HTML_FILE"
echo "💰 Summary: Total Cost $TOTAL_COST | Total Tokens $TOTAL_TOKENS"
else
echo "❌ Report not found: $REPORT_FILE"
exit 1
fi
FILE:scripts/daily-cost-report.sh
#!/usr/bin/env bash
set -euo pipefail
# Daily Cost Report Script for OpenClaw
# Aggregates token usage and costs by agent, channel, and model
# --- Configuration ---
OPENCLAW_BIN="$HOME/homebrew/bin/openclaw"
export PATH="$HOME/homebrew/bin:$PATH"
# --- Argument Parsing ---
TARGET_DATE="-yesterday"
if [ "$TARGET_DATE" = "yesterday" ]; then
TARGET_DATE=$(date -v-1d +%Y-%m-%d 2>/dev/null || date -d "yesterday" +%Y-%m-%d 2>/dev/null)
elif [ "$TARGET_DATE" = "today" ]; then
TARGET_DATE=$(date +%Y-%m-%d)
fi
# Calculate start/end timestamps (midnight to midnight in local time)
START_TS=$(date -j -f "%Y-%m-%d %H:%M:%S" "$TARGET_DATE 00:00:00" +%s 2>/dev/null || date -d "$TARGET_DATE 00:00:00" +%s)
END_TS=$(date -j -f "%Y-%m-%d %H:%M:%S" "$TARGET_DATE 23:59:59" +%s 2>/dev/null || date -d "$TARGET_DATE 23:59:59" +%s)
# Convert to milliseconds for comparison
START_MS=$((START_TS * 1000))
END_MS=$((END_TS * 1000))
OUTPUT_FILE="/tmp/cost-report-TARGET_DATE.md"
TEMP_DIR="/tmp/cost-report-$$"
mkdir -p "$TEMP_DIR"
echo "🐈⬛ Generating cost report for $TARGET_DATE"
echo " Start: $(date -r $START_TS '+%Y-%m-%d %H:%M:%S %Z')"
echo " End: $(date -r $END_TS '+%Y-%m-%d %H:%M:%S %Z')"
echo ""
# --- Fetch Session Data ---
SESSIONS_JSON=$("$OPENCLAW_BIN" sessions --all-agents --json 2>/dev/null)
# Filter sessions by date range and add cost calculation
PROCESSED_DATA=$(echo "$SESSIONS_JSON" | jq -r --argjson start "$START_MS" --argjson end "$END_MS" '
# Model pricing per 1M tokens
def model_pricing:
{
"claude-haiku-4-5": {input: 1, output: 5, cache_read: 0.1, cache_write: 2},
"claude-sonnet-4-5": {input: 3, output: 15, cache_read: 0.3, cache_write: 6},
"claude-opus-4-6": {input: 5, output: 25, cache_read: 0.5, cache_write: 10},
"deepseek/deepseek-v3.2": {input: 0.32, output: 0.89, cache_read: 0, cache_write: 0},
"openai/gpt-4o-mini": {input: 0.15, output: 0.6, cache_read: 0, cache_write: 0}
};
.sessions[]
| select(.updatedAt >= $start and .updatedAt <= $end)
| select(.totalTokens != null and .totalTokens > 0)
| . as $session
| ($session.inputTokens // 0) as $input
| ($session.outputTokens // 0) as $output
| ($session.cacheCreationInputTokens // 0) as $cache_write
| ($session.cacheReadInputTokens // 0) as $cache_read
| model_pricing[.model] as $pricing
| (if $pricing then
(($input / 1000000) * $pricing.input) +
(($output / 1000000) * $pricing.output) +
(($cache_read / 1000000) * $pricing.cache_read) +
(($cache_write / 1000000) * $pricing.cache_write)
else 0 end) as $cost
| (if $pricing then
(($cache_read / 1000000) * ($pricing.input - $pricing.cache_read))
else 0 end) as $savings
| [
.key,
.agentId,
(.key | split(":")[2] // "unknown"),
.model,
$input,
$output,
(.totalTokens // 0),
$cache_write,
$cache_read,
$cost,
$savings
]
| @tsv
')
if [ -z "$PROCESSED_DATA" ]; then
echo "No sessions found for $TARGET_DATE"
cat > "$OUTPUT_FILE" <<EOF
# OpenClaw Daily Cost Report
**Date:** $TARGET_DATE
No sessions found with token usage for this date.
EOF
cat "$OUTPUT_FILE"
rm -rf "$TEMP_DIR"
exit 0
fi
# --- Calculate Aggregates ---
echo "$PROCESSED_DATA" | awk -F'\t' '
{
agent = $2
channel = $3
model = $4
input = $5
output = $6
tokens = $7
cache_write = $8
cache_read = $9
cost = $10
savings = $11
printf "agent\t%s\t%s\t%s\t%s\t%s\n", agent, cost, tokens, input, output
printf "model\t%s\t%s\t%s\t%s\t%s\n", model, cost, tokens, input, output
printf "channel\t%s\t%s\t%s\n", channel, cost, tokens
printf "total\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", "all", cost, tokens, input, output, cache_write, cache_read, savings
printf "session\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, agent, model, channel, tokens, cost
}
' > "$TEMP_DIR/raw_data.txt"
# Calculate totals
TOTALS=$(awk '$1 == "total"' "$TEMP_DIR/raw_data.txt" | awk '{
cost += $3
tokens += $4
input += $5
output += $6
cache_write += $7
cache_read += $8
savings += $9
}
END {
printf "%.4f %d %d %d %d %d %.4f", cost, tokens, input, output, cache_write, cache_read, savings
}')
read TOTAL_COST TOTAL_TOKENS TOTAL_INPUT TOTAL_OUTPUT TOTAL_CACHE_WRITE TOTAL_CACHE_READ TOTAL_SAVINGS <<< "$TOTALS"
# Aggregate by agent
awk '$1 == "agent"' "$TEMP_DIR/raw_data.txt" | awk '{
agent = $2
cost[agent] += $3
tokens[agent] += $4
input[agent] += $5
output[agent] += $6
}
END {
for (a in cost) {
printf "%s\t%.4f\t%d\t%d\t%d\n", a, cost[a], tokens[a], input[a], output[a]
}
}' | sort -t$'\t' -k2 -rn > "$TEMP_DIR/by_agent.txt"
# Aggregate by model
awk '$1 == "model"' "$TEMP_DIR/raw_data.txt" | awk '{
model = $2
cost[model] += $3
tokens[model] += $4
input[model] += $5
output[model] += $6
}
END {
for (m in cost) {
printf "%s\t%.4f\t%d\t%d\t%d\n", m, cost[m], tokens[m], input[m], output[m]
}
}' | sort -t$'\t' -k2 -rn > "$TEMP_DIR/by_model.txt"
# Aggregate by channel
awk '$1 == "channel"' "$TEMP_DIR/raw_data.txt" | awk '{
channel = $2
cost[channel] += $3
tokens[channel] += $4
}
END {
for (c in cost) {
printf "%s\t%.4f\t%d\n", c, cost[c], tokens[c]
}
}' | sort -t$'\t' -k2 -rn > "$TEMP_DIR/by_channel.txt"
# Top sessions (sort numerically on cost column)
awk '$1 == "session"' "$TEMP_DIR/raw_data.txt" | sort -t$'\t' -k7 -g -r | head -10 > "$TEMP_DIR/top_sessions.txt"
# --- Generate Report ---
cat > "$OUTPUT_FILE" <<EOF
# OpenClaw Daily Cost Report 🐈⬛
**Date:** $TARGET_DATE
**Generated:** $(date '+%Y-%m-%d %H:%M:%S %Z')
---
## Summary
| Metric | Value |
|--------|-------|
| **Total Cost** | \$$TOTAL_COST |
| **Total Tokens** | $(printf "%'d" $TOTAL_TOKENS) |
| **Input Tokens** | $(printf "%'d" $TOTAL_INPUT) |
| **Output Tokens** | $(printf "%'d" $TOTAL_OUTPUT) |
| **Cache Write Tokens** | $(printf "%'d" $TOTAL_CACHE_WRITE) |
| **Cache Read Tokens** | $(printf "%'d" $TOTAL_CACHE_READ) |
| **Cache Savings** | \$$TOTAL_SAVINGS |
---
## Cost by Agent
| Agent | Cost | Tokens | Input | Output |
|-------|------|--------|-------|--------|
EOF
while IFS=$'\t' read -r agent cost tokens input output; do
printf "| %s | \$%.4f | %'d | %'d | %'d |\n" "$agent" "$cost" "$tokens" "$input" "$output" >> "$OUTPUT_FILE"
done < "$TEMP_DIR/by_agent.txt"
cat >> "$OUTPUT_FILE" <<EOF
---
## Cost by Model
| Model | Cost | Tokens | Input | Output |
|-------|------|--------|-------|--------|
EOF
while IFS=$'\t' read -r model cost tokens input output; do
printf "| %s | \$%.4f | %'d | %'d | %'d |\n" "$model" "$cost" "$tokens" "$input" "$output" >> "$OUTPUT_FILE"
done < "$TEMP_DIR/by_model.txt"
cat >> "$OUTPUT_FILE" <<EOF
---
## Cost by Channel
| Channel | Cost | Tokens |
|---------|------|--------|
EOF
while IFS=$'\t' read -r channel cost tokens; do
printf "| %s | \$%.4f | %'d |\n" "$channel" "$cost" "$tokens" >> "$OUTPUT_FILE"
done < "$TEMP_DIR/by_channel.txt"
cat >> "$OUTPUT_FILE" <<EOF
---
## Top Sessions by Cost
| Session Key | Agent | Model | Tokens | Cost |
|-------------|-------|-------|--------|------|
EOF
while IFS=$'\t' read -r _ key agent model channel tokens cost; do
printf "| \`%s\` | %s | %s | %'d | \$%.4f |\n" "$key" "$agent" "$model" "$tokens" "$cost" >> "$OUTPUT_FILE"
done < "$TEMP_DIR/top_sessions.txt"
cat >> "$OUTPUT_FILE" <<EOF
---
*Report generated by OpenClaw daily-cost-report.sh*
EOF
# Cleanup
rm -rf "$TEMP_DIR"
# Output to stdout
cat "$OUTPUT_FILE"
echo ""
echo "✅ Report saved to: $OUTPUT_FILE"
FILE:scripts/send-cost-report.sh
#!/bin/bash
# Send daily cost report via email
# Usage: send-cost-report.sh [recipient-email] [date]
RECIPIENT="-"
DATE="-yesterday"
if [ -z "$RECIPIENT" ]; then
echo "❌ Usage: $0 <email> [date]"
exit 1
fi
# Generate HTML report
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
bash "$SCRIPT_DIR/daily-cost-report-email.sh" "$DATE"
# Determine report date
if date -v-1d +%Y-%m-%d >/dev/null 2>&1; then
DATE_STR=$(date -j -f "%Y-%m-%d" "$DATE" "+%Y-%m-%d" 2>/dev/null || date -j -v-1d "+%Y-%m-%d")
else
DATE_STR=$(date -d "$DATE" "+%Y-%m-%d" 2>/dev/null || date -d "yesterday" "+%Y-%m-%d")
fi
HTML_FILE="/tmp/cost-report-DATE_STR.html"
REPORT_FILE="/tmp/cost-report-DATE_STR.md"
if [ ! -f "$HTML_FILE" ]; then
echo "❌ HTML file not found: $HTML_FILE"
exit 1
fi
# Extract cost from report for subject
TOTAL_COST=$(grep "Total Cost" "$REPORT_FILE" 2>/dev/null | head -1 | awk -F'|' '{print $3}' | xargs 2>/dev/null || echo "unknown")
PRETTY_DATE=$(date -j -f "%Y-%m-%d" "$DATE_STR" "+%B %d, %Y" 2>/dev/null || date -d "$DATE_STR" "+%B %d, %Y")
# Send via mail command (macOS/Linux compatible)
{
# Email headers
echo "From: Bernie <$(whoami)@localhost>"
echo "To: $RECIPIENT"
echo "Subject: 🐈⬛ OpenClaw Cost Report - $PRETTY_DATE ($TOTAL_COST)"
echo "MIME-Version: 1.0"
echo "Content-Type: text/html; charset=UTF-8"
echo "Content-Transfer-Encoding: 8bit"
echo ""
# Email body (HTML)
cat "$HTML_FILE"
} | mail -t -v "$RECIPIENT" 2>/dev/null || {
echo "⚠️ mail command failed. Trying alternative method..."
# Fallback: write to file instead
cp "$HTML_FILE" "/tmp/cost-report-DATE_STR-UNSENT.html"
echo "ℹ️ Report saved to: /tmp/cost-report-DATE_STR-UNSENT.html"
echo "ℹ️ To send: mail -s 'Cost Report' -a 'Content-Type: text/html' $RECIPIENT < /tmp/cost-report-DATE_STR.html"
}
echo "✅ Cost report processed for $PRETTY_DATE"
Create and configure OpenClaw cron jobs with correct scheduling, execution modes, and delivery patterns. Use when asked to schedule a task, set up a recurrin...
--- name: create-cron-job description: Create and configure OpenClaw cron jobs with correct scheduling, execution modes, and delivery patterns. Use when asked to schedule a task, set up a recurring job, create a reminder, run something at a specific time, or automate a periodic operation. verified-against: "2026.3.2" --- # Create Cron Job Set up a scheduled task per `conventions/cron.md`. Read the convention first. **Need a periodic check instead?** Consider `HEARTBEAT.md` — see "Cron vs Heartbeat" in `conventions/cron.md`. ## Before You Start Determine: - Which agent handles the job - Main session (needs conversation context) or isolated (standalone) - Delivery mode (announce, webhook, none) ## Steps ### 1. Choose the execution mode | Question | If yes | If no | |---|---|---| | Does the task need recent conversation context? | Main session | Isolated | | Does the agent need its AGENTS.md/SOUL.md? | Isolated (normal) | Isolated + `lightContext` | | Is this a one-shot reminder? | `--at` with `--delete-after-run` | Recurring schedule | ### 2. Choose a job name Format: `<agent-id>-<purpose>` in kebab-case. Examples: `auditor-daily-report`, `archivist-daily-backup`, `reminder-standup-meeting` ### 3. Choose a schedule | Pattern | CLI flag | Example | |---|---|---| | Cron expression | `--cron` | `--cron "0 7 * * *"` (7 AM daily) | | Fixed interval | `--every` | `--every "4h"` | | One-shot (relative) | `--at` | `--at "20m"` | | One-shot (absolute) | `--at` | `--at "2026-03-15T09:00:00Z"` | Always set timezone for cron expressions: `--tz "America/Los_Angeles"` ### 4. Create the job **Main session** (task needs conversational context): ```bash openclaw cron add \ --name "<agent-id>-<purpose>" \ --every "<interval>" \ --session main \ --system-event "<instruction text>" \ --wake now ``` **Isolated** (standalone task): ```bash openclaw cron add \ --name "<agent-id>-<purpose>" \ --cron "<expr>" \ --tz "<timezone>" \ --session isolated \ --message "<instruction text>" \ --agent <agent-id> \ --announce \ --channel <channel> \ --to "<target>" ``` **Isolated + lightweight context** (simple, self-contained chore): ```bash openclaw cron add \ --name "<agent-id>-<purpose>" \ --every "<interval>" \ --session isolated \ --message "<self-contained instruction>" \ --light-context \ --announce ``` **One-shot reminder:** ```bash openclaw cron add \ --name "reminder-<purpose>" \ --at "<time>" \ --session isolated \ --message "<reminder text>" \ --announce \ --delete-after-run ``` ### 5. Bind to an agent Always use `--agent <agent-id>` for agent-specific jobs. ### 6. Set delivery mode - **Announce** (most jobs): `--announce --channel <ch> --to "<target>"` - **Webhook**: `--webhook <url>` - **None**: omit delivery flags ### 7. Document in the agent's AGENTS.md ```markdown ## Scheduled Tasks | Job | Schedule | Message | Action | |---|---|---|---| | `<job-name>` | `<schedule>` | `<message>` | <what the agent does> | ``` ### 8. Create a skill if the job uses scripts If the cron job executes scripts (not just a self-contained message), create a skill using the `create-skill` skill: - Scripts live in `workspace/skills/<skill-name>/scripts/`, not in ad-hoc workspace directories - The skill makes the capability discoverable for on-demand use, not just cron - Description should cover both automated (cron) and on-demand (user request) triggers - See "Workspace File Placement" in `conventions/skills.md` for where files belong Skip if the cron job's message is fully self-contained (no external scripts or supporting files). ### 9. Test the job ```bash openclaw cron run <jobId> # Force immediate execution openclaw cron list # Verify job exists openclaw cron runs --id <jobId> # Check run history ``` ## Post-Creation Checklist - [ ] Job name follows `<agent-id>-<purpose>` kebab-case convention - [ ] Timezone set for cron expressions (`--tz`) - [ ] Explicit `--agent` binding for agent-specific jobs - [ ] Delivery mode set (announce/webhook/none) - [ ] Channel and target specified for announce delivery - [ ] Agent's AGENTS.md updated with Scheduled Tasks entry - [ ] Job tested with `openclaw cron run <jobId>` - [ ] No duplicate: checked that no system crontab or existing job covers the same task - [ ] One-shot reminders use `--delete-after-run` - [ ] If job uses scripts: skill created in `workspace/skills/<name>/` with scripts in its `scripts/` subdir - [ ] No ad-hoc files or directories created at workspace root (see `conventions/skills.md`)
Configure OpenClaw tool policies, exec security, and per-agent tool restrictions. Use when asked to set up tool access for an agent, restrict tools, configur...
---
name: configure-tools
description: Configure OpenClaw tool policies, exec security, and per-agent tool restrictions. Use when asked to set up tool access for an agent, restrict tools, configure exec security or approvals, set up a tool profile, enable plugin tools, or lock down an agent's capabilities.
verified-against: "2026.3.2"
---
# Configure Tools
Set up tool policies and security following `conventions/tools.md`. Read the convention first for profiles, groups, exec security options, and policy layering rules.
## Decision Flow
1. **What scope?**
- Global (all agents) → `tools.*` in `openclaw.json`
- Single agent → `agents.list[].tools.*`
- Single provider/model → `tools.byProvider.*` or `agents.list[].tools.byProvider.*`
2. **Start with a profile or build custom?**
- Agent fits a standard role → use a profile (`full`, `coding`, `messaging`, `minimal`)
- Agent needs a specific tool mix → use explicit `allow`/`deny` with `group:*` shorthands
3. **Does exec need configuration?**
- Agent runs shell commands → configure `host`, `security`, `ask` (see convention for options)
- Agent should not run shell commands → deny `group:runtime`
## Config Syntax
### Set a profile
```json5
// Global
{ tools: { profile: "coding" } }
// Per-agent
{ agents: { list: [{ id: "<agent-id>", tools: { profile: "messaging" } }] } }
```
### Fine-tune with allow/deny
Use `group:*` shorthands (listed in `conventions/tools.md`) over individual tool names. Deny wins over allow.
```json5
// Profile + deny specific groups
{ id: "<agent-id>", tools: { profile: "coding", deny: ["group:ui", "group:web"] } }
// Profile + allow extras
{ id: "<agent-id>", tools: { profile: "messaging", allow: ["web_search"] } }
// Explicit allow (no profile)
{ id: "<agent-id>", tools: { allow: ["read", "session_status", "memory_search"] } }
```
### Enable plugin tools
Use `alsoAllow` (additive, safe) rather than replacing the allowlist:
```json5
{ tools: { alsoAllow: ["lobster", "llm-task"] } }
```
### Configure exec security
```json5
// Sandboxed (safest)
{ tools: { exec: { host: "sandbox", security: "deny" } } }
// Gateway with approvals (most agents)
{ tools: { exec: { host: "gateway", security: "allowlist", ask: "on-miss" } } }
// Trusted main agent (wide open)
{ tools: { exec: { host: "gateway", security: "full", ask: "off" } } }
```
### Restrict by provider
```json5
{ tools: { byProvider: { "google/gemini-2.5-flash": { profile: "coding" } } } }
```
## Apply Changes
Use the `gateway` tool:
```json
{ "tool": "gateway", "action": "config.patch", "patch": { "tools": { ... } } }
```
Or edit `~/.openclaw/openclaw.json` directly and restart the Gateway.
## Post-Configuration Checklist
- [ ] Non-main agents use least-privilege tool access (profile or explicit allow)
- [ ] Exec security configured appropriately (`host`, `security`, `ask`)
- [ ] No interpreter binaries (`python3`, `node`, `bash`) in `tools.exec.safeBins`
- [ ] Plugin tools explicitly opted in via `alsoAllow` where needed
- [ ] Provider-specific restrictions set for less capable models if applicable
- [ ] Configuration applied and verified