{
  "title": "Monitor recent Apple App Store reviews",
  "schemaVersion": 1,
  "type": "object",
  "description": "Collect a bounded sample from Apple's public customer-review feeds. Filter ratings and literal keywords, then optionally compare saved review observations.",
  "properties": {
    "apps": {
      "title": "Apps and storefronts",
      "type": "array",
      "editor": "json",
      "prefill": [
        {
          "appId": "570060128",
          "country": "us"
        }
      ],
      "items": {
        "type": "object",
        "properties": {
          "appId": {
            "title": "App Store app ID",
            "type": "string",
            "editor": "textfield",
            "description": "Numeric ID from the app's Apple App Store URL, for example 570060128.",
            "pattern": "^[0-9]+$"
          },
          "country": {
            "title": "Storefront country code",
            "type": "string",
            "editor": "textfield",
            "description": "Two-letter storefront code, for example us or gb. This does not identify a reviewer's location.",
            "pattern": "^[a-zA-Z]{2}$"
          }
        },
        "required": [
          "appId",
          "country"
        ],
        "additionalProperties": false
      },
      "description": "App Store numeric IDs and two-letter country codes. Different storefronts have different reviews. If omitted or empty, the code uses Duolingo US (570060128)."
    },
    "maxResults": {
      "title": "Maximum saved review observations",
      "type": "integer",
      "minimum": 1,
      "maximum": 1000,
      "default": 5,
      "description": "Run-wide cap on real saved rows after filters. Limits are ceilings, not guaranteed result counts."
    },
    "maxPagesPerApp": {
      "title": "Maximum feed pages per app",
      "type": "integer",
      "minimum": 1,
      "maximum": 10,
      "default": 1,
      "description": "Read at most this many recent-feed pages for each app and storefront. Public feeds are a limited recent window, not a complete archive."
    },
    "maxRating": {
      "title": "Maximum review rating",
      "type": "integer",
      "minimum": 1,
      "maximum": 5,
      "default": 5,
      "description": "Keep reviews with a rating at or below this value. Use 2 to focus on one- and two-star feedback."
    },
    "keywords": {
      "title": "Keywords in review title or text",
      "type": "array",
      "editor": "stringList",
      "items": {
        "type": "string"
      },
      "default": [],
      "description": "Optional literal, case-insensitive terms. A review must match any one term in its title or body. No regular expressions or semantic sentiment analysis."
    },
    "comparePrevious": {
      "title": "Compare with saved observations",
      "type": "boolean",
      "default": false,
      "description": "Compare the same app, storefront and review ID with saved history. First-seen means new to this snapshot, not necessarily newly published."
    },
    "onlyChanges": {
      "title": "Save only first-seen or changed reviews",
      "type": "boolean",
      "default": false,
      "description": "Implies comparison. Later unchanged runs may save zero rows. Reviews missing from the feed are not reported as deleted."
    },
    "snapshotName": {
      "title": "Snapshot name",
      "type": "string",
      "editor": "textfield",
      "default": "default",
      "minLength": 1,
      "maxLength": 80,
      "pattern": "^[A-Za-z0-9_-]{1,80}$",
      "description": "Letters, digits, underscores and hyphens only. Reuse this name for scheduled comparisons; use one sequential writer per snapshot."
    },
    "maxRunSeconds": {
      "title": "Run time budget in seconds",
      "type": "integer",
      "minimum": 30,
      "maximum": 900,
      "default": 240,
      "description": "Maximum work budget. Source delays, platform timeout and output limits can end the run earlier."
    }
  }
}
