Tim Habersack

Where I put my things..

Getting Discord Messages from Openclaw to Actually Show Up

4 days ago

When I was building automated monitoring systems for OpenClaw — weather forecasts, air quality checks, that kind of thing.. they all needed to post to Discord channels automatically. Seemed straightforward enough, right?

Yeah, no.

The Problem

I spent way more time than I'd like to admit fighting with message delivery.

OpenClaw has this feature for isolated session cron jobs called delivery.mode: "announce". You configure it like this:

{
  "delivery": {
    "mode": "announce",
    "channel": "1470927938055573692"
  }
}

...and it should automatically post job results to the channel you specify.

Except it didn't work reliably. Messages would sometimes not deliver. Or they'd deliver to the wrong place. Or they'd just vanish. This was particularly annoying for critical stuff like battery warnings — you know, the kind of thing where if you miss the alert, you end up with a dead laptop.

Plus, my monitoring scripts output nicely formatted data designed for Discord code blocks:

☀️ SAN DIEGO WEATHER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

TODAY
• 72°F currently
• High: 76°F at 2 PM
• No rain expected

The triple-backtick formatting would sometimes get stripped or parsed as markdown. The result was pretty much unreadable in Discord.

After much hunting around, I finally figured out what actually works.

The Solution

The solution ended up being really simple (which is how you know you overcomplicated things to begin with):

Run cron jobs with delivery.mode: "none", and have the agent explicitly call the message tool to post results.

That's it. No automatic delivery. No routing logic. Just "hey agent, run this script and send the output to this specific channel."

Here's what a working cron job looks like:

{
  "name": "Weather System",
  "schedule": {
    "kind": "cron",
    "expr": "50 6 * * *",
    "tz": "America/Los_Angeles"
  },
  "payload": {
    "kind": "agentTurn",
    "message": "Run weather forecast for San Diego (92130), wrap output in code block, send to #weather",
    "model": "sonnet"
  },
  "delivery": {
    "mode": "none"
  },
  "sessionTarget": "isolated"
}

The key things: 1. delivery.mode: "none" — no automatic announcement 2. The message text explicitly says what to do: "send to [#weather](https://tim.hithlonde.com/tag/weather)" 3. The agent handles everything — runs the script, wraps output, sends it

What the Agent Does

When the cron job fires:

  1. Run the script — executes the Python script that fetches and formats the data
  2. Wrap the output — takes whatever the script printed and wraps it in triple backticks
  3. Send it — calls the message tool directly with the channel ID

The agent calls the message tool directly:

# What the agent does internally (conceptually)
message_tool(
    action="send",
    channel="1470927938055573692",
    message=f"```\n{script_output}\n```"
)
An Example

Here's my weather forecast system. The Python script does all the formatting:

#!/usr/bin/env python3
# weather-system.py

def format_weather(data):
    output = f"""☀️ SAN DIEGO WEATHER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

TODAY
• {data['current_temp']}°F currently
• High: {data['high']}°F at {data['high_time']}
• {data['precipitation_summary']}

TOMORROW
• High: {data['tomorrow_high']}°F
• Low: {data['tomorrow_low']}°F
• {data['tomorrow_summary']}"""

    return output

# Just print it, agent handles the rest
print(format_weather(weather_data))

The script outputs pre-formatted text. The agent wraps it in code blocks and posts it to [#weather](https://tim.hithlonde.com/tag/weather). Works perfectly every time.

Why This Works

Explicit Control — The agent knows exactly when to send the message, where to send it (specific channel ID), and how to format it (code blocks stay intact). No intermediary processing that might mess things up.

Debuggable — When something goes wrong, you can check the isolated session logs, see the exact tool call that was made, verify the channel ID and permissions, and test the script output directly.

Simple Pattern — Every monitoring system uses the same approach: script generates formatted output, agent wraps it in code blocks, agent sends it via message tool. Once you have the pattern down, adding new systems is really easy.

Summary

This took some time to figure out!

My OpenClaw Script Pattern (Or: How I Stopped Burning Tokens and Started Building Tools)

1 week ago

After using OpenClaw for a couple weeks, I've settled on a pattern that works really well: make scripts do the actual work, OpenClaw just executes them.

The Problem with Pure AI Generation

For the first day or so, I had OpenClaw generating output directly. Need a weather forecast? Ask the AI to fetch the data, format it nicely, and post it to Discord.

This worked, but it burned tokens, produced inconsistent formatting, and wasn't reusable outside of AI conversations. I needed a better approach.

The Script-First Architecture

The pattern I landed on is pretty straightforward: write focused Python scripts that do one thing well, then have OpenClaw execute them.

Instead of this:

"OpenClaw, fetch the weather for San Diego, format it nicely with emoji and box drawing characters, and post it to #weather"

I do this:

"OpenClaw, run the weather script and post the output to #weather"

The script handles all the logic — API calls, data parsing, formatting, output. OpenClaw just executes it and relays the result.

Example: Weather Forecast System

Every morning at 6:50 AM, I get a weather forecast for San Diego posted to my #weather Discord channel.

File structure:

weather-system/
├── .venv/                    # Python virtual environment
├── weather-system.py         # Main script
├── requirements.txt          # Dependencies (requests)
└── .last-run                 # Timestamp tracking

What the script does:

  • Calls Open-Meteo API for San Diego coordinates
  • Parses current temp, today's high, precipitation timing
  • Formats output with weather emoji and Unicode box drawing
  • Prints pre-formatted text

Output format:

☀️ WEATHER - 92130
Current: 62°F @ 06:50
Today High: 68°F @ 14:00
Precipitation: 04:00-09:00 (heavy 05:00-06:00)

Tomorrow:
High: 58°F @ 13:00
Low: 48°F
Precipitation: none

What OpenClaw does:

  1. Runs the script at 6:50 AM (scheduled via cron)
  2. Wraps the output in Discord code blocks
  3. Posts to #weather channel
  4. Done

Total tokens used per run: minimal (just executing a script and posting output, not generating the forecast content).

Example: Air Quality Monitor

This one runs every hour but only posts when air quality is above 50 or crosses the threshold in either direction.

File structure:

aqi-monitor/
├── .venv/
├── aqi-monitor.py
├── aqi-state.json           # Tracks last reading for threshold detection
└── requirements.txt

What the script does:

  • Fetches current AQI from IQAir API
  • Loads previous reading from state file
  • Checks if current AQI > 50 OR crossed threshold
  • If alert condition: prints formatted alert
  • If no alert needed: prints "SILENT"
  • Updates state file with current reading

Output format (when alerting):

AQI - 92130 (San Diego) @ 11:00
AQI: 65 | Main: PM2.5

What OpenClaw does:

  1. Runs the script hourly (scheduled via cron)
  2. If output is "SILENT": does nothing
  3. If output is alert data: posts to #weather
  4. Done

This prevents spam — I only get notified when air quality actually matters, not every single hour saying "everything is fine."

The Pattern

Both systems (and the four others I've built) follow the same approach:

1. Focused Python script that does one thing well
2. Pre-formatted output designed for Discord code blocks
3. State tracking when needed (for thresholds, timestamps, rate limiting)
4. Simple execution via OpenClaw cron jobs
5. Explicit channel routing using the message tool

No complex logic in the cron configuration. No asking the AI to format things on the fly. Just "run this script, post the output to this channel."

Token Management Strategy

Another benefit of this is easier cron job setups. I pay for Claude API usage and need tokens for work during the day. So automation tasks run as cron jobs in the middle of the night. Also sometimes a bigger operation I will make as a script then have it be cronned for late at night.

This keeps my daytime budget available for interactive work. Plus I wake up to completed tasks posted to Discord, which is pretty nice.

Summary

Once you have the pattern down, adding new systems is pretty straightforward. I can usually go from idea to working automation in under an hour.

Anyway, that's how I approach making tools for Openclaw so far. If you're building something similar, hopefully this gives you a useful starting point.

Reverting just a couple files from a while ago

Jan 18th 2026

There were 3 files in my repo that had been inadvertently changed like 20 commits ago. I just wanted to revert 3 of them, not all the files in the commit. This was easiest way to do it.

git show <commit-hash> -- path/to/file1.php path/to/file2.js path/to/file3.html | git apply -R

I always forget this and relearn it every 2 years, so maybe next time around I'll look here.

Freedom - Haiku

Jun 26th 2025
On rolling green plains,
arms outstretched, child runs, eyes up
at the cotton clouds.

#poetry

Treats - Haiku

Apr 2nd 2025
Thin strawberry slice
Atop warm, sweet-smelling crepe
Powdered-sugar snow

#poetry

Spring - Haiku

Mar 26th 2025
Baby-new green leaf
displays a crystal clear orb,
above rich, dark earth

#poetry

Jan 17th 2025

I prefer tongue-tied knowledge to ignorant loquacity.

Marcus Tullius Cicero

#quote

Jan 15th 2025

A home without books is a body without soul.

Marcus Tullius Cicero

#quote

Jan 14th 2025

Gratitude is not only the greatest of virtues, but the parent of all the others.

Marcus Tullius Cicero

#quote

Jan 13th 2025

It's been a very long time since I have posted here. This is the obligatory "oh gosh I will post here more frequently!" post. :D

Initially I am going to be posting quotes that inspire me every morning. This sounds super cheesy YET, it's nice to think about things outside the normal daily cycle, you know?

< Older