{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://docs.salesos.org/schemas/salesos.app.schema.json",
  "title": "SalesOS app manifest (salesos.app.json)",
  "type": "object",
  "required": [
    "manifestVersion",
    "slug",
    "name",
    "version",
    "publisher",
    "oauth"
  ],
  "additionalProperties": false,
  "definitions": {
    "key": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,47}$" },
    "httpsUrl": { "type": "string", "maxLength": 2048, "pattern": "^https://" },
    "label": { "type": "string", "minLength": 1, "maxLength": 80 },
    "objectType": {
      "type": "string",
      "pattern": "^(LEAD|CONTACT|ACCOUNT|OPPORTUNITY|TASK|custom:[a-z][a-z0-9_]{1,47})$"
    },
    "objectSchema": {
      "type": "object",
      "required": ["type", "properties"],
      "properties": {
        "type": { "const": "object" },
        "properties": { "type": "object", "maxProperties": 50 },
        "required": { "type": "array", "items": { "type": "string" } },
        "additionalProperties": { "type": "boolean" }
      }
    },
    "stringList": {
      "type": "array",
      "uniqueItems": true,
      "items": { "type": "string", "minLength": 1, "maxLength": 200 }
    }
  },
  "properties": {
    "$schema": { "type": "string" },
    "manifestVersion": { "const": 1 },
    "slug": { "type": "string", "pattern": "^[a-z][a-z0-9-]{2,39}$" },
    "name": { "type": "string", "minLength": 1, "maxLength": 80 },
    "version": {
      "type": "string",
      "maxLength": 40,
      "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
    },
    "description": { "type": "string", "maxLength": 2000 },
    "publisher": {
      "type": "object",
      "required": ["name"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "minLength": 1, "maxLength": 100 },
        "website": { "$ref": "#/definitions/httpsUrl" },
        "supportEmail": {
          "type": "string",
          "maxLength": 200,
          "pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
        }
      }
    },
    "icons": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "64": { "$ref": "#/definitions/httpsUrl" },
        "128": { "$ref": "#/definitions/httpsUrl" }
      }
    },
    "urls": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "homepage": { "$ref": "#/definitions/httpsUrl" },
        "privacyPolicy": { "$ref": "#/definitions/httpsUrl" },
        "termsOfService": { "$ref": "#/definitions/httpsUrl" },
        "support": { "$ref": "#/definitions/httpsUrl" }
      }
    },
    "oauth": {
      "type": "object",
      "required": ["clientId", "scopes"],
      "additionalProperties": false,
      "properties": {
        "clientId": { "type": "string", "minLength": 1, "maxLength": 100 },
        "scopes": {
          "type": "array",
          "maxItems": 64,
          "uniqueItems": true,
          "items": {
            "type": "string",
            "pattern": "^((READ|WRITE)_[A-Z_]+|openid|profile|email|offline_access)$"
          }
        }
      }
    },
    "customObjects": {
      "type": "array",
      "maxItems": 5,
      "items": {
        "type": "object",
        "required": ["key", "label", "pluralLabel"],
        "additionalProperties": false,
        "properties": {
          "key": { "$ref": "#/definitions/key" },
          "label": { "$ref": "#/definitions/label" },
          "pluralLabel": { "$ref": "#/definitions/label" },
          "description": { "type": "string", "maxLength": 1000 },
          "fields": {
            "type": "array",
            "maxItems": 30,
            "items": {
              "type": "object",
              "required": ["name", "label", "type"],
              "additionalProperties": false,
              "properties": {
                "name": { "$ref": "#/definitions/key" },
                "label": { "$ref": "#/definitions/label" },
                "type": {
                  "enum": [
                    "text",
                    "textarea",
                    "number",
                    "currency",
                    "date",
                    "boolean",
                    "select",
                    "email",
                    "phone",
                    "url",
                    "lookup",
                    "master_detail"
                  ]
                },
                "required": { "type": "boolean" },
                "options": {
                  "$ref": "#/definitions/stringList",
                  "maxItems": 100
                },
                "target": {
                  "type": "string",
                  "maxLength": 70,
                  "description": "lookup / master_detail: LEAD, CONTACT, ACCOUNT, OPPORTUNITY, TASK, CASE or custom:<key> of an object declared earlier in this manifest"
                },
                "onDelete": {
                  "enum": ["CLEAR", "RESTRICT", "CASCADE"],
                  "description": "lookup: CLEAR (default) or RESTRICT; master_detail: CASCADE (default) or RESTRICT"
                }
              }
            }
          }
        }
      }
    },
    "customFields": {
      "type": "array",
      "maxItems": 25,
      "items": {
        "type": "object",
        "required": ["key", "object", "label", "type"],
        "additionalProperties": false,
        "properties": {
          "key": { "$ref": "#/definitions/key" },
          "object": {
            "enum": [
              "LEAD",
              "CONTACT",
              "ACCOUNT",
              "OPPORTUNITY",
              "PRODUCT",
              "QUOTE"
            ]
          },
          "label": { "$ref": "#/definitions/label" },
          "type": {
            "enum": [
              "TEXT",
              "TEXTAREA",
              "NUMBER",
              "CURRENCY",
              "DATE",
              "DATETIME",
              "CHECKBOX",
              "PICKLIST",
              "MULTI_PICKLIST",
              "EMAIL",
              "PHONE",
              "URL"
            ]
          },
          "description": { "type": "string", "maxLength": 500 },
          "required": { "type": "boolean" },
          "options": { "$ref": "#/definitions/stringList", "maxItems": 100 }
        }
      }
    },
    "customActions": {
      "type": "array",
      "maxItems": 20,
      "items": {
        "type": "object",
        "required": [
          "key",
          "label",
          "url",
          "kind",
          "inputSchema",
          "outputSchema"
        ],
        "additionalProperties": false,
        "properties": {
          "key": { "$ref": "#/definitions/key" },
          "label": { "$ref": "#/definitions/label" },
          "description": { "type": "string", "maxLength": 1000 },
          "url": { "$ref": "#/definitions/httpsUrl" },
          "kind": { "enum": ["READ", "WRITE"] },
          "inputSchema": { "$ref": "#/definitions/objectSchema" },
          "outputSchema": { "$ref": "#/definitions/objectSchema" },
          "objectTypes": {
            "type": "array",
            "maxItems": 10,
            "uniqueItems": true,
            "items": { "$ref": "#/definitions/objectType" }
          },
          "allowManual": { "type": "boolean" },
          "exposeAsTool": { "type": "boolean" },
          "toolDescription": { "type": "string", "maxLength": 1000 },
          "idempotent": { "type": "boolean" },
          "timeoutMs": { "type": "integer", "minimum": 500, "maximum": 30000 },
          "inputMapping": { "type": "object" },
          "outputMapping": {
            "type": "object",
            "additionalProperties": { "type": "string" }
          }
        }
      }
    },
    "beforeSaveHooks": {
      "type": "array",
      "maxItems": 10,
      "items": {
        "type": "object",
        "required": ["key", "name", "objectType", "operations", "url"],
        "additionalProperties": false,
        "properties": {
          "key": { "$ref": "#/definitions/key" },
          "name": { "$ref": "#/definitions/label" },
          "description": { "type": "string", "maxLength": 1000 },
          "objectType": { "$ref": "#/definitions/objectType" },
          "operations": {
            "type": "array",
            "minItems": 1,
            "uniqueItems": true,
            "items": { "enum": ["CREATE", "UPDATE", "DELETE"] }
          },
          "url": { "$ref": "#/definitions/httpsUrl" },
          "timeoutMs": { "type": "integer", "minimum": 250, "maximum": 5000 },
          "failureMode": { "enum": ["FAIL_OPEN", "FAIL_CLOSED"] }
        }
      }
    },
    "webhooks": {
      "type": "array",
      "maxItems": 10,
      "items": {
        "type": "object",
        "required": ["key", "url", "events"],
        "additionalProperties": false,
        "properties": {
          "key": { "$ref": "#/definitions/key" },
          "url": { "$ref": "#/definitions/httpsUrl" },
          "description": { "type": "string", "maxLength": 500 },
          "events": {
            "type": "array",
            "minItems": 1,
            "maxItems": 60,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "description": "An event type from GET /v1/events/catalog"
            }
          }
        }
      }
    },
    "uiExtensions": {
      "type": "array",
      "maxItems": 20,
      "items": {
        "type": "object",
        "required": ["key", "slot", "label", "url"],
        "additionalProperties": false,
        "properties": {
          "key": { "$ref": "#/definitions/key" },
          "slot": {
            "enum": [
              "record.tab",
              "record.sidePanel",
              "list.action",
              "dashboard.widget",
              "record.headerAction",
              "app.page",
              "global.modal"
            ]
          },
          "label": { "type": "string", "minLength": 1, "maxLength": 40 },
          "url": { "$ref": "#/definitions/httpsUrl" },
          "objects": {
            "description": "Record and list slots: the objects they appear on. custom:<apiName> for a custom object (an app's own objects use their key).",
            "type": "array",
            "maxItems": 20,
            "uniqueItems": true,
            "items": {
              "anyOf": [
                { "enum": ["LEAD", "CONTACT", "ACCOUNT", "OPPORTUNITY"] },
                { "type": "string", "pattern": "^custom:[a-z][a-z0-9_]{0,62}$" }
              ]
            }
          },
          "height": { "type": "integer", "minimum": 80, "maximum": 2000 },
          "formFactors": {
            "description": "Where the extension renders: desktop (768 px and wider) and/or mobile. Default both.",
            "type": "array",
            "minItems": 1,
            "uniqueItems": true,
            "items": { "enum": ["desktop", "mobile"] }
          },
          "modalSize": {
            "description": "Dialog size for list.action, record.headerAction and global.modal (phones always use the full screen).",
            "enum": ["sm", "md", "lg", "full"]
          }
        }
      }
    },
    "functions": {
      "description": "Functions: function.json settings plus `source` (TypeScript or JavaScript) and `language`. No secrets and no runAsAdmin; they run as the installing user. Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 10,
      "items": { "type": "object", "required": ["apiName", "name", "source"] }
    },
    "validationRules": {
      "description": "Validation rules (validation-rules/*.rule.json). Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 50,
      "items": { "type": "object", "required": ["entityType", "name"] }
    },
    "workflows": {
      "description": "Workflows (workflows/*.workflow.json); installed active. Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 25,
      "items": { "type": "object", "required": ["name"] }
    },
    "flows": {
      "description": "Flows (flows/*.flow.json); installed active. Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 25,
      "items": { "type": "object", "required": ["apiName"] }
    },
    "approvalProcesses": {
      "description": "Approval processes (approval-processes/*.approval.json); installed active. Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 10,
      "items": { "type": "object", "required": ["entity", "name"] }
    },
    "customLabels": {
      "description": "Custom labels (labels/*.label.json). Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 500,
      "items": { "type": "object", "required": ["name"] }
    },
    "translations": {
      "description": "Translations, one entry per locale (translations/*.translation.json). Uninstall removes only the keys the app added. Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 10,
      "items": { "type": "object", "required": ["locale"] }
    },
    "pageLayouts": {
      "description": "Page layouts (layouts/*.layout.json); app slots as app/<slug>:<key>. Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 20,
      "items": { "type": "object", "required": ["objectType", "name"] }
    },
    "pages": {
      "description": "Page-builder record pages (pages/*.page.json). Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 20,
      "items": { "type": "object", "required": ["apiName"] }
    },
    "appPages": {
      "description": "Page-builder app pages (app-pages/*.apppage.json). Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 10,
      "items": { "type": "object", "required": ["apiName"] }
    },
    "reports": {
      "description": "Report definitions (reports/*.report.json). Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 50,
      "items": { "type": "object", "required": ["name"] }
    },
    "dashboards": {
      "description": "Team or org dashboards (dashboards/*.dashboard.json). Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 20,
      "items": { "type": "object", "required": ["name"] }
    },
    "emailTemplates": {
      "description": "Shared email templates (email-templates/*.email.json). Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 50,
      "items": { "type": "object", "required": ["name"] }
    },
    "customEventTypes": {
      "description": "Custom event types (events/*.event.json), published as custom.<apiName>. Same JSON as the metadata source file (https://docs.salesos.org/schemas/salesos.metadata.schema.json); checked against the installing org on install.",
      "type": "array",
      "maxItems": 20,
      "items": { "type": "object", "required": ["apiName", "label"] }
    },
    "namedCredentials": {
      "description": "Named credentials (credentials/*.credential.json), configuration only. Never include secrets: one that needs a secret installs inactive and the installing admin sets it in Settings, Developer, Connections.",
      "type": "array",
      "maxItems": 10,
      "items": {
        "type": "object",
        "required": ["apiName", "label", "baseUrl"],
        "not": { "anyOf": [{ "required": ["secret"] }, { "required": ["clientSecret"] }] }
      }
    },
    "settings": {
      "description": "Settings the installing admin fills in: the same JSON Schema subset as custom action inputs",
      "$ref": "#/definitions/objectSchema"
    }
  }
}
