Scheduler Monitor Mode
Wake a scheduled job only when a watched source changed — and hand the agent only the diff
Turn any scheduled job into a change-aware monitor — wake only when a watched source changed, and hand the agent only the diff.
Quick Start
- Agent-centric — watch a page, tell me only what's new
The agent schedules the monitor for you from a plain request.
python from praisonaiagents import Agent from praisonaiagents.tools import schedule_add agent = Agent(name="watcher", tools=[schedule_add]) agent.start( "Every hour, monitor https://status.example. Only tell me when it changes, " "and tell me only what's new." ) - Direct Python — build the ScheduleJob yourself
Set
monitorto name a cheap source to probe each tick.python from praisonaiagents.scheduler import ScheduleJob, parse_schedule job = ScheduleJob( name="site-watch", message="The watched page changed. Here is the diff; summarise what is new.", schedule=parse_schedule("weekdays at 9am"), # natural language also works monitor={"url": "https://status.example"}, # or {"command": "..."} ) - Shell source — probe a cheap counter
A bounded command is a better source than a full HTML page.
python job = ScheduleJob( name="unread-watch", message="Unread count changed — summarise what is new.", schedule=parse_schedule("every 1h"), monitor={"command": "curl -s https://api.example/unread | jq '.count'"}, ) - YAML — configure via agents.yaml
yaml schedule: every: "1h" message: "Summarise what is new." monitor: url: "https://status.example" deliver: "telegram:123456"
How It Works
The wrapper hashes the probed source and compares it to the last-seen hash held in per-job state.
Caller contract (verbatim from the SDK): “Because a legacy stateless gate accepts only job, callers MUST NOT unconditionally pass state=. Detect capability first — e.g. call should_run(job, state=prior) only when the gate opts into state, otherwise fall back to should_run(job) — so stateless gates keep working unchanged and never raise TypeError.”
An error probe (a transient 500) leaves prior state untouched, so a later recovery to the prior output still suppresses — a transient failure does not count as a change.
Run Status: no_change vs the others
RunRecord.status gains a fourth value so an operator can tell a silent monitor apart from a deliberate skip.
status | Meaning | Tokens | Delivery |
|---|---|---|---|
succeeded | The model turn ran and finished | Yes | Yes |
failed | The run errored | Maybe | On-failure only |
skipped | A go/no-go gate said “don’t run” | No | No |
no_change | A watched source was unchanged since the last tick | No | No |
What’s Stored in Per-Job State
The monitor keeps a small durable scratchpad keyed by job id — typically the last-seen hash, plus an optional cursor / watermark.
from typing import Any, Dict, Protocol, runtime_checkable
@runtime_checkable
class JobStateStoreProtocol(Protocol):
"""Bounded per-job durable scratchpad. All methods OPTIONAL — callers use hasattr()."""
def get_state(self, job_id: str) -> Dict[str, Any]: ...
def set_state(self, job_id: str, state: Dict[str, Any]) -> None: ... # size-capped
def clear_state(self, job_id: str) -> None: ... # called when a job is removedThe state is bounded (size-capped by the implementation) and cleared when the job is removed. The core owns only the shape; the wrapper decides where to persist it.
Distinct From Other Gates
Pick the right sibling — monitor is the stateful one that seeds a diff.
| Field | Stateful? | Seeds | Distinct outcome |
|---|---|---|---|
pre_run | No | Whole gate output | skipped |
monitor | Yes | A bounded diff | no_change |
command | No | Nothing (no LLM) | model-free action |
Configuration Options
The monitor field is a small mapping naming one cheap source to probe each tick.
| Key | Type | Description |
|---|---|---|
command | str | A shell command whose stdout is hashed and diffed |
url | str | A bounded fetch whose body is hashed and diffed |
monitor={"url": "https://status.example"}
# or
monitor={"command": "curl -s https://api.example/unread | jq '.count'"}Best Practices
Prefer a bounded, cheap probe
A JSON endpoint returning a single counter beats hashing a full HTML page — it changes only when the thing you care about changes, and it is fast on every tick.
# Good — a small, stable signal
monitor={"command": "curl -s https://api.example/unread | jq '.count'"}
# Avoid — a whole page churns on ads, timestamps, and markup noise
monitor={"url": "https://example.com/full-homepage"}A transient error is not a change
An error probe leaves prior state untouched — a transient 500 does not “count as a change”. When the source recovers to its prior output, the tick still suppresses. Design probes that fail loudly (non-zero exit) rather than emitting an error page as “content”.
Use no_change records to prove a monitor is alive
A silent monitor looks identical to a broken one. Check run history — a stream of no_change records proves the monitor is probing every tick and simply has nothing to report.
praisonai session show # look for no_change entriesNever pass state= to a stateless gate
Callers MUST detect capability first. Pass state= only when the gate opts into state; otherwise fall back to should_run(job) so legacy stateless gates never raise TypeError.
See Also
Configure a monitor from the CLI with --monitor-url (public HTTP(S)) or --monitor-command (shell stdout) — see CLI: schedule → Wake only when a page changes. Both flags produce byte-identical jobs to the monitor={"url": …} / monitor={"command": …} form here, and the URL path carries the reliability guarantees documented on the core Scheduler Monitor page.
Related
The notepad that persists state across runs — now wired
Scheduler Pre-Run GateThe stateless sibling — a cheap go/no-go gate
Scheduler Command ActionThe no-LLM sibling — deliver a command’s stdout verbatim
Scheduler DeliveryWhere results go — note no_change ticks do not deliver
The scheduler this monitor plugs into