Skip to main content
Recipe prompt
Use stable application IDs to collect changing web data, then compare only records whose commercial terms match. Store accepted values separately from failed attempts and unresolved observations.

Discover and checkpoint sources

Use Map URLs with urlRegex to select a section, or a scoped crawl to follow its links. Review the resulting URL list before ingestion and persist it so a retry does not start discovery over. For each source, keep an application ID, original URL, current URL, last successful observation, and next retry time. Do not use a mutable title as an identity key.

Collect and retain observations

Parsed fields are deterministic. A rule that matches nothing returns null for an item and [] for a list, so a missing value stays unknown instead of being guessed. Values are strings; convert numbers in your application and keep the original text. Rules run on the rendered HTML after sharedParams.mainContentOnly, includeSelectors, and excludeSelectors, so leave those filters off unless every selector targets content they keep. The worker below sends this body for each source, requesting the parsed fields and the page Markdown from one visit.
Scrape request body
See the Scrape guide for this request in every SDK. Run the Python application below with CONTEXT_DEV_API_KEY set on the server.

Store records separately from attempts

Keep three kinds of state: the source queue, the latest accepted record, and historical observations. The SQLite worker commits each source independently, so a failure on one page cannot delete another page’s data.
catalog.py
Run the worker
The first command resumes pending or eligible retryable items and skips completed ones. The second starts a new observation cycle for the listed sources. Because the application IDs are stable, a changed price updates the current record instead of creating a duplicate course. Each accepted refresh also records an observation for history. The worker retries 408, 429, and 5xx responses after the Retry-After delay, and stops the run on 401, 403, and 429. A 400 WEBSITE_BLOCKED becomes an error; parsed.success: false or an unexpected parsed shape goes to review without replacing the accepted record. A record with an unexpected shape, a price that cannot be parsed, or a final URL on another host goes to review and leaves the last accepted record in place. This is a single-worker example. Use queue leases or transactional job claiming before running multiple workers. Schedule retries after next_attempt_at; do not repeatedly restart after an authentication or account-limit error. Review exhausted retries and invalid records explicitly.

Compare facts on compatible terms

For a research question spanning several pages, send this body to Answers, then keep its source URLs and the time of the observation:
Answers request body
Keep missing prices as null. Do not compare a monthly commitment with an annual promotion, or prices in different currencies, as though they were interchangeable:
comparison.ts
Name the intended plan or product variant in the research task. Inspect the source for currency, billing period, tax treatment, minimum seats, usage tiers, and promotional conditions. A source URL identifies a page, not a field-level citation; retain the supporting excerpt for claims that need review.

Reconcile changes

Refresh observations under their existing application IDs. Keep the last accepted values after errors or ambiguous extraction, and distinguish unknown values from confirmed changes. Promote a new observation only after schema and source checks pass. Re-run discovery periodically to find added and removed URLs. A page missing from an incomplete index or crawl is not proof that the item disappeared. For notifications, feed accepted changes into a digest.