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 await and a few import.meta quirks 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.cloud if you’re on the hosted tier, or http://localhost:4333 if 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.readonly scope.
  • curl access 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 folder
  • skills/    — custom addons, including gog
  • secrets.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.getForecast and todoist.getOverdue are 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:

    1. If the daemon crashes at 05:00, the 07:00 job never fires. Cron restarts the daemon before next tick.
    2. Logs live in /var/log/cron and 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:

    • pm2 keeps the Node process alive and handles logs.
    • We hit the OpenClaw internal HTTP endpoint to fire the trigger manually; the id in YAML must match.
    • All creds are in secrets.env, not in the cron file. Keeps /var/log/syslog clean.

    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 important criteria 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. Set sudo dpkg-reconfigure tzdata.
    • Slack HTML stripping: if you paste full HTML into blocks Slack 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.batchGet or raise maxRequestsPerSecond quota.

    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.exec step to dump disk usage or CI status.
    • Pipe the same HTML to /tmp/briefing.html and 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: gog is 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.