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!