‹ Back to notes

Field Note

Business Notifications vs System Notifications: Notification Isn't One Semantic — Split Into Two Channels

Fixed two versions in one day to handle what looked like a small notification problem — Suwan's morning brief got buried under the watchdog title in Feishu, and the sender fell back to Zhao Zilong. Took both fixes to learn: business delivery and system health are two different notifications.

OpenClawStudioNotification DesignIdentity IsolationFeishupublic-safe
Watercolor sketch: two Feishu messages on a phone — one titled 'Suwan · Morning Brief Draft', the other 'Zhao Zilong · Control Tower · System Alert', titles cleanly separated

OpenClaw Studio · Notification Design

Business delivery (Suwan wrote a morning brief) and system health (the watchdog finished a health-check round) are not the same kind of notification. Push them through the same channel and the business signal will get drowned. Every time.

I only really accepted this after eating two faceplants on Day 1 of the trial. The first one — I opened Feishu and saw nothing but watchdog titles. No way to tell what Suwan had actually delivered. The second one — after I fixed the titles, the sender was still Zhao Zilong. The header said "Suwan · Morning Brief Draft" but the message was coming out of someone else's mouth.

Two versions in one day. Half an hour apart. Looking back, neither one is the principle on its own — it's both together. Drop either and business and system messages glue back into one stream.

The day before, I'd just fixed PATH and assumed the notification layer was stable

The night before, I'd just patched an old LaunchAgent PATH bug — the runner would die halfway through because it couldn't find its tools, and five morning tasks never went out. I fixed the PATH overnight and wrapped the runner in a watchdog so it would always emit an alert when something cut out mid-run. (That story is in the previous post.)

I assumed the notification layer was now solid — runs, doesn't drop, has a safety net. Then I opened Feishu the next morning and discovered the notification layer was a lot more complicated than I thought. It wasn't a "can it send" problem. It was a "is what comes out actually usable by a human" problem. That layer hadn't even been on my radar.

Before v2.4: the watchdog buried everyone

Just after midnight I opened Feishu and saw this stream:

  • "OpenClaw trial watchdog checkpoint complete · task_id=xxx · status=PASS · manual_review_required=yes"
  • "OpenClaw trial watchdog checkpoint complete · task_id=xxx · status=PASS · manual_review_required=yes"
  • "OpenClaw trial watchdog checkpoint complete · task_id=xxx · status=PASS · manual_review_required=yes"

One after another. Once the watchdog wrapper was in place, every task notification got rewrapped — Suwan filed a morning brief, Huo Rui shipped a research memo, all of it rewritten into "watchdog checkpoint complete" format. The owner of the task (Suwan owns the morning brief, for instance), the deliverable, the actual content — all of it buried behind a wall of fields.

Worse — Codex fires one of these every time it finishes a checkpoint, PASS or FAIL. 99% of checkpoints are PASS. Which means 99% of the messages are really just "health check passed" — the kind of thing that should never interrupt anyone. But they were sharing a channel with Suwan's morning brief, so the business signal was drowning in a flood of system noise.

That's when it landed: business delivery and system health are two different notifications. Business delivery is something you read, use, reply to. System health is PASS 99% of the time, and PASS should be silent. Putting them in the same channel means the safety net eats the main delivery.

v2.4: split the content — give every owner their own title and body

12:32. I pushed v2.4 (commit f62382b). One job — make business-delivery notifications look like business deliveries.

Four changes landed at once.

One: per-owner delivery renderer. Every kind of deliverable gets its own title template — no more sharing a generic "watchdog format".

  • "Suwan · Morning Brief Draft", "Suwan · Column Draft", "Suwan · Evening Brief Draft"
  • "Huo Rui · Pre-Market Research Brief", "Huo Rui · Mid-Session Summary", "Huo Rui · Closing Summary", "Huo Rui · Post-Close Research Review", "Huo Rui · Deep Research Review", "Huo Rui · Weekly Summary", "Huo Rui · Next-Phase Research Recommendations"
  • "Shen Zhixing · Daily Source Package"

One glance at the title and you know who shipped what. You don't have to dig into task_id to find out.

Two: an actual content snippet in the body. Before, the body was a dump of task_id, status, output_path, with a file path tacked on the end. After the change, the renderer reads output.md directly, extracts the real body starting at the ## content_body anchor, and sends a 1500-word excerpt with 800 words for the section tail. What I see is the draft itself, not metadata about the draft.

Three: a silence rule for the Codex watchdog. Checkpoints read overall_status from the JSON they just generated —

  • PASS / PASS_WITH_WARNINGS → silent. Write the file, send nothing to Feishu. Log one line in state: skipped=true, reason=checkpoint_pass_silent.
  • WARN / FAIL / UNKNOWN → fire the alert.

PASS is the default state. The default state shouldn't make noise. Only when PASS stops being PASS is it worth interrupting a human.

Four: unified alert title. Every system alert now starts with "OpenClaw Watchdog Alert". The watchdog no longer borrows the business title format. From the title alone you can tell: this is business, that is system.

I ran a FAKE_PASS_SMOKE test — forced Codex to return PASS to confirm the silence path actually fires. Notification log was 13 lines going in, 13 lines coming out — nothing sent. State had one new entry: skipped=true reason=checkpoint_pass_silent. The silence path worked.

Then I resent the five Day-1 business deliveries that had been buried under watchdog titles. This time what came through was "Suwan · Morning Brief Draft" plus the actual body. I thought the problem was fixed.

v2.5: split the identity — the sender has to be the right person

It wasn't fixed.

After v2.4 went out I looked at the new messages myself — the title really did say "Suwan · Morning Brief Draft", but in the Feishu chat list the sender avatar and name were still "Zhao Zilong · Control Tower".

That's impersonation. Title says Suwan, sender is Zhao Zilong — psychologically, Zhao Zilong is putting words in Suwan's mouth.

I traced it down. The openclaw message send call wasn't passing --account, so the gateway fell back to zhao_zilong — the platform's default account. Every message, no matter what title it carried, was being sent from Zhao Zilong's bot.

Not a new bug — I'd noticed it before, but kept assuming "the body says who it's from, that's enough". After v2.4 shipped and I saw the result, I finally got it: not even close. The sender identity and the title content can't live apart. Once they're separated, what humans see is impersonation, not collaboration.

Six Agents, each one has to send from its own Feishu bot. Each message arrives as a peer in my chat list, not all crammed into a single Zhao Zilong window. This is identity isolation, not UI polish.

13:00. v2.5 (commit 5129a83).

Change one: AGENT_FEISHU_ROUTES, an explicit map. A routing table that pins each agent to a specific profile + account + display name:

  • suwan → openclaw-studio / su_wan / Suwan
  • huorui → openclaw-studio / huo_rui / Huo Rui
  • shenzhixing → openclaw-jiyanran / shen_zhixing / Shen Zhixing
  • watchdog → openclaw-studio / zhao_zilong / Zhao Zilong · Control Tower

Look at the third row — Shen Zhixing isn't in the openclaw-studio profile, he lives in openclaw-jiyanran. Which means routing isn't just picking an account, the profile has to cross over too. I hadn't thought of that early on, but once the routing table makes it explicit, "cross-profile" is just one extra column in the table.

Change two: send_feishu_message(message, route=...) + resolve_feishu_personal_target(profile, account). Thread profile and account all the way through every openclaw call — stop letting the gateway guess. Every message goes out from the owning agent's own bot. In Feishu I now see four distinct open_ids landing in my chat list, one independent window per Agent.

Change three: an "article-first" delivery template. v2.4's body was "dump a pile of header fields, then 1500 words of truncated body". v2.5 flips it — the body comes first, because the business deliverable is the message content. No header dump needed. Suwan's article goes through in full, up to 12000 words (no more 1500-word truncation, since the body is the deliverable). Huo Rui's reports get parsed on ## sections, and 12 banned words get re-scanned at delivery time — compliance issues shouldn't be the renderer's blind spot.

Change four: rewrite watchdog alerts in plain language. Before, watchdog alerts were field-and-path dumps that read like a machine talking to itself. Now they're four short Chinese sections: what happened / impact / what's been done / what's needed. I don't have to decode the alert anymore — I see immediately what happened, who's affected, what the watchdog has already done, what I still need to do.

Both versions together are what "semantic separation" actually means

v2.4 and v2.5 are not solving the same problem.

v2.4 fixes "what the content looks like" — titles, snippets, silence rules. Business messages look like business, system alerts look like system.

v2.5 fixes "who the message comes from" — routing table, explicit profile + account, independent bots. Each Agent is actually speaking for themselves.

Just v2.4: title is right, sender is wrong — psychologically zhao_zilong is still impersonating Suwan. Just v2.5: sender is right, body is still a header dump — Suwan's bot is sending out the same robotic field-concat. Both together is the first time business and system actually run on two separate pipes: split on content, split on identity.

The whole point of "business notifications vs system notifications" is this. A notification system that wants to actually split into two channels has to split on at least two layers at once — title and body (who it looks like), and sender identity (where it comes from). Drop either layer and it isn't a split.

Where this generalizes

In OpenClaw Studio this happens to be Feishu + multiple Agents, but the principle is tool-agnostic. Any setup where "business results" and "system health" share a single notification channel will hit the same wall:

  • CI/CD — build success and infra monitoring share one Slack channel, build success gets drowned in the monitoring PASS heartbeat.
  • Monitoring alerts — business-metric anomalies and infrastructure health use the same title template, business anomalies get buried under infra PASS spam.
  • Cron jobs, scrapers — task delivery and scheduler health share one notification queue, the delivery content gets buried under "scheduler completed a round" receipts within minutes.
  • Multi-Agent systems — every Agent sends through the same bot, every message looks like it's from the same sender, the sense of collaboration flattens into one person doing it all.

The test is simple: open your notification list. Can you tell at a glance which messages are content meant for you, and which are machines clocking in? If you can't, split the channel. And splitting isn't just adding tags or separate channels — it has to happen on content templates, silence rules, and sender identity all at once.

One more: PASS notifications and FAIL notifications shouldn't share a title template. PASS is the default state, and the default state shouldn't make noise. Give FAIL its own loud prefix (e.g. "OpenClaw Watchdog Alert") so a human can spot it instantly in 100 messages. This pairs with the two-channel split — if you split the channels but still let PASS make noise, the business channel will still drown.

v2.4 + v2.5 have been running for a few days. Six Agents each send from their own bot, PASS stays silent, FAIL stands out. Business delivery doesn't get eaten by system heartbeats anymore. That layer is stable for now.

But every new Agent means another row hand-added to AGENT_FEISHU_ROUTES. Next step is auto-registration — agents declare their own route at startup, no human maintaining a global table. The Codex watchdog's "silence threshold" is still being tuned — whether PASS_WITH_WARNINGS counts as PASS or as WARN, there are a few edge cases I haven't closed yet. Huo Rui's research reviews get really long sometimes, and parsing by ## sections still drops edge content — the 12000-word cap is fine for Suwan but tight for Huo Rui.

Notification design is never "done". Every new Agent, every new deliverable type, every new system alert means walking through both layers again. I'm used to it now — notification isn't one semantic, it's at least two, and it'll keep splitting from here.

Turn this note into a route

After reading, ask a follow-up, return to the curated archive, or use the tag index to follow the same thread.

Ask about this Open archive Browse tags