Getting Discord Messages from Openclaw to Actually Show Up
4 days agoWhen 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:
- Run the script — executes the Python script that fetches and formats the data
- Wrap the output — takes whatever the script printed and wraps it in triple backticks
- 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 agoAfter 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:
- Runs the script at 6:50 AM (scheduled via cron)
- Wraps the output in Discord code blocks
- Posts to #weather channel
- 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:
- Runs the script hourly (scheduled via cron)
- If output is "SILENT": does nothing
- If output is alert data: posts to #weather
- 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 2026There 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 2025On rolling green plains, arms outstretched, child runs, eyes up at the cotton clouds.
Treats - Haiku
Apr 2nd 2025Thin strawberry slice Atop warm, sweet-smelling crepe Powdered-sugar snow
Spring - Haiku
Mar 26th 2025Baby-new green leaf displays a crystal clear orb, above rich, dark earth
I prefer tongue-tied knowledge to ignorant loquacity.
A home without books is a body without soul.
Gratitude is not only the greatest of virtues, but the parent of all the others.
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?