Schedule icon
CLI icon
If icon
Switch icon
Log icon
SlackIncomingWebhook icon

Scrapy Price Drop Watch and Re-Verification Gate

Scrape a product page's price with Scrapy, alert on a threshold breach, and re-verify with a second scrape before trusting it.

Categories
BusinessData

A price-watch script that only scrapes tells you a number; it does not decide whether that number is real, whether anyone should be told, or when to check again. This blueprint keeps the spider to extraction only - title and price off one listing page - and does every decision in Kestra: finding the lowest price, comparing it to a threshold, optionally re-scraping once more to rule out a glitch, and deciding whether the Slack alert actually fires.

How it works

  1. daily_price_watch (io.kestra.plugin.core.trigger.Schedule) runs daily, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.
  2. scrape_book_prices (io.kestra.plugin.scrapy.CLI) runs a minimal inline spider (scrapy runspider ... -o books.json) that visits only target_url (CLOSESPIDER_PAGECOUNT: 1, no pagination), extracting every article.product_pod entry's title and price with the same CSS selectors shown in the official plugin documentation. The spider strips the currency symbol and parses a float - that is the only logic it contains.
  3. evaluate_price_breach (io.kestra.plugin.core.flow.If) reads books.json, finds the lowest non-null price with jq, and compares it to price_threshold.
    • then (breach found): route_reverify (io.kestra.plugin.core.flow.Switch on auto_remediate) either re-runs the identical spider ("true") and logs whether the second scrape confirms or fails to reproduce the breach, or skips re-verification ("false"). Independently, route_alert (io.kestra.plugin.core.flow.If on dry_run) either fires alert_price_breach to Slack ("false") or only logs what would have been sent ("true").
    • else (no breach): log_clean_run records a clean check.
  4. log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.
  5. The errors block alerts Slack separately if the flow itself fails - an unreachable page or a markup change must not read as "no breach".

What you get

  • A price-threshold watch backed by an actual scrape, not a guess from a cached catalog export.
  • A confirmatory re-scrape you can turn on (auto_remediate) to filter out one-off scraping glitches before trusting a breach.
  • An independent dry_run gate so you can tune price_threshold without spamming Slack while you calibrate it.
  • A final status log every run, breach or not, so the execution history doubles as a price history.

Who it's for

  • Shoppers and resellers who want a specific listing watched without a paid price-tracking SaaS.
  • Small e-commerce or ops teams who want a cheap, inspectable watch on a competitor's or supplier's published price.
  • Anyone evaluating the Scrapy plugin who wants a worked example where Kestra, not the spider, makes every decision.

Why orchestrate this with Kestra

A bare scrapy runspider prints a file and exits - it has no memory of yesterday's price, no threshold logic, no way to decide "alert now" versus "re-check first", and no notification channel. Kestra supplies the schedule, the threshold comparison as a first-class branch, an independent re-verification step to cut false positives, and a notification gate that can be exercised safely in dry-run mode before it is trusted to page anyone.

Prerequisites

  • Docker available to the Kestra Worker (io.kestra.plugin.scrapy.CLI defaults to the Docker task runner with the ghcr.io/kestra-io/scrapy image).
  • Network access from the Kestra Worker to target_url. The default (https://books.toscrape.com) is a public scraping sandbox requiring no account.
  • A Slack incoming webhook for alerts.

Local testing against your own static page, if you do not want to rely on a public site: docker run -d --name static-site -p 8088:80 -v $(pwd)/site:/usr/share/nginx/html nginx:alpine docker network connect YOUR_KESTRA_NETWORK static-site (only needed if the Kestra Worker runs in a separate Docker network than this container).

Secrets

  • SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_price_breach and the errors block.
  • In Kestra OSS (no Enterprise secrets backend), secrets are supplied as environment variables prefixed SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_SLACK_WEBHOOK_URL=$(echo -n 'https://hooks.slack.com/...' | base64). This keeps the webhook out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.

Inputs

  • target_url (STRING, default https://books.toscrape.com): listing page to scrape, one page only.
  • price_threshold (FLOAT, default 20.0): alert when the lowest scraped price is at or below this value.
  • auto_remediate (SELECT: "false", "true"; default "false"): if "true", re-scrape once more to confirm a breach before trusting it.
  • dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false" for the Slack alert to actually send.

Outputs

  • outputs.scrape_book_prices.outputFiles['books.json']: every scraped {title, price} pair from the first scrape.
  • outputs.reverify_book_prices.outputFiles['books_reverify.json']: present only when auto_remediate was "true" and a breach was found - the confirmatory second scrape.

Quick start

  1. Add the SLACK_WEBHOOK_URL secret.
  2. Run the flow manually with the defaults and confirm log_clean_run or the breach branch fires correctly against books.toscrape.com's real catalog prices.
  3. Set price_threshold below the site's cheapest book price to force a breach, and confirm log_dry_run_alert logs it without sending to Slack (default dry_run: "true").
  4. Run once more with dry_run: "false" and confirm alert_price_breach actually posts.
  5. Run with auto_remediate: "true" and confirm log_reverify_result reports whether the second scrape confirmed the breach.
  6. Enable daily_price_watch once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.
  7. Point target_url at your own real listing page once you are done testing against the sandbox.

How to extend

  • Loop over several listing pages with io.kestra.plugin.core.flow.Loop (ForEach is deprecated) to watch more than one page in one run.
  • Store each run's lowest price in the Kestra KV store and alert only on a new breach, the same way a change-triggered alert avoids repeating itself every run.
  • Follow pagination (CLOSESPIDER_PAGECOUNT raised, or a next link follow added to parse) if the real catalog spans more than one page - this stays a minimal spider change, not new Kestra logic.
  • Track a specific item by title instead of the catalog minimum, by filtering the jq expression on .title before taking min on .price.

Pitfalls

  • The spider is deliberately single-page. CLOSESPIDER_PAGECOUNT: 1 and no pagination-follow logic mean only target_url itself is scraped; a catalog spanning multiple pages will under-report unless you extend the spider per "How to extend".
  • Price text must be stripped of its currency symbol before jq sees it. p.price_color::text on books.toscrape.com returns a symbol-prefixed string (for example a value like "£51.77"); jq's tonumber cannot coerce that directly, so the spider parses the float itself, keeping jq's job to pure numeric comparison.
  • auto_remediate and dry_run control two different things, not one gate. auto_remediate only decides whether a confirmatory re-scrape runs; dry_run independently decides whether the Slack alert fires. Setting auto_remediate: "true" with dry_run: "true" re-verifies the breach and still sends nothing to Slack.
  • A null price (missing or unparsable price_color) is excluded from the minimum, not treated as zero. The jq filter explicitly drops nulls before taking min; without that, one unparsable entry would make every run report a breach.
  • Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.