Guides
Conditions
"conditions": { "all": [
{ "field": "availability", "op": "=", "value": "in_stock" },
{ "field": "price", "op": "<=", "value": 120 }
] }Use all or any, with up to 10 conditions. A field that isn't on the page never passes, and a blocked site is never guessed.
Fields
field | Values |
|---|---|
price | Dollars (120), "$119.99", or exact amounts like "$0.0137" |
price_drop | Down by "10%" or "$20", with compare_to |
price_change | Up (positive) or down (negative) by "0.5%" or "-$2", with compare_to |
price_move | Moved either way by "0.5%", with compare_to |
kind | product, listing, slot, ticket, fare, quote, release, post, record, region, document or profile |
availability | in_stock, low_stock, out_of_stock, back_in_stock, open_slot, closed |
quantity | A number, when the site shows one |
version | A number. Needs match: line or category |
release_status | announced, preorder, available, sold_out |
release_date, date, opens_at | "2026-12-15" or a datetime |
seller, or any attribute | Text or a number |
items | The list itself, for added and removed |
item | The whole result, for judge |
in_stock includes low stock. low_stock means only low stock: Nike's low level, or 3 or fewer left. back_in_stock is true on the one run where a variant goes from out of stock to buyable.
Operators
=, !=, <, <=, >, >=, in, not_in (with a list), exists (the field appeared), changed (it differs from the last run), added and removed (with field items: something joined or left the list), judge (an LLM checks a rule you write).
document is a policy, regulation or PDF that has versions, with text, text_hash and length; { "field": "text_hash", "op": "changed" } fires on a new version. profile is a repo, a company or a business listing whose numbers change. Both work with every condition.
compare_to
Compare with an earlier run of the same variant on the same site instead of a fixed value:
{ "field": "price_drop", "op": ">=", "value": "10%", "compare_to": "first_snapshot" }
{ "field": "price", "op": "<", "compare_to": "last_snapshot" }
{ "field": "price", "op": "<=", "value": "$20", "compare_to": "first_snapshot" }The last one means at least $20 under the first price.
compare_to works on any number: price, quantity, or a numeric attribute a site gives, like a rank or a count. With a value it means "by at least": { "field": "rank", "op": "<=", "value": 10, "compare_to": "first_snapshot" } is up 10 places or more.
window
compare_to: "window" compares with the oldest run inside window, for moves over a stretch of time:
{ "field": "price_move", "op": ">=", "value": "0.5%", "compare_to": "window", "window": "10m" }That fires when the price is 0.5% or more away from where it was 10 minutes ago, either way. The window needs checks inside it, so pair it with an every shorter than the window.
Prices are exact. A token at $0.0137 is compared as 0.0137, not rounded to a cent; results carry price.amount next to price.cents.
over
over asks about a set instead of one variant.
{ "field": "availability", "op": "=", "value": "in_stock",
"over": { "key": "sizes", "require": "all", "same": "seller" } }| Key | Values come from |
|---|---|
sizes | attributes.size (a list) |
dates | Every dated slot found |
items | Every product the match found |
require is all, any, or min_n with n. same is site or seller, when the set has to come from one place.
Lists: added and removed
{ "field": "items", "op": "added" }
{ "field": "items", "op": "removed" }added is true for a result that wasn't on the site last run. removed is true for one that was and isn't now; it's reported as it looked when last seen, since the site no longer lists it. Use them for sanctions and entity lists, a company's subprocessors, an exchange's new trading pairs, a newsroom, or a store's new arrivals. Combine them with other conditions: under all, { "field": "country", "op": "=", "value": "IR" } next to added reports only Iranian additions.
The first read is the starting list, so nothing is added on it. Only full reads count: a blocked or unreachable run removes nothing, and a list that reads empty when it held 3 or more items last time is taken as a bad read. A list that drops from 1 or 2 items to none counts as removals. A reader that returns only the first page of a long list reports items that fall off that page as removed, so pick readers that return the whole list. Each change is news once, so it goes out even inside the cooldown. matched and changed events carry added and removed, each a list of results. added and removed compare with the last run by themselves, so leave out compare_to, window and over.
Judged conditions
{ "field": "text", "op": "judge", "value": "a plan's price went up, or a plan was removed" }An LLM reads the result as it was last run and as it is now, and says whether your rule holds. field is what it reads: item for the whole result, or one field such as a page change's text. It's asked only when that field changed since the last run. Unchanged, the condition is false; on a site's first read it's unknown. The same change is never judged twice.
Each watcher gets 20 judged checks a day, and 10 in one run. Past that, or while the server's LLM budget or your account's monthly LLM budget is used up, an unanswered condition is unknown, and unknown never passes. A change that wasn't judged then is kept and judged on a later run, oldest first (up to 20 waiting, for 7 days; a result that changes again is judged on its new change instead). Until then the condition reads unknown. A late verdict counts on the run it's answered, so it fires then if your other conditions also hold on that run. Don't pair the judge with changed conditions: it only asks when the text changed anyway, and a late verdict can't fire on a run where nothing changed. Put cheap conditions that don't depend on the run next to it under all (kind = document): only results that pass them are judged. Up to 3 judged conditions per watcher, each rule up to 300 characters.
text on a page or document carries its first 2,000 characters, but for a page change or PDF the judge reads change_excerpt instead: the lines that changed since the last run, with one unchanged line either side, up to 3,000 characters. So a change on page 30 of a long document is judged. On other readers whose text is cut, a change past the first 2,000 characters still reads unknown; judge a narrower region (selector) there.
Events carry judged: [{ site, url, rule, reason }]. Previews don't call the LLM, so judged conditions read unknown there. A server without an LLM refuses judged conditions with judge_unavailable.
Page changes
A page-change watch gives one result of kind region (a part of a page), document (a PDF), or one record per item with list. These attributes:
| Attribute | |
|---|---|
text_hash | A hash of the region's text after noise is removed. { "field": "text_hash", "op": "changed" } fires when the text changes |
contains | true when every phrase in target.attributes.contains is there, as whole words |
contains_any | true when at least one of them is |
contains_<phrase> | One per phrase, with the words joined by _: contains_sold_out for "sold out" |
length | Characters in the region. { "field": "length", "op": "<", "value": 500, "compare_to": "last_snapshot" } catches a page that got much shorter |
found | false when selector matched nothing or the region has no text (after it matched once; see elements) |
elements | How many elements selector matched. A selector that matches nothing on the first read fails with selector_not_found and names close selectors that do match. After it has matched once, 0 is reported, not an error, so you can watch for a button to go away. To wait for one to appear, watch a larger region with contains |
text | The first 2,000 characters, for the webhook |
change_excerpt | The lines that changed since the last run, with one line of context either side, up to 3,000 characters. What a judged condition reads |
final_url | Where the page redirected to, when it did |
previous_hash | The hash before the latest change. Empty on the first read |
version_seen_at | When the current text was first seen |
last_change | What changed last time, in a short line like changed "Rev. October 2018" to "Rev. March 2024"; added "...". At most 1,000 characters |
last_change_added, last_change_removed, last_change_changed | Paragraphs added, removed and changed |
last_change_summary | A one- or two-sentence summary of that change, with summarize: true |
pages, pdf_title, pdf_created, pdf_modified | PDFs |
position, href, date | List items, with list |
page_title, meta_description, canonical, meta_robots, noindex, h1, h1_count, lang, script_hosts, script_count, seo_hash | With seo: true. { "field": "noindex", "op": "=", "value": true } catches a page dropped from search; { "field": "seo_hash", "op": "changed" } catches a new tag or script host |
target.attributes.contains is the phrase to look for and the result's contains is whether it was found, so { "field": "contains", "op": "=", "value": true } is the condition to use.
Several sites
where.aggregate decides what one notification is about.
aggregate | Sends |
|---|---|
each | One result per site. The default |
best | The lowest price, or the earliest date, across sites |
first | The first site to match, then the watcher stops |
dedupe | One result per product, with every site's offer |
Kind
The watcher's kind comes from conditions that watch for a change: price, availability, release, or mixed when there are two of those. A fixed cap like price <= 180 doesn't make a release watcher mixed.