Swirls

SWIRLS_

  • How it works
  • Pricing

56 matches

OverviewDeliver client operations10Win and grow accounts8Resolve customer issues6Create and distribute content9Run people and operations5Review money and agreements3Monitor systems and data9Compose dependable work6

56 matches

OverviewDeliver client operations10Win and grow accounts8Resolve customer issues6Create and distribute content9Run people and operations5Review money and agreements3Monitor systems and data9Compose dependable work6
← Sales & MarketingScheduleView on GitHub

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

check_pricesschedule
monitor_prices
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

Workflow nodes

CODE
Code Node
Start
SCRAPE
Scrape Node
Scrape pricing page
AI
AI Node
Extract pricing data
STREAM
Stream Node
Get previous prices
CODE
Code Node
Store prices
CODE
Code Node
Compare with previous prices
SWITCH
Switch Node
Route on change
EMAIL
Email Node
Alert on price change
CODE
Code Node
No change
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.