Skip to main content
Turn a scheduled agent into a monitor: probe a cheap source each tick, run the model only when it changed, and stay silent when it didn’t.
The monitor field is a declarable spec the core round-trips through storage. The probe, hashing, bounded diff, state persistence, and the no_change record live in the praisonai-bot / wrapper layer — the core owns only the contract.

Quick Start

1

Turn an agent into a monitor

Add monitor to a ScheduleJob. The agent runs only when the watched URL changes.
2

Watch a shell source instead

Use command inside monitor to watch any cheap shell output — a file, a database count, a health probe.

What The User Sees

Silence is the feature — an unchanged source produces no ping and spends no tokens.

Three Outcomes

Every tick records exactly one status.

no_change is distinct from skipped (a generic gate go/no-go) so you can tell “nothing changed” apart from “the gate said don’t run”.

How It Works

Each tick probes the source, hashes the output, and compares it to the last-seen hash held in per-job state.
no_change is a distinct status from skipped. skipped means “a gate said don’t run”; no_change means “a watched source was unchanged” — so operators can tell the two apart in run history.

Configuration Options

monitor spec

monitor is a small mapping naming one cheap source to probe each tick.

ScheduleJob field

GateResult — monitor outcome

The wrapper’s monitor gate returns a GateResult carrying the monitor-mode outcome. Setting no_change=True always forces run=False, so a monitor can never fire a tick the contract defines as suppressed.

RunRecord.status


URL Monitor Reliability & Safety

Every URL probe runs behind a bounded, hardened path. These are the guarantees an operator can rely on before pointing a monitor at anything they don’t own end-to-end.
Slow-body drip protection (PR #4237): the total-deadline watchdog now aborts even on Connection: close responses where http.client has already nulled the socket. Previously a hostile page dripping one byte per socket window could stall the entire sequential scheduler tick — the fix retains a raw socket handle so the watchdog can still shut the connection down.
The retained preview is currently smaller (2048 B) than the digest window (64 KiB), so a change that lands past byte 2048 reports "Monitor output changed outside retained preview." as the seeded diff — the run still fires, but the diff is a marker rather than the bytes that changed. Track PraisonAI #4233 for the follow-up that bounds the preview to the digest window.

Monitor vs Pre-Run vs Command

Three scheduler features feel similar but solve different problems.
From the SDK docstring: “monitor is distinct from pre_run (a stateless go/no-go gate) and command (a model-free delivery action).”
On the scheduled delivery path, a no_change outcome is silently suppressed — no ping, no tokens. See Scheduler Delivery and Bot Intentional Silence for how unattended-monitor silence is enforced unconditionally.The heavy MonitorGate (URL/shell probe + hashing + diffing + state persistence) is a wrapper-layer feature. This page documents the core contract — the declarable monitor spec, the GateResult outcomes, and the state-store protocol.

Per-Job State

A monitor needs memory across wake-ups — the last-seen hash or a watermark. The core defines the JobStateStoreProtocol contract, and the concrete ConfigYamlScheduleStore implementation now ships (PR #4057) with a 16 KiB per-job cap. The executor auto-injects a “Notepad” block into the prompt and persists GateResult.state_updates on go and no_change ticks alike.
All JobStateStoreProtocol methods are optional. Callers detect support with hasattr() and treat absence as “no per-job state” — today’s stateless behaviour. Legacy stateless gates (should_run(self, job)) still satisfy the protocol under runtime_checkable, so callers must detect capability before passing state=.
See Cross-Run Memory for the full notepad walkthrough — the format, how the agent writes back, and the one-shot-job caveat.
PR #4205: Scheduled ticks now execute via the shared run_sync_or_offload bridge instead of a fresh asyncio.run() per fire. This preserves per-loop LiteLLM / HTTPX / asyncpg connection pools across ticks, lets Langfuse (and other flushers) actually flush from scheduled runs, and honours scoped_bridge bindings. Long-running scheduled agents run unbounded (timeout=None) — matching pre-fix behaviour — because ScheduleLoop already claims the occurrence atomically before on_trigger fires.

CLI, YAML, and Python Parity

The same monitor spec round-trips through every front-end.
ScheduleJob.to_dict() / from_dict() only persist monitor when it’s configured, so stateless jobs stay byte-for-byte unchanged.

Best Practices

The probe runs on every tick. Watch a small, cheap source — a status endpoint, a row count, a file’s modification marker — not a multi-megabyte page. A bounded source keeps the ticker responsive.
Reach for monitor when the whole point is “only tell me when something changed” — a page, a feed, a metric. Unchanged ticks cost zero tokens and deliver nothing.
If the decision is a stateless go/no-go (an inbox check, a queue depth), use pre_run instead — it doesn’t need to remember a prior hash.
The first tick has no prior state, so it always runs and stores the baseline hash. Expect one delivery when a monitor job is created, then silence until the source moves.
Filter run history on no_change to see “how often was the source unchanged” without conflating it with a generic gate skip.
Return state_updates=None on an error probe so prior state is untouched — a later recovery to the prior output still suppresses correctly.
State is a small scratchpad, not a database. Store cursors and hashes, not full payloads, so a runaway write can’t grow unbounded.
Each URL probe is capped at a total wall-clock deadline (default 30 s) covering connect and body read, so a slow-drip attacker can no longer stall the scheduler tick. A legitimate page you don’t need still costs a fetch on every tick, though — prefer a small JSON endpoint over a full page and the whole loop stays cheap.

Cross-Run Memory

The notepad a monitor reads and writes across runs

Pre-Run Gate

Stateless go/no-go gate — skip when there’s nothing to do

Command Action

Model-free action — deliver a command’s stdout verbatim

Scheduler Delivery

Push scheduled results to Telegram/Discord/Slack/WhatsApp

Bot Intentional Silence

How unattended silence is enforced

Async Scheduler

Schedule agents on intervals, cron, or one-shot timestamps

Context Chaining

Fan a monitor’s output into a downstream job as bounded context

Scheduled Run Policy

Safety gates on what a scheduled run may do