If you landed here looking for a clear recipe on how to set up an OpenClaw morning briefing with an email summary — the kind that hits your inbox at 07:00 with calendar events, unread Gmail, weather, and an overdue-tasks nudge — this is the guide you need. Everything below has been battle-tested on OpenClaw 0.36.1 running on Node 22.2 and a small t4g.micro in ClawCloud. I’ll walk through the exact cron line, the community-maintained gog skill we use for Gmail + Google Calendar roll-ups, how to blend in weather and task lists, and finally how to customise the output per channel (Slack vs. email vs. Telegram) without duplicating logic.
Why a morning briefing is still the killer use case
Most people first install OpenClaw to automate chat replies. A week later they realise the real time-saver is a consistent morning digest: one message that spares them five browser tabs and three different apps. In the GitHub issues the request shows up under different names (#1840 "daily digest", #2301 "assistant morning mode"), but the pattern is identical:
- At a fixed time (usually 06:30-08:00 local) the agent triggers on its own.
- It pulls Gmail threads marked Important or Starred.
- It fetches Google Calendar events for today + tomorrow morning, plus out-of-office flags.
- It sprinkles in weather, commute time, and overdue tasks from a PM tool (most commonly Todoist or Notion).
- It sends an HTML email. Optionally mirrors to Slack #morning-briefing and Telegram.
We’ll recreate exactly that.
Prerequisites and environment
I’m assuming:
- Node.js 22.0 or newer (OpenClaw uses top-level
awaitand a fewimport.metaquirks that broke on Node 20). - OpenClaw 0.36.1 (
npm i -g openclaw@latest). - A running daemon (the long-lived worker) and the gateway UI reachable at
https://<your-subdomain>.claw.cloudif you’re on the hosted tier, orhttp://localhost:4333if self-hosting. - A Gmail account with API creds (OAuth2 client + refresh token). Make sure the scopes include
https://www.googleapis.com/auth/gmail.readonly. - A Google Calendar API token. Same OAuth2 project, but add the
calendar.readonlyscope. curlaccess on the box where cron runs.- Optional: an OpenWeatherMap API key for local weather, and a Todoist token.
Directory layout I keep in ~/openclaw/:
agents/— each agent gets its own folderskills/— custom addons, includinggogsecrets.env— exported by both daemon and cron job
Installing and wiring the community gog Gmail + Calendar skill
gog (stands for Google-on-Go-Go) is a small TypeScript skill written by @luba-s in the community Discord. It batches unread or starred Gmail threads and upcoming calendar events into a single JSON blob. The advantage: OpenClaw receives one consistent structure no matter which Google API endpoint changed last week. To install:
cd ~/openclaw/skills
npm i gog-skill
Then expose it to the agent. Your agent.yml (simplified):
name: morning-bot
skills:
- local: ../skills/gog-skill
env:
GOG_GMAIL_LABEL: "\u2605" # starred
GOG_CALENDAR_ID: "primary"
GOG_TIMEZONE: "Europe/Vienna"
GOG_DAYS_AHEAD: 1
At runtime gog will emit something like:
{
"gmail": [
{"subject": "Invoice due", "from": "acct@vendor.com", "snippet": "Reminder that...", "url": "https://mail.google.com/..."}
],
"calendar": [
{"title": "Daily stand-up", "start": "2024-06-04T09:00:00+02:00", "location": "Zoom"}
]
}
We’ll feed that straight into the LLM prompt.
Building the briefing flow in openclaw.yml
OpenClaw still uses the mis-named openclaw.yml for behaviour trees. Mine sits next to the agent bundle:
version: 1
triggers:
- id: "daily-07:00"
schedule: "0 7 * * *" # cron-style, local time
actions:
- call: gog.fetchDailyDigest
save_as: gogData
- call: weather.getForecast
with:
city: "Vienna"
units: metric
save_as: weather
- call: todoist.getOverdue
save_as: tasks
- call: llm.chat
with:
model: gpt-4o
system_prompt: |
You are a concise personal assistant.
Format the output in HTML /- .
user_prompt: |
Gmail:
{{gogData.gmail}}
Calendar:
{{gogData.calendar}}
Weather:
{{weather}}
Tasks:
{{tasks}}
- call: email.send
with:
to: "me@company.com"
subject: "Morning Briefing {{today}}"
html_body: "{{last.action.result}}"
- call: slack.postMessage
with:
channel: "#morning-briefing"
text: "Morning Briefing — see thread for HTML"
blocks: "{{last.action.result}}"
Notes:
- Schedule lives inside OpenClaw, but I still prefer an outer cron (below) because if the daemon crashes, cron restarts it.
weather.getForecastandtodoist.getOverdueare stock skills from the Composio bundle (npm i @composio/weather @composio/todoist).- The LLM call is last-mile formatting only. We’re not asking GPT to do heavy reasoning; latency stays <3 s on gpt-4o.
Scheduling delivery: cron vs. OpenClaw’s internal scheduler
OpenClaw added a built-in Quartz-lite scheduler in v0.29. It works, but I still recommend a host-level cron entry for two reasons:
- If the daemon crashes at 05:00, the 07:00 job never fires. Cron restarts the daemon before next tick.
- Logs live in
/var/log/cronand your usual alerting pipeline.
My crontab -e on the ClawCloud box:
# restart daemon if missing and run 2 minutes later so env is loaded
58 6 * * * source ~/openclaw/secrets.env && pm2 startOrRestart ~/openclaw/ecosystem.config.cjs
0 7 * * * source ~/openclaw/secrets.env && curl -X POST http://localhost:4333/agents/morning-bot/triggers/daily-07:00/execute
Things to watch:
pm2keeps the Node process alive and handles logs.- We hit the OpenClaw internal HTTP endpoint to fire the trigger manually; the
idin YAML must match. - All creds are in
secrets.env, not in the cron file. Keeps/var/log/syslogclean.
Adding weather and task lists to the same email
The weather skill is a thin wrapper around OpenWeatherMap’s /onecall endpoint. Config block:
skills:
- openweather
env:
OWM_API_KEY: "$OWM_KEY"
The call we used earlier (weather.getForecast) returns roughly:
{"temp": "16°", "feels_like": "15°", "description": "light rain"}
For tasks I prefer Todoist because the API is sane:
skills:
- todoist
env:
TODOIST_TOKEN: "$TDIST_TOKEN"
The getOverdue helper filters tasks where due_date < today and is_completed == false. Returned as an array of {content, project, url}.
We push both blobs into the same LLM prompt so the model can decide ordering. If you care about latency even more, drop the LLM and hand-craft HTML in Node; just template out the JSON with mustache or eta.
Customising delivery time and format per channel
Maybe you want the email at 07:00 but the Slack message at 08:30 when colleagues are awake. Two ways:
1. Split triggers
triggers:
- id: "email-07:00"
schedule: "0 7 * * *"
actions: [ ... only email.send ... ]
- id: "slack-08:30"
schedule: "30 8 * * *"
actions: [ ... only slack.postMessage ... ]
Simple, duplicates the fetch + LLM step unless you cache. Acceptable for small accounts.
2. Single fetch, multiple deliveries
Keep one trigger at 07:00, store the rendered HTML in a Redis KV (there’s a built-in kv.put/get helper), then have a second trigger that only retrieves the cached blob and posts it. Example:
- id: "cache-briefing"
schedule: "0 7 * * *"
actions:
- ...fetch + LLM...
- call: kv.put
with:
key: "briefing:today"
value: "{{last.action.result}}"
- id: "slack-push"
schedule: "30 8 * * *"
actions:
- call: kv.get
with: { key: "briefing:today" }
save_as: html
- call: slack.postMessage
with:
channel: "#morning-briefing"
blocks: "{{html}}"
If you’re on ClawCloud, the Redis instance is included; self-hosters can point the KV skill at any redis:// URL.
Formatting differences:
- Email client likes full HTML with inline CSS. Slack hates CSS but supports Block Kit. The trick: ask GPT to produce both inside the same response. Use YAML front-matter so you can split later.
system_prompt: |
Return a YAML with keys html_email and slack_blocks.
user_prompt: |
...same data blobs...
Parse in a post-processing step:
- call: llm.chat ...
save_as: rawYaml
- call: util.yamlParse
with: { input: "{{rawYaml}}" }
save_as: rendered
- call: email.send
with: { html_body: "{{rendered.html_email}}" }
- call: slack.postMessage
with: { blocks: "{{rendered.slack_blocks}}" }
Testing, logging, and troubleshooting
Few things that typically trip up first-time setups:
- OAuth refresh tokens expire if you re-run consent screens. Gmail will silently drop
importantcriteria and return zero threads. Sniff the raw JSON. - Cron timezone on ClawCloud defaults to UTC. Your agent schedule (
Europe/Vienna) may be correct but cron fires at 05:00 instead of 07:00. Setsudo dpkg-reconfigure tzdata. - Slack HTML stripping: if you paste full HTML into
blocksSlack escapes tags. Use Block Kit JSON or simple*bold*formatting. - LLM hallucinations: once a month GPT decides 12°C is
perfect beach weather
. Validate numbers server-side if accuracy matters. - Rate limits: Google APIs are picky. Batch Gmail calls via
users.messages.batchGetor raisemaxRequestsPerSecondquota.
Log tails I keep open in tmux:
pm2 logs morning-bot # daemon
journalctl -fu cron # trigger calls
And to manually re-run today’s digest without waiting:
curl -X POST http://localhost:4333/agents/morning-bot/triggers/daily-07:00/execute
Where to go from here
The morning briefing is usually step one. From here you can:
- Add a
shell.execstep to dump disk usage or CI status. - Pipe the same HTML to
/tmp/briefing.htmland expose via the gateway as a miniature dashboard. - Use the new
schedule: "sun-thu 22:00"syntax (landed in 0.37-beta) for an evening planning brief. - Contribute back:
gogis looking for Outlook and Microsoft Teams support. PRs welcome.
Go set it up, ship it, and buy yourself the 30 minutes you used to spend checking five apps every morning.