← Back to cookbook

Price Monitor

Monitors competitor pricing pages and alerts on changes.

scrapeaistreamswitchemail

Source

/**
 * Monitors competitor pricing pages and alerts on changes.
 */

schedule check_prices {
  label: "Check Prices"
  cron: "0 6 * * *"
  timezone: "America/New_York"
}

workflow monitor_prices {
  label: "Monitor Competitor Prices"

  root {
    type: code
    label: "Start"
    code: @ts {
      return { checked_at: new Date().toISOString() }
    }
    outputSchema: @json {
      {
        "type": "object",
        "properties": {
          "checked_at": { "type": "string" }
        }
      }
    }
  }

  node scrape_competitor {
    type: scrape
    label: "Scrape pricing page"
    url: @ts { return "https://competitor.example.com/pricing" }
    onlyMainContent: true
    formats: ["markdown"]
  }

  node extract_prices {
    type: ai
    label: "Extract pricing data"
    kind: object
    model: "google/gemini-2.5-flash"
    prompt: @ts {
      return "Extract all pricing tiers and their prices from this pricing page content. Return structured data.\n\n" + JSON.stringify(context.nodes.scrape_competitor.output)
    }
    schema: @json {
      {
        "type": "object",
        "required": ["tiers"],
        "properties": {
          "tiers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": { "type": "string" },
                "monthly_price": { "type": "number" },
                "annual_price": { "type": "number" },
                "features": { "type": "array", "items": { "type": "string" } }
              }
            }
          }
        }
      }
    }
  }

  node check_history {
    type: stream
    label: "Get previous prices"
    stream: price_history
    version: v1
    filter: @ts {
      return {}
    }
  }

  node store_result {
    type: code
    label: "Store prices"
    code: @ts {
      return {
        checked_at: context.nodes.root.output.checked_at,
        tiers: context.nodes.extract_prices.output.tiers,
        tier_count: context.nodes.extract_prices.output.tiers.length
      }
    }
  }

  node compare {
    type: code
    label: "Compare with previous prices"
    code: @ts {
      const rows = context.nodes.check_history.output || []
      const previous = rows.length > 0 && rows[0].tiers ? rows[0].tiers : null
      const current = context.nodes.extract_prices.output.tiers
      const key = function(tiers) {
        return JSON.stringify((tiers || []).map(function(t) {
          return { name: t.name, monthly_price: t.monthly_price, annual_price: t.annual_price }
        }))
      }
      const changed = previous !== null && key(previous) !== key(current)
      return {
        changed: changed,
        current_count: current.length,
        previous_count: previous ? previous.length : 0,
        current_tiers: current
      }
    }
  }

  node route_change {
    type: switch
    label: "Route on change"
    cases: ["changed", "unchanged"]
    router: @ts {
      if (context.nodes.compare.output.changed) return "changed"
      return "unchanged"
    }
  }

  node alert_change {
    type: email
    label: "Alert on price change"
    from: @ts { return "[email protected]" }
    to: @ts { return "[email protected]" }
    subject: @ts { return "Competitor pricing changed" }
    text: @ts {
      const cmp = context.nodes.compare.output
      return "Competitor pricing changed.\n\nTiers now: " + cmp.current_count + " (was " + cmp.previous_count + ")\n\nCurrent tiers:\n" + JSON.stringify(cmp.current_tiers, null, 2)
    }
  }

  node no_change {
    type: code
    label: "No change"
    code: @ts {
      return { changed: false }
    }
  }

  flow {
    root -> scrape_competitor
    scrape_competitor -> extract_prices
    root -> check_history
    extract_prices -> store_result
    extract_prices -> compare
    check_history -> compare
    compare -> route_change
    route_change -["changed"]-> alert_change
    route_change -["unchanged"]-> no_change
  }
}

stream price_history {
  label: "Price history"
  workflow: monitor_prices
  version: v1

  versions: {
    v1 {
      schema: @json {
        {
          "type": "object",
          "required": ["checked_at", "tier_count"],
          "properties": {
            "checked_at": { "type": "string" },
            "tiers": { "type": "array" },
            "tier_count": { "type": "number" }
          }
        }
      }

      condition: @ts { return context.output.store_result != null }

      prepare: @ts { return context.output.store_result }
    }
  }
}

trigger on_price_check {
  schedule:check_prices -> monitor_prices
  enabled: true
}

Flow

Trigger to workflow

Workflow nodes