How competitor monitoring works
A competitor watch is a recurring row in the Olympus scheduler table. On its cron tick the scheduler hands the stored prompt to a named agent persona, the agent fetches the competitor's public pages, writes a dated snapshot to a persistent volume, diffs it against the previous snapshot, and reports the difference to a Slack destination. There is no separate monitoring service; the whole system is one table, one poller, and one durable directory.
The loop, step by step
The watch is a scheduler row
Each watch is one row in olympus.scheduled_tasks holding the full instruction prompt, a cron_pattern such as 0 13 * * 1 for Mondays at 13:00 UTC, and the persona that should run it. Cancelled watches stay in the table with state cancelled, so always confirm which row is pending before assuming a schedule is live.
The scheduler service ticks
A dedicated scheduler container polls the table on a loop and runs scripts/run-scheduler-once.ts. It is gated by SCHEDULER_DISABLED and only starts under the compose profile that includes it, so a lean local stack will never fire a watch.
A persona executes the prompt
The due row is dispatched to a Hermes worker running the named persona. The prompt is the entire specification: which URLs to fetch, where to store snapshots, what counts as a material change, and what to escalate. Nothing about the watch lives in code, so editing a watch means editing its prompt.
Pages are fetched and a dated snapshot is written
The agent fetches the listed pages with its web extraction tool and writes a markdown snapshot named snapshot-YYYY-MM-DD.md into the watch directory on the shared hermes-homes Docker volume.
The new snapshot is diffed against the previous one
If no snapshot exists the run captures a baseline and stops. Otherwise it compares against the newest file and reports only material differences: pricing, packaging, product surface, integrations, positioning, and proof points.
Material changes are reported and escalated
The digest goes to the configured Slack destination. Each watch also carries an explicit escalation condition, the single change that would alter Legion's competitive answer, which the agent is told to flag loudly in the week it happens.
Prerequisites before a watch goes live
| Requirement | Why it matters | How to verify |
|---|---|---|
| A running scheduler service | The Olympus stack must be up with the scheduler container running and SCHEDULER_DISABLED unset. On a lean local stack the scheduler is not started and no watch will ever fire. | docker compose ps scheduler |
| A persona that can reach the web | The watch must name a persona whose profile includes a web extraction tool. Hephaestus and Iris both qualify. A persona without web tooling will fire and produce nothing. | olympus profiles |
| A persistent snapshot directory | Snapshots must be written under the hermes-homes volume, mounted in every worker at /data/hermes-homes/<workspace-id>/competitor-watch/<slug>/. This is the single most common failure: paths like /root or /tmp are inside the worker container and are destroyed on every redeploy, which silently resets the baseline and means no diff is ever produced. | docker compose exec worker-1 ls /data/hermes-homes/<workspace-id>/competitor-watch/ |
| A Slack destination | Without a configured Slack channel or thread the digest is written only to the scheduler log, where nobody reads it. | Check HERMES_SLACK_CHANNEL is set for the stack. |
| Confirmed live URLs | Verify every URL in the prompt returns HTTP 200 before the watch goes live. A watch pointed at a domain that does not serve will still run, still write snapshots, and still report, producing confident output about nothing. | curl -s -o /dev/null -w '%{http_code}' https://<competitor-domain>/ |
Known failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Every run reports a fresh baseline and no diff ever appears. | The snapshot directory is a container-local path, so it is wiped on redeploy. | Pin the absolute /data/hermes-homes/<workspace-id>/... path in the prompt and confirm the files persist after a deploy. |
| A watch appears scheduled but nothing fires. | The row is in state cancelled, or a duplicate pending row with the same cron replaced it, or the scheduler container is not running. | List the table and confirm exactly one pending row per watch, then confirm the scheduler service is up. |
| The record cites a domain that does not resolve or serve. | A URL was transcribed from memory rather than verified against the live site. | Curl every source URL when the watch is created and again whenever the record is republished. |
| Historical context is missing from a report. | The Wayback CDX and availability endpoints intermittently return HTTP 503 and 429. | Record the retrieval failure as a caveat. Absence of a snapshot is not evidence that a page did not exist. |
| A real change is captured but never surfaced to anyone. | The weekly digest summarises headline movement and can pass over a large change in a secondary line item. | Give the watch an explicit numeric-drift instruction so any figure that moves by more than half is called out on its own. |
Add a company to the watch
To add a company to the watch, paste this into Slack addressed to Iris, replacing the bracketed values. It creates one recurring watch with a durable baseline and an explicit escalation condition.
@Iris create a weekly competitor watch for [COMPANY] at [https://example.com/]. Monitor these pages every Monday at 13:00 UTC: homepage, pricing, integrations, product, docs index, changelog or blog, and customer stories. Before the first run, confirm every URL returns HTTP 200 and tell me if any does not. Store snapshots at /data/hermes-homes/<workspace-id>/competitor-watch/[slug]/snapshot-YYYY-MM-DD.md on the persistent volume. Never write them to /root or /tmp; those are inside the worker container and are wiped on every redeploy. On the first run, capture a baseline and stop. On later runs, diff against the newest snapshot and report only material changes to pricing, packaging, product surface, integrations, positioning, or proof points. If a published figure moves by more than 50 percent in either direction, call it out on its own line even if nothing else changed. Escalate loudly if [THE ONE CHANGE THAT WOULD ALTER LEGION'S COMPETITIVE ANSWER, for example: they ship owned sending infrastructure, or they launch a managed done-for-you service tier]. Cite a URL and a capture date for every claim. Keep vendor claims and LeadGrow analysis clearly separate. If a page cannot be retrieved, say so rather than inferring what it said. Report to this thread.
Once created, confirm the watch is live by asking Iris to list scheduled tasks and checking there is exactly one pending row for the new company with the cron pattern you expect.
Weekly monitoring loop
Each week, an analyst compares Oxygen’s public product, pricing, integration, and positioning pages with the last dated record. A meaningful difference is captured with its source URL and observation date, then written as vendor evidence; inference remains labelled as LeadGrow assessment.
Compare public signals
Review the same first-party pages and flag changed copy, offers, integrations, pricing, or positioning.
Preserve the evidence
Save the exact claim beside its URL, capture date, publisher, and caveats so readers can retrace it.
Route material changes
Escalate changes that alter commercial positioning, buyer access, pricing, or product scope for Legion review; retain lower-signal changes in the next weekly record.
What this system does not yet do
- Watches monitor a fixed URL list. They do not re-scan sitemap.xml, robots.txt, docs navigation or RSS to discover new pages, so a competitor can publish an entirely new section without the watch noticing.
- There is no change-severity model. Every difference arrives with the same weight, which is why a 96 percent drop in a sending line item can sit in the same paragraph as a copy tweak.
- Reports are prose in Slack. They are not written back into this deck, so the published record does not update itself when a watch fires.