{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://pushbuild.com/groove/groove.schema.json",
  "title": "Groove groove",
  "description": "A repeatable browser automation that Groove can import and run in the user's own browser.",
  "type": "object",
  "required": [
    "format",
    "version",
    "name",
    "steps"
  ],
  "additionalProperties": false,
  "properties": {
    "format": {
      "const": "groove"
    },
    "version": {
      "const": 1
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    },
    "description": {
      "type": "string",
      "description": "What the groove is for, in the person's words where they gave any."
    },
    "startUrl": {
      "type": "string",
      "description": "Where tab 1 starts. Optional if the first step is goto."
    },
    "source": {
      "type": "string",
      "description": "What the groove was made from, e.g. the recording label or 'page reading'."
    },
    "askInputs": {
      "type": "boolean",
      "default": true,
      "description": "Show the person a form with the inputs whenever the groove starts (including when an agent starts it). false: run straight away when every input has a value."
    },
    "variables": {
      "type": "array",
      "description": "Inputs (asked for each run) and settings (fixed values saved with the groove). Steps refer to both as {{name}}.",
      "items": {
        "type": "object",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*$"
          },
          "label": {
            "type": "string"
          },
          "kind": {
            "enum": [
              "input",
              "setting"
            ],
            "default": "input",
            "description": "input: asked for each run, prefilled with the last value or default. setting: saved with the groove as value, edited in Groove's library."
          },
          "default": {
            "type": "string",
            "description": "input: the value offered the first time."
          },
          "value": {
            "type": "string",
            "description": "setting: its value. Shared with the groove unless secret."
          },
          "secret": {
            "type": "boolean",
            "description": "Masked when asked for and never shared with the groove."
          }
        }
      }
    },
    "steps": {
      "type": "array",
      "minItems": 1,
      "items": {
        "$ref": "#/$defs/step"
      }
    },
    "schedule": {
      "type": "object",
      "additionalProperties": false,
      "description": "Run by itself (approved grooves only, in a background tab, Chrome open). The person's inputs come from last values or defaults.",
      "properties": {
        "every": {
          "enum": [
            "off",
            "minutes",
            "hour",
            "day",
            "weekdays"
          ]
        },
        "n": {
          "type": "integer",
          "minimum": 5,
          "description": "minutes: how many"
        },
        "at": {
          "type": "string",
          "pattern": "^\\d{2}:\\d{2}$",
          "description": "day / weekdays: local time HH:MM"
        }
      }
    },
    "lineage": {
      "type": "object",
      "description": "Set by Groove when a groove is exported. Leave it exactly as it is when you change a groove: Groove uses it to show the person what changed, beside the original.",
      "properties": {
        "from": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "fingerprint": {
          "type": "string"
        },
        "exported": {
          "type": "string"
        }
      }
    }
  },
  "$defs": {
    "target": {
      "type": "object",
      "description": "How to find the element. Give as many as you can; Groove tries them from most to least robust: testid, role+name, text, css, xpath.",
      "additionalProperties": false,
      "properties": {
        "role": {
          "type": "string",
          "examples": [
            "button",
            "link",
            "textbox",
            "combobox",
            "checkbox",
            "tab",
            "menuitem"
          ]
        },
        "name": {
          "type": "string",
          "description": "Accessible name: the label, aria-label or visible text."
        },
        "text": {
          "type": "string",
          "description": "Visible text contained in the element."
        },
        "css": {
          "type": "string"
        },
        "xpath": {
          "type": "string"
        },
        "testid": {
          "type": "string"
        },
        "shadowHost": {
          "type": "string",
          "description": "CSS of the shadow host when the element is inside shadow DOM."
        },
        "nth": {
          "type": "integer",
          "minimum": 0,
          "description": "Which match, when several elements match (0 = first)."
        }
      }
    },
    "step": {
      "type": "object",
      "required": [
        "action"
      ],
      "additionalProperties": false,
      "properties": {
        "action": {
          "enum": [
            "goto",
            "click",
            "type",
            "select",
            "check",
            "uncheck",
            "press",
            "extract",
            "waitFor",
            "assert",
            "wait",
            "newTab",
            "switchTab",
            "closeTab",
            "back",
            "reload",
            "scroll",
            "note",
            "extractAll",
            "notify",
            "webhook",
            "download",
            "email"
          ]
        },
        "tab": {
          "type": "integer",
          "minimum": 1,
          "default": 1,
          "description": "Which tab, numbered in order of opening."
        },
        "frame": {
          "type": "string",
          "default": "top",
          "description": "'top', or the CSS selector of the iframe. Nested frames: 'outer-css >>> inner-css'."
        },
        "target": {
          "$ref": "#/$defs/target"
        },
        "url": {
          "type": "string",
          "description": "goto / newTab. May contain {{variables}}. For webhook: the address results are sent to."
        },
        "value": {
          "type": "string",
          "description": "type / select. May contain {{variables}}."
        },
        "key": {
          "type": "string",
          "description": "extractAll: the field that identifies a row (de-duplication, onlyNew). Default: the link field."
        },
        "text": {
          "type": "string",
          "description": "assert / waitFor: text that must be visible (in target if given, else on the page). For email: a message above the results. For download (text format): the file's contents."
        },
        "saveAs": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]*$",
          "description": "extract: variable that receives the target's text. extractAll: variable that receives the rows. Every saveAs is an output of the run."
        },
        "ms": {
          "type": "integer",
          "minimum": 0,
          "maximum": 600000,
          "description": "wait: fixed pause. Prefer waitFor."
        },
        "timeoutMs": {
          "type": "integer",
          "minimum": 0,
          "maximum": 600000,
          "default": 15000
        },
        "optional": {
          "type": "boolean",
          "description": "Carry on if this step cannot be done (e.g. a cookie banner that is not always there)."
        },
        "confirm": {
          "type": "boolean",
          "description": "Pause and ask the user before this step. Required for anything that submits, pays, sends or deletes."
        },
        "note": {
          "type": "string",
          "description": "Plain-language description shown to the user."
        },
        "comment": {
          "type": "string",
          "description": "The person's own note on this step (why, when, exceptions). Copy it from the recording's steps.jsonl comment."
        },
        "item": {
          "type": "string",
          "description": "extractAll: CSS selector matching every item of the list (each becomes a row)."
        },
        "fields": {
          "type": "array",
          "description": "extractAll: what to read from each item.",
          "items": {
            "type": "object",
            "required": [
              "name"
            ],
            "additionalProperties": false,
            "properties": {
              "name": {
                "type": "string",
                "pattern": "^[a-z][a-z0-9_]*$"
              },
              "sel": {
                "type": "string",
                "description": "CSS selector inside the item; omit for the item itself."
              },
              "attr": {
                "enum": [
                  "text",
                  "href",
                  "src"
                ],
                "default": "text"
              }
            }
          }
        },
        "next": {
          "$ref": "#/$defs/target",
          "description": "extractAll: the 'next page' control, clicked between pages."
        },
        "maxPages": {
          "type": "integer",
          "minimum": 1,
          "maximum": 50,
          "default": 1
        },
        "limit": {
          "type": "integer",
          "minimum": 1,
          "maximum": 2000,
          "default": 500,
          "description": "extractAll: most rows per page."
        },
        "onlyNew": {
          "type": "boolean",
          "description": "extractAll: keep only rows not seen on an earlier finished run (a news watch)."
        },
        "message": {
          "type": "string",
          "description": "notify: the notification text. May contain {{variables}}; a list reads as its count."
        },
        "if": {
          "type": "object",
          "additionalProperties": false,
          "description": "notify: only when this holds.",
          "properties": {
            "text": {
              "type": "string",
              "description": "the page shows this text"
            },
            "target": {
              "$ref": "#/$defs/target"
            },
            "var": {
              "type": "string",
              "description": "this variable is non-empty (a list with rows)"
            },
            "not": {
              "type": "boolean",
              "description": "invert"
            }
          }
        },
        "send": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "webhook / email: the values to send (variable or saveAs names). Default: the run's inputs (not secret ones) and every saveAs value so far."
        },
        "method": {
          "enum": [
            "POST",
            "PUT"
          ],
          "description": "webhook: default POST. The body is JSON: {\"groove\": name, \"at\": ISO time, \"data\": {name: value}}. No cookies are sent."
        },
        "format": {
          "enum": [
            "csv",
            "json",
            "text"
          ],
          "description": "download: default csv. A list saves one row per item."
        },
        "from": {
          "type": "string",
          "description": "download: the variable or saveAs value to save. Default: the run's inputs (not secret ones) and every saveAs value so far."
        },
        "filename": {
          "type": "string",
          "description": "download: file name, saved in Downloads/Groove/. Can use {{variables}}, {{date}} (2026-10-31) and {{time}} (2215)."
        },
        "to": {
          "type": "string",
          "description": "email: the address to send to (or a {{variable}}). The person must confirm it once from their browser; Groove sends from groove@pushbuild.com, at most 100 emails a day."
        },
        "subject": {
          "type": "string",
          "description": "email: subject line. Can use {{variables}}."
        }
      }
    }
  }
}
