New to Kestra?
Use blueprints to kickstart your first workflows.
Scrape a product page's price with Scrapy, alert on a threshold breach, and re-verify with a second scrape before trusting it.
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.
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.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.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.log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.errors block alerts Slack separately if the flow itself fails - an unreachable page or a markup change must not read as "no breach".auto_remediate) to filter out one-off scraping glitches before trusting a breach.dry_run gate so you can tune price_threshold without spamming Slack while you calibrate it.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.
io.kestra.plugin.scrapy.CLI defaults to the Docker task runner with the ghcr.io/kestra-io/scrapy image).target_url. The default (https://books.toscrape.com) is a public scraping sandbox requiring no account.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).
SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_price_breach and the errors block.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.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.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.SLACK_WEBHOOK_URL secret.log_clean_run or the breach branch fires correctly against books.toscrape.com's real catalog prices.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").dry_run: "false" and confirm alert_price_breach actually posts.auto_remediate: "true" and confirm log_reverify_result reports whether the second scrape confirmed the breach.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.target_url at your own real listing page once you are done testing against the sandbox.io.kestra.plugin.core.flow.Loop (ForEach is deprecated) to watch more than one page in one run.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.jq expression on .title before taking min on .price.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".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.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.