Snag Watcher Docs

Guides

Conditions

JSON
"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

fieldValues
priceDollars (120), "$119.99", or exact amounts like "$0.0137"
price_dropDown by "10%" or "$20", with compare_to
price_changeUp (positive) or down (negative) by "0.5%" or "-$2", with compare_to
price_moveMoved either way by "0.5%", with compare_to
kindproduct, listing, slot, ticket, fare, quote, release, post, record, region, document or profile
availabilityin_stock, low_stock, out_of_stock, back_in_stock, open_slot, closed
quantityA number, when the site shows one
versionA number. Needs match: line or category
release_statusannounced, preorder, available, sold_out
release_date, date, opens_at"2026-12-15" or a datetime
seller, or any attributeText or a number
itemsThe list itself, for added and removed
itemThe 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:

JSON
{ "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:

JSON
{ "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.

JSON
{ "field": "availability", "op": "=", "value": "in_stock",
  "over": { "key": "sizes", "require": "all", "same": "seller" } }
KeyValues come from
sizesattributes.size (a list)
datesEvery dated slot found
itemsEvery 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

JSON
{ "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

JSON
{ "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_hashA hash of the region's text after noise is removed. { "field": "text_hash", "op": "changed" } fires when the text changes
containstrue when every phrase in target.attributes.contains is there, as whole words
contains_anytrue when at least one of them is
contains_<phrase>One per phrase, with the words joined by _: contains_sold_out for "sold out"
lengthCharacters in the region. { "field": "length", "op": "<", "value": 500, "compare_to": "last_snapshot" } catches a page that got much shorter
foundfalse when selector matched nothing or the region has no text (after it matched once; see elements)
elementsHow 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
textThe first 2,000 characters, for the webhook
change_excerptThe 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_urlWhere the page redirected to, when it did
previous_hashThe hash before the latest change. Empty on the first read
version_seen_atWhen the current text was first seen
last_changeWhat 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_changedParagraphs added, removed and changed
last_change_summaryA one- or two-sentence summary of that change, with summarize: true
pages, pdf_title, pdf_created, pdf_modifiedPDFs
position, href, dateList items, with list
page_title, meta_description, canonical, meta_robots, noindex, h1, h1_count, lang, script_hosts, script_count, seo_hashWith 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.

aggregateSends
eachOne result per site. The default
bestThe lowest price, or the earliest date, across sites
firstThe first site to match, then the watcher stops
dedupeOne 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.