{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://composal.ai/schema/verify/v2/scenario.json",
  "title": "Scenario file v2 with named actors",
  "description": "One Composal Verify journey with explicit actor ownership. Actor personas belong to this project. Run version 2 journeys with adaptive execution.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "$schema",
    "name",
    "steps",
    "outcomes",
    "actors",
    "primary_actor"
  ],
  "dependentRequired": {
    "id": [
      "version"
    ],
    "version": [
      "id"
    ]
  },
  "properties": {
    "$schema": {
      "description": "This schema's URL, which identifies the file as a Scenario file.",
      "const": "https://composal.ai/schema/verify/v2/scenario.json"
    },
    "id": {
      "description": "The stored Scenario this file edits. Omit on a new file; upload writes it back.",
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "key": {
      "description": "Set by upload on a new file so a retried upload updates the Scenario it created instead of creating another. Lowercase letters, digits, - and _. Do not edit.",
      "type": "string",
      "pattern": "^[a-z0-9_-]{1,100}$"
    },
    "version": {
      "description": "The stored Scenario's version when this file was downloaded or uploaded. Upload refuses the file if the Scenario changed since.",
      "type": "integer",
      "minimum": 0
    },
    "name": {
      "description": "\"A <role> can <do the thing>\", or \"can't\" for a permission check, such as \"An admin can invite a teammate by email\". About 10 words, under 70 characters, one success criterion. Put the area in labels; leave out \"successfully\", reloads, tabs and timing.",
      "type": "string",
      "minLength": 1,
      "maxLength": 160,
      "pattern": "\\S"
    },
    "labels": {
      "description": "Short tags that group Scenarios and choose which to run together, such as \"checkout\" or \"smoke\". Omit to keep the current labels; [] removes them.",
      "type": "array",
      "maxItems": 20,
      "uniqueItems": true,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 40,
        "pattern": "\\S"
      }
    },
    "fixtures": {
      "description": "Files an upload step can choose, such as an invoice to import. Each path is relative to this file; upload sends the bytes. Omit to keep the current fixtures; [] removes them.",
      "type": "array",
      "maxItems": 100,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "path"
        ],
        "properties": {
          "path": {
            "description": "The file, relative to this Scenario file, such as fixtures/invoice.pdf.",
            "type": "string",
            "minLength": 1,
            "maxLength": 1024
          },
          "name": {
            "description": "The file name the page receives. Defaults to the path's last part.",
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "pattern": "^(?!\\.\\.?$)[^/\\\\\\u0000-\\u001f\\u007f-\\u009f]+$"
          },
          "content_type": {
            "description": "The MIME type the page receives, such as application/pdf. Defaults to one chosen from the name.",
            "type": "string",
            "pattern": "^[A-Za-z0-9_.+-]{1,127}/[A-Za-z0-9_.+-]{1,127}$"
          }
        }
      }
    },
    "allowed_actions": {
      "description": "read_only (default) only looks; writes may create, change, or delete data.",
      "enum": [
        "read_only",
        "writes"
      ]
    },
    "starting_conditions": {
      "description": "What must already be true before the first step, in one sentence.",
      "type": [
        "string",
        "null"
      ],
      "maxLength": 4000
    },
    "steps": {
      "type": "array",
      "maxItems": 1000,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "actor",
          "instruction"
        ],
        "properties": {
          "actor": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]{0,39}$"
          },
          "instruction": {
            "description": "One imperative user action of at most 12 words, such as \"Switch to the original tab\". The runner applies evidence, timing and retry rules; leave them out.",
            "type": "string",
            "minLength": 1,
            "maxLength": 4000,
            "pattern": "\\S"
          },
          "authentication": {
            "description": "This authored step signs in or creates its assigned account; actual identity verification is required before its checkpoint passes.",
            "type": "boolean"
          }
        }
      }
    },
    "cleanup": {
      "type": "array",
      "maxItems": 100,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "actor",
          "instruction"
        ],
        "properties": {
          "actor": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]{0,39}$"
          },
          "instruction": {
            "description": "One imperative user action of at most 12 words, such as \"Switch to the original tab\". The runner applies evidence, timing and retry rules; leave them out.",
            "type": "string",
            "minLength": 1,
            "maxLength": 4000,
            "pattern": "\\S"
          },
          "authentication": {
            "description": "This authored step signs in or creates its assigned account; actual identity verification is required before its checkpoint passes.",
            "type": "boolean"
          }
        }
      },
      "description": "Optional ordered cleanup instructions, attempted after checks pass or fail. Omit on new files for no application cleanup; omit on updates to preserve saved instructions; [] clears them. Cleanup results are separate from Scenario checks. Only act within the authorized scope."
    },
    "outcomes": {
      "type": "array",
      "maxItems": 100,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "actors",
          "instruction"
        ],
        "properties": {
          "actors": {
            "type": "array",
            "minItems": 1,
            "maxItems": 4,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9_]{0,39}$"
            }
          },
          "instruction": {
            "description": "One visible assertion of at most 12 words, such as \"Queued row shows in both tabs\". State the intended result only; the runner applies evidence rules.",
            "type": "string",
            "minLength": 1,
            "maxLength": 4000,
            "pattern": "\\S"
          }
        }
      }
    },
    "primary_actor": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9_]{0,39}$",
      "description": "The actor whose Persona is the journey primary Persona."
    },
    "actors": {
      "type": "object",
      "minProperties": 1,
      "maxProperties": 4,
      "propertyNames": {
        "type": "string",
        "pattern": "^[a-z][a-z0-9_]{0,39}$"
      },
      "additionalProperties": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "persona"
        ],
        "properties": {
          "persona": {
            "type": "string",
            "minLength": 1,
            "maxLength": 160
          },
          "messaging": {
            "description": "Start this single-actor journey in the environment's Messages conversation.",
            "const": true
          },
          "distinct_from": {
            "type": "array",
            "maxItems": 3,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9_]{0,39}$"
            }
          }
        }
      }
    },
    "identity_probe": {
      "description": "A same-origin JSON identity endpoint used privately to verify the application subject and email match the selected account. Required for two or more signed-in actors; optional for one signed-in actor or anonymous-only journeys. A supplied probe is always used. Responses stay private. Connected Scenarios with multiple signed-in Personas also require a probe, on the launching Scenario or inherited consistently from connected sources. The app must make its normal authenticated GET to this endpoint; declaring it does not initiate a guessed identity request.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "path",
        "subject_pointer",
        "email_pointer"
      ],
      "properties": {
        "path": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500,
          "pattern": "^/(?!/)[^?#\\s\\\\\\u0000-\\u001f\\u007f]*$"
        },
        "subject_pointer": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500,
          "pattern": "^(?:/(?:[^~/]|~[01])*)+$"
        },
        "email_pointer": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500,
          "pattern": "^(?:/(?:[^~/]|~[01])*)+$"
        }
      }
    },
    "dependencies": {
      "type": "array",
      "maxItems": 100,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "scenario_id",
          "after_step",
          "before_step"
        ],
        "properties": {
          "scenario_id": {
            "type": "string",
            "description": "Saved Scenario ID or ./relative-file.yaml, resolved relative to this file within the submitted batch. Include referenced files in the same upload or run request. Paths never read server files.",
            "anyOf": [
              {
                "pattern": "^scenario_",
                "maxLength": 100
              },
              {
                "pattern": "^\\./[^\\\\\\r\\n\\u0000]+\\.ya?ml$",
                "maxLength": 500
              }
            ]
          },
          "after_step": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "description": "The referenced Scenario must finish this authored step (1-based). Use its last step for a whole-Scenario prerequisite, or an earlier step for a mid-Scenario handoff. This does not wait for its final outcomes."
          },
          "before_step": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "description": "This receiving Scenario waits before this authored step (1-based). Use 1 to wait before starting."
          },
          "name": {
            "type": "string",
            "maxLength": 80,
            "description": "Optional human-readable checkpoint label."
          }
        }
      },
      "description": "Waits owned by this receiving Scenario. Each entry releases before_step only after scenario_id completes after_step in the same run. Source step numbers are 1-based authored steps, excluding inferred sign-in, outcomes and cleanup. Running any member includes its incoming and outgoing connected Scenarios. Omission preserves saved dependencies on upload; [] clears them; null is invalid. Use ./relative-file.yaml to save new connected files together without obtaining IDs first. Saved dependencies contain IDs. Save linked files before running them. Guide: https://composal.ai/skills/composal-scenario-dependencies.md",
      "examples": [
        [
          {
            "scenario_id": "./process-receipt.yaml",
            "after_step": 2,
            "before_step": 2,
            "name": "Processing finished"
          }
        ]
      ]
    },
    "triggers": {
      "description": "Repository paths that always select this Scenario for fresh PR verification. Files, folders (./fe), and positive CODEOWNERS-style globs. ./ or / anchors at the repository root; bare names match at any depth. Any matching pattern adds the Scenario alongside automatic selection, including its dependencies. Omit to keep saved triggers; [] clears them.",
      "type": "array",
      "maxItems": 100,
      "uniqueItems": true,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 500,
        "pattern": "^(?!\\s)(?!.*\\s$)(?!.*[\\u0000-\\u001f\\u007f\\\\\\[\\]!#])(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*//)(?!/?$)(?!\\./$).+$"
      },
      "examples": [
        [
          "./fe",
          "/api/receipts/**",
          "**/*.graphql"
        ]
      ]
    },
    "on": {
      "type": [
        "object",
        "null"
      ],
      "description": "Explicit Actions-style automatic execution rules. Omit to preserve existing rules; null restores relevance selection. An empty object disables automatic execution.",
      "additionalProperties": false,
      "properties": {
        "pull_request": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "types": {
              "type": "array",
              "minItems": 1,
              "uniqueItems": true,
              "items": {
                "enum": [
                  "opened",
                  "reopened",
                  "synchronize",
                  "ready_for_review",
                  "labeled",
                  "unlabeled",
                  "edited"
                ]
              }
            },
            "paths": {
              "type": "array",
              "minItems": 1,
              "maxItems": 100,
              "uniqueItems": true,
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500
              }
            },
            "paths-ignore": {
              "type": "array",
              "minItems": 1,
              "maxItems": 100,
              "uniqueItems": true,
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500
              }
            },
            "branches": {
              "type": "array",
              "minItems": 1,
              "maxItems": 100,
              "uniqueItems": true,
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500
              }
            },
            "branches-ignore": {
              "type": "array",
              "minItems": 1,
              "maxItems": 100,
              "uniqueItems": true,
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500
              }
            }
          },
          "allOf": [
            {
              "not": {
                "required": [
                  "paths",
                  "paths-ignore"
                ]
              }
            },
            {
              "not": {
                "required": [
                  "branches",
                  "branches-ignore"
                ]
              }
            }
          ]
        },
        "schedule": {
          "type": "array",
          "maxItems": 10,
          "uniqueItems": true,
          "description": "Recurring runs. Cron minute slots must be at least five minutes apart. Each entry names the project environment it tests; timezone defaults to UTC.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "cron",
              "environment"
            ],
            "properties": {
              "cron": {
                "type": "string",
                "minLength": 9,
                "maxLength": 100
              },
              "timezone": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100,
                "default": "UTC"
              },
              "environment": {
                "type": "string",
                "pattern": "^[a-zA-Z0-9_-]{1,100}$",
                "description": "An existing, unarchived environment slug or venv_ ID from this Project."
              }
            }
          }
        }
      }
    },
    "if": {
      "type": [
        "string",
        "null"
      ],
      "maxLength": 1000,
      "description": "A documented subset of Actions expressions. Supports comparisons, &&, ||, !, parentheses, contains, startsWith and endsWith over GitHub event metadata. Empty or null clears it. Applied to automatic execution."
    }
  },
  "examples": [
    {
      "$schema": "https://composal.ai/schema/verify/v2/scenario.json",
      "name": "An owner can share a conversation with a teammate",
      "primary_actor": "owner",
      "actors": {
        "owner": {
          "persona": "Owner"
        },
        "teammate": {
          "persona": "Teammate",
          "distinct_from": [
            "owner"
          ]
        }
      },
      "allowed_actions": "writes",
      "steps": [
        {
          "actor": "owner",
          "instruction": "Share this run's conversation with the teammate."
        },
        {
          "actor": "teammate",
          "instruction": "Open that same shared conversation."
        }
      ],
      "outcomes": [
        {
          "actors": [
            "owner",
            "teammate"
          ],
          "instruction": "Both actors see that same conversation through their own sessions."
        }
      ]
    }
  ]
}
