progress-notifier is a lightweight observability plugin for long-running agent tasks in OpenClaw.
It gives long jobs a visible task state instead of leaving you to guess whether the agent is still working, waiting on an external API, or possibly stalled.
Core capabilities:
- structured task progress with stable
taskId - parent and child task trees
- automatic parent aggregation for
percent,status,stage, andlabel - weighted child progress with optional
weight - heartbeat and stale-task watchdog signals
- explicit external-wait tracking with
activityState: "waiting_external" - Feishu pinned cards with automatic refresh
- proactive Feishu alerts for severe states
- file persistence with startup restore for pinned cards and heartbeat visibility
- prompt-context injection for active work
This plugin is intentionally small in scope.
It does:
- record task state
- infer task visibility signals
- sync that state to text output and Feishu cards
It does not try to become:
- a workflow orchestrator
- a tracing platform
- a network diagnostics tool
- a general project-management system
This repository can be used as a local OpenClaw plugin directory. The plugin manifest is:
Enable it in your OpenClaw config:
{
"plugins": {
"entries": {
"progress-notifier": {
"enabled": true
}
}
}
}If your OpenClaw setup requires an explicit path:
{
"plugins": {
"entries": {
"progress-notifier": {
"enabled": true,
"path": "C:\\path\\to\\openclaw-progress-notifier",
"config": {
"injectPromptContext": true,
"persistenceMode": "file",
"persistenceDir": ".progress-store"
}
}
}
}
}Restart OpenClaw and confirm tools such as progress_update, progress_get, progress_tree, and progress_pin_card are available.
The most useful pattern is:
- Create one parent task for the overall job.
- Create child tasks for meaningful work units.
- Update child tasks as work advances.
- Let the parent task aggregate automatically.
- Pin the parent task to Feishu if you want an external progress surface.
Parent task:
{
"taskId": "paper-1",
"label": "Paper workflow",
"stage": "start",
"status": "running"
}Child task:
{
"taskId": "paper-1.search",
"parentTaskId": "paper-1",
"label": "Search sources",
"stage": "research",
"status": "running"
}As child tasks move, the parent task automatically derives:
percentstatusstagelabel
Example parent output:
[research] 1/2 child tasks complete, 1 running
[======>---] 67%
activity: last activity 4s ago
If child tasks do not contribute equally, set weight.
{
"taskId": "paper-1.write",
"parentTaskId": "paper-1",
"label": "Write main draft",
"weight": 3,
"stage": "research",
"status": "running"
}When weights are present, the parent percent becomes a weighted average:
[research] 50% complete (weighted), 1/2 child tasks complete, 1 running
[====>-----] 50%
activity: last activity 8s ago
One of the main goals of this plugin is to make silent agent runs less opaque.
The watchdog separates:
- real progress
- heartbeat-only liveness
- external waiting
- severe waiting
- likely stalls
When you know a task is waiting on an external dependency, update the same task with:
{
"taskId": "paper-1",
"label": "Waiting for OpenAI response",
"status": "running",
"activityState": "waiting_external",
"waitingOn": "openai"
}That produces text like:
[research] [waiting:openai] Waiting for OpenAI response
[====>-----] 48%
activity: waiting on openai | waiting 42s | last activity 42s ago | heartbeat 5s ago
If the wait lasts longer than staleAfterMs, the state upgrades to a slow external call instead of a generic stall:
[research] [api-slow:openai] Waiting for OpenAI response
[====>-----] 48%
watchdog: external call slow (openai) | waiting 3m | last activity 3m ago | heartbeat 5s ago
If there is no external wait signal and no real activity for too long:
[research] [stale] Drafting report
[====>-----] 48%
watchdog: possibly stalled | last activity 4m ago | heartbeat 10s ago
A normal real progress_update clears the waiting state automatically.
Parents now surface the dominant waiting signal from unfinished descendants.
Examples:
waiting on openaiexternal call slow (openai)
Priority rules:
waiting_external_slowwins overwaiting_external- ties fall back to the most recently updated waiting child
This keeps the parent card useful even when only children are being updated.
Feishu is the main external visibility surface for this plugin.
Use progress_pin_card to create or bind a pinned card:
{
"taskId": "paper-1",
"receiveId": "your-chat-id",
"receiveIdType": "chat_id",
"showSummary": true
}When a pinned task changes:
- the card refreshes automatically
- pinned parent cards refresh when derived parent state changes
- watchdog state shows up in the card subtitle and activity section
The card is the main surface. Alerts are only a lightweight escalation layer.
If enableFeishuAlerts is enabled, the plugin sends a separate Feishu text alert when a pinned task enters one of these severe states:
waiting_external_slowstale
Alert text stays intentionally short:
External call slow: waiting on openaiPossibly stalled: no real activity for 4m
Alert behavior:
- alerts trigger on state transition, not every heartbeat
- the same severe state does not repeatedly alert on every refresh
alertCooldownMsprovides extra protection against noisy flapping- alerts are sent to the same Feishu target as the pinned card
When persistenceMode is set to file, the plugin persists task state and Feishu card bindings on disk.
If restoreStateOnStartup is enabled, startup restore will:
- restore pinned Feishu card bindings
- restart heartbeat visibility for tasks in
runningorretrying
First version limits:
- it restores pinned cards and heartbeat/watchdog visibility
- it does not replay old manual
summaryschedules - it does not try to reconstruct full workflow orchestration
The config schema lives in openclaw.plugin.json.
Common options:
| Config | Type | Default | Description |
|---|---|---|---|
ttlMs |
number |
600000 |
Task expiration time in milliseconds |
injectPromptContext |
boolean |
true |
Inject active progress context into prompts |
promptContextLimit |
number |
2 |
Maximum active tasks injected into prompts |
defaultStages |
string[] |
["start","research","draft","revise","done"] |
Stage pipeline used for stage-based percent inference |
persistenceMode |
"memory" | "file" |
"memory" |
Storage backend |
persistenceDir |
string |
".progress-store" |
File persistence directory |
enableScheduledUpdates |
boolean |
false |
Enable manual scheduled updates |
defaultUpdateIntervalMs |
number |
60000 |
Default heartbeat interval |
pushScheduledMessages |
boolean |
true |
Send scheduled update messages to the conversation |
staleAfterMs |
number |
180000 |
Mark active tasks as stale after this much real inactivity |
autoHeartbeatOnProgress |
boolean |
true |
Auto-start heartbeat when a task becomes running or retrying |
enableFeishuAlerts |
boolean |
false |
Send proactive Feishu alerts for severe pinned-task states |
alertCooldownMs |
number |
300000 |
Extra anti-flap cooldown for proactive Feishu alerts |
restoreStateOnStartup |
boolean |
true |
Restore pinned cards and heartbeat visibility on startup in file mode |
feishuAppId |
string |
none | Feishu app id for card push |
feishuAppSecret |
string |
none | Feishu app secret for card push |
Recommended file-persistence setup:
{
"plugins": {
"entries": {
"progress-notifier": {
"enabled": true,
"config": {
"persistenceMode": "file",
"persistenceDir": ".progress-store",
"defaultUpdateIntervalMs": 60000,
"autoHeartbeatOnProgress": true,
"staleAfterMs": 180000,
"restoreStateOnStartup": true,
"enableFeishuAlerts": true,
"alertCooldownMs": 300000
}
}
}
}
}Task management:
| Tool | Purpose |
|---|---|
progress_update |
Create or update a task |
progress_get |
Get a task by taskId |
progress_list |
List tasks in the current conversation |
progress_clear |
Remove one task or all tasks |
Analysis:
| Tool | Purpose |
|---|---|
progress_summary |
Summarize task progress |
progress_replay |
Replay the full event history |
progress_metrics |
Show duration, retries, blocks, and metrics |
progress_children |
List direct children of a task |
progress_tree |
Render the task tree |
Admin:
| Tool | Purpose |
|---|---|
progress_conversations |
List persisted conversation ids |
progress_health |
Show plugin health and config |
progress_cleanup |
Remove expired or empty task records |
Scheduling:
| Tool | Purpose |
|---|---|
progress_schedule |
Start scheduled updates for a task |
progress_unschedule |
Stop scheduled updates |
Feishu:
| Tool | Purpose |
|---|---|
progress_pin_card |
Create or bind a pinned Feishu card |
progress_refresh_card |
Refresh an existing pinned card |
progress_unpin_card |
Remove a pinned card binding |
progress_card_status |
Show current card bindings |
Install dependencies:
npm installType-check:
npm run typecheckRun tests:
npm test- Task data is conversation-scoped.
- Parent aggregation only happens after child task events exist.
- If you do not provide
percent, the plugin can infer it fromstage. - If you do not provide either
percentorstage, leaf tasks can still advance via history-based fallback. - Heartbeats do not count as real progress for fallback percent inference.
MIT