{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://vestra.ai/developers/vestra.schema.json",
  "title": "vestra.yaml",
  "description": "A Bash deployment in one file. Apply it with vestra deploy, or with PUT /v1/deployments/{name} and the file as JSON.",
  "type": "object",
  "properties": {
    "$schema": {
      "description": "For editors reading JSON; ignored.",
      "type": "string",
      "maxLength": 500
    },
    "name": {
      "type": "string",
      "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
      "description": "The deployment's name, as in `PUT /v1/deployments/{name}`: lowercase letters, digits and dashes.",
      "examples": [
        "trip-rescue"
      ]
    },
    "instructions": {
      "default": "",
      "description": "What Bash does here, and how, in plain words: up to 8,000 characters.",
      "examples": [
        "You help Tripnest travellers fix broken trips. Always offer two options."
      ],
      "type": "string",
      "maxLength": 8000
    },
    "language": {
      "default": "auto",
      "description": "The language Bash speaks: a tag such as `en`, `hi` or `pt-BR`, or `auto` to follow each user.",
      "examples": [
        "hi"
      ],
      "type": "string",
      "minLength": 1,
      "maxLength": 35
    },
    "model": {
      "default": "default",
      "description": "`default`, or a model id from `GET /v1/models`.",
      "examples": [
        "default"
      ],
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "apps": {
      "default": "all",
      "description": "The apps its users may connect: `all`, or a list.",
      "examples": [
        [
          "gmail",
          "google-calendar"
        ]
      ],
      "anyOf": [
        {
          "type": "string",
          "const": "all"
        },
        {
          "maxItems": 200,
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          }
        }
      ]
    },
    "tools": {
      "default": [],
      "description": "Tools Bash may call: MCP servers by their URL, and functions your own code answers. At most 50.",
      "examples": [
        [
          {
            "function": "get_booking",
            "description": "A booking by its PNR",
            "input": {
              "pnr": "string"
            },
            "read_only": true
          },
          {
            "mcp": "https://mcp.example.com/travel",
            "require_approval": [
              "book_flight"
            ]
          }
        ]
      ],
      "maxItems": 50,
      "type": "array",
      "items": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "mcp": {
                "type": "string",
                "maxLength": 2048,
                "format": "uri",
                "description": "The server's public https URL. The first file that names it registers it, and later files find it by it.",
                "examples": [
                  "https://mcp.example.com/travel"
                ]
              },
              "name": {
                "description": "How the server is shown. Absent: the URL's host.",
                "examples": [
                  "Travel desk"
                ],
                "type": "string",
                "minLength": 1,
                "maxLength": 80
              },
              "auth": {
                "description": "One company secret, written `${NAME}`: sent as `Authorization: Bearer` and its value.",
                "examples": [
                  "${TRAVEL_MCP_TOKEN}"
                ],
                "type": "string",
                "pattern": "^\\$\\{(?:(?:secret|user_secret):)?[A-Z][A-Z0-9_]{0,63}\\}$"
              },
              "headers": {
                "description": "Other headers to send. A value may carry `${NAME}` (a company secret) or `${user_secret:NAME}` (each user's own).",
                "examples": [
                  {
                    "X-Agency": "${user_secret:AGENCY_ID}"
                  }
                ],
                "type": "object",
                "propertyNames": {
                  "type": "string",
                  "pattern": "^[A-Za-z0-9-]{1,64}$"
                },
                "additionalProperties": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2048
                }
              },
              "deny": {
                "description": "Tools of this server Bash never calls. With `require_approval`, it is this version's own tool policy (`mcp_tool_policy`), on top of the server's floor.",
                "examples": [
                  [
                    "refund_booking"
                  ]
                ],
                "maxItems": 200,
                "type": "array",
                "items": {
                  "type": "string",
                  "pattern": "^[A-Za-z0-9_.-]{1,128}$"
                }
              },
              "require_approval": {
                "description": "Tools of this server that always wait for the person's yes, even one the server marks read-only.",
                "examples": [
                  [
                    "book_flight"
                  ]
                ],
                "maxItems": 200,
                "type": "array",
                "items": {
                  "type": "string",
                  "pattern": "^[A-Za-z0-9_.-]{1,128}$"
                }
              },
              "run_without_asking": {
                "description": "Tools of this server Bash runs without waiting for the person's yes. Every other tool waits for it, unless the server marks the tool read-only (readOnlyHint).",
                "examples": [
                  [
                    "points_balance"
                  ]
                ],
                "maxItems": 200,
                "type": "array",
                "items": {
                  "type": "string",
                  "pattern": "^[A-Za-z0-9_.-]{1,128}$"
                }
              }
            },
            "required": [
              "mcp"
            ],
            "additionalProperties": false,
            "description": "An MCP server Bash may call, by its URL. Each of its tools waits for the person's yes before it runs, unless the server marks it read-only or `run_without_asking` lists it."
          },
          {
            "type": "object",
            "properties": {
              "function": {
                "type": "string",
                "pattern": "^(?!mcp__)[a-z][a-z0-9_]{0,63}$",
                "description": "The tool's name: lowercase letters, digits and underscores, starting with a letter.",
                "examples": [
                  "get_booking"
                ]
              },
              "description": {
                "type": "string",
                "minLength": 1,
                "maxLength": 1024,
                "description": "What the tool does, so Bash knows when to call it.",
                "examples": [
                  "A booking by its PNR"
                ]
              },
              "input": {
                "default": {},
                "description": "Its arguments: shorthand such as `{ pnr: string }`, with `[]` for a list and `?` for optional, or JSON Schema.",
                "examples": [
                  {
                    "pnr": "string",
                    "seats": "integer?"
                  }
                ],
                "type": "object",
                "propertyNames": {
                  "type": "string"
                },
                "additionalProperties": {}
              },
              "timeout_s": {
                "description": "Seconds Bash waits for your answer, 5 to 600. Absent: 120.",
                "examples": [
                  30
                ],
                "type": "integer",
                "minimum": 5,
                "maximum": 600
              },
              "read_only": {
                "description": "true when the tool changes nothing, such as a lookup: Bash calls it without waiting for the person's yes. Absent: false, so it waits.",
                "examples": [
                  true
                ],
                "type": "boolean"
              },
              "run_without_asking": {
                "description": "true when the tool changes something that needs no yes: Bash calls it without waiting for the person's yes. Absent: false, so it waits.",
                "examples": [
                  true
                ],
                "type": "boolean"
              }
            },
            "required": [
              "function",
              "description"
            ],
            "additionalProperties": false,
            "description": "A tool your own code answers: Bash's call reaches you as an interaction of type `tool_call`. It waits for the person's yes first, unless `read_only` or `run_without_asking` is true."
          }
        ]
      }
    },
    "skills": {
      "default": [],
      "description": "Skills Bash may use: a local folder of skills, or a list of folders, company skills by id and skills Bash has by name. At most 100.",
      "examples": [
        [
          "./skills/refunds",
          {
            "name": "pdf"
          }
        ]
      ],
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 500
        },
        {
          "maxItems": 100,
          "type": "array",
          "items": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 500,
                "description": "A local folder: the CLI and the SDKs upload it."
              },
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "pattern": "^skl_",
                    "description": "A company skill's `skl_` id."
                  }
                },
                "required": [
                  "id"
                ],
                "additionalProperties": false,
                "description": "A company skill, by its id."
              },
              {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,63}$",
                    "description": "The skill's name."
                  }
                },
                "required": [
                  "name"
                ],
                "additionalProperties": false,
                "description": "A skill Bash already has, by its name."
              }
            ]
          }
        }
      ]
    },
    "knowledge": {
      "default": [],
      "description": "What Bash reads before it answers: a local folder or file, or a list of them and notebooks by id. At most 50.",
      "examples": [
        "./policies"
      ],
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 500
        },
        {
          "maxItems": 50,
          "type": "array",
          "items": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 500,
                "description": "A local folder or file: the CLI and the SDKs upload it into a notebook."
              },
              {
                "type": "object",
                "properties": {
                  "notebook": {
                    "type": "string",
                    "pattern": "^nb_",
                    "description": "The notebook's `nb_` id."
                  }
                },
                "required": [
                  "notebook"
                ],
                "additionalProperties": false,
                "description": "A notebook, by its id."
              }
            ]
          }
        }
      ]
    },
    "approvals": {
      "default": [
        "payment",
        "booking",
        "cancellation"
      ],
      "description": "What Bash asks the person before doing. A system approval enforces what `approvals` lists for your own functions and your MCP servers' tools that wait for a yes; in a browser or a connected app, Bash is instructed to ask.",
      "examples": [
        [
          "payment",
          "booking",
          "cancellation",
          "send_email"
        ]
      ],
      "maxItems": 6,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "payment",
          "booking",
          "cancellation",
          "send_email",
          "publish",
          "delete"
        ]
      }
    },
    "memory": {
      "description": "What Bash remembers about each user: `vestra` (Bash keeps it), `customer` (your side keeps it) or `none`. Absent: the company's default.",
      "examples": [
        "vestra"
      ],
      "type": "string",
      "enum": [
        "vestra",
        "customer",
        "none"
      ]
    },
    "computer": {
      "description": "Bash's computer, ready before the first task: a setup script, repositories, environment variables and files.",
      "examples": [
        {
          "setup": "./setup.sh",
          "env": {
            "REGION": "ap-south-1"
          }
        }
      ],
      "type": "object",
      "properties": {
        "setup": {
          "description": "A setup script, run once on each computer. Name a local file: the CLI and the SDKs read it and send its text.",
          "examples": [
            "./setup.sh"
          ],
          "type": "string",
          "maxLength": 20000
        },
        "repos": {
          "description": "Repositories cloned onto the computer, at most 10.",
          "examples": [
            [
              "github.com/acme/booking-tools"
            ]
          ],
          "maxItems": 10,
          "type": "array",
          "items": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 2048,
                "description": "`github.com/owner/repo`, or its https URL: branch main, cloned into a folder named after the repo."
              },
              {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2048,
                    "description": "The repository's address."
                  },
                  "ref": {
                    "description": "The branch, tag or commit. Absent: main.",
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "path": {
                    "description": "The folder to clone into. Absent: the repository's name.",
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "token": {
                    "description": "A token kept as a company secret, written `${NAME}`: never the raw value.",
                    "type": "string",
                    "pattern": "^\\$\\{(?:(?:secret|user_secret):)?[A-Z][A-Z0-9_]{0,63}\\}$"
                  }
                },
                "required": [
                  "url"
                ],
                "additionalProperties": false,
                "description": "A repository with its branch, folder or token."
              }
            ]
          }
        },
        "env": {
          "description": "Environment variables. A credential is `${NAME}`, never a raw value.",
          "examples": [
            {
              "REGION": "ap-south-1",
              "BOOKING_TOKEN": "${BOOKING_TOKEN}"
            }
          ],
          "type": "object",
          "propertyNames": {
            "type": "string",
            "pattern": "^[A-Z_][A-Z0-9_]*$"
          },
          "additionalProperties": {
            "type": "string",
            "maxLength": 4096
          }
        },
        "files": {
          "description": "Files uploaded with `POST /v1/files`, placed on the computer.",
          "examples": [
            [
              {
                "file_id": "file_8f2k",
                "path": "data/fares.csv"
              }
            ]
          ],
          "maxItems": 50,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "file_id": {
                "type": "string",
                "pattern": "^file_",
                "description": "The file's `file_` id."
              },
              "path": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200,
                "description": "Where it goes on the computer."
              }
            },
            "required": [
              "file_id",
              "path"
            ],
            "additionalProperties": false
          }
        }
      },
      "additionalProperties": false
    },
    "browser": {
      "description": "The sites a browser fetch may open, and the pages the browser never opens or acts on. Absent: a fetch opens nothing, and no page is closed.",
      "examples": [
        {
          "allowed_domains": [
            "ixigo.com"
          ],
          "blocked_paths": [
            "ixigo.com/payments/"
          ]
        }
      ],
      "type": "object",
      "properties": {
        "allowed_domains": {
          "description": "The sites it may open, as bare host names: `ixigo.com` also allows `www.ixigo.com`. Up to 20.",
          "minItems": 1,
          "maxItems": 20,
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 253,
            "pattern": "^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$"
          }
        },
        "blocked_paths": {
          "description": "Pages the browser never opens or acts on, as a site and the start of a path, such as `ixigo.com/payments/`. Up to 20.",
          "minItems": 1,
          "maxItems": 20,
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 300,
            "pattern": "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]\\/[^\\s?#]*$"
          }
        }
      },
      "additionalProperties": false
    },
    "routines": {
      "default": [],
      "description": "Jobs Bash runs on a schedule or on a call, for your company or for each user. At most 50.",
      "examples": [
        [
          {
            "ask": "Check today's trips for delays",
            "every": "weekday at 08:00",
            "for": "each customer"
          }
        ]
      ],
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "description": "Its name, which finds the routine again on the next apply. Absent: the start of `ask`.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "ask": {
            "type": "string",
            "minLength": 1,
            "maxLength": 8000,
            "description": "What Bash does on each run, in plain words."
          },
          "every": {
            "description": "When it runs, in plain words: `weekday at 08:00`, `mon, wed, fri at 18:30`, `30 minutes`, `month on the 1st at 09:00`. Give one of `every`, `schedule` or `on`.",
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "schedule": {
            "description": "When it runs, as cron.",
            "type": "object",
            "properties": {
              "cron": {
                "type": "string",
                "minLength": 1,
                "maxLength": 120,
                "description": "A cron expression: `0 7 * * *` runs at 7:00 every day."
              },
              "timezone": {
                "description": "The zone the cron is read in, an IANA name such as `Asia/Kolkata`.",
                "type": "string",
                "minLength": 1,
                "maxLength": 64
              }
            },
            "required": [
              "cron"
            ],
            "additionalProperties": false
          },
          "on": {
            "description": "`call`: it runs whenever your system posts to its `trigger_url`.",
            "type": "string",
            "const": "call"
          },
          "filters": {
            "description": "With `on: call`, what a call's payload must match for the routine to run.",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          },
          "timezone": {
            "description": "The zone `every` is read in, an IANA name. Absent: each user's own (or your company's own Bash's), then UTC.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "for": {
            "type": "string",
            "enum": [
              "us",
              "each customer"
            ],
            "description": "`us`: your company's own Bash. `each customer`: a copy for every user, made the first time they talk to Bash here."
          },
          "enabled": {
            "description": "false keeps the routine but stops its runs. A routine for each user is on while it is in the file.",
            "type": "boolean"
          }
        },
        "required": [
          "ask",
          "for"
        ],
        "additionalProperties": false
      }
    },
    "limits": {
      "default": {
        "per_task_usd": 2
      },
      "description": "What one task may spend.",
      "examples": [
        {
          "per_task_usd": 5
        }
      ],
      "type": "object",
      "properties": {
        "per_task_usd": {
          "default": 2,
          "description": "The most one task may spend, in dollars: above 0, at most 100. Absent: 2.",
          "type": "number",
          "exclusiveMinimum": 0,
          "maximum": 100
        }
      },
      "additionalProperties": false
    },
    "tests": {
      "default": [],
      "description": "Tests each new version runs before it is promoted. At most 20.",
      "examples": [
        [
          {
            "name": "Bag allowance",
            "ask": "How many kilos can I take on booking TN-48213?",
            "expect": "Says 7 kg of cabin baggage.",
            "tool_results": {
              "get_booking": {
                "cabin_kg": 7
              }
            }
          }
        ]
      ],
      "maxItems": 20,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "description": "Its name, which finds the test again on the next apply. Absent: the start of `ask`.",
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "ask": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "description": "What the test user wants, in their words."
          },
          "expect": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 500
              },
              {
                "minItems": 1,
                "maxItems": 12,
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 500
                }
              }
            ],
            "description": "What a pass looks like: one line, or up to 12."
          },
          "never": {
            "description": "What must never happen: one line, or up to 12.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 500
              },
              {
                "maxItems": 12,
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 500
                }
              }
            ]
          },
          "persona": {
            "description": "Who the test user is. Absent: a person who uses the company's app.",
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          },
          "tool_results": {
            "description": "What each tool answers in this test: a function tool by its name, and a tool of an MCP server as <server>.<tool> (the server's slug, from its URL's host), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error.",
            "type": "object",
            "propertyNames": {
              "type": "string",
              "pattern": "^(?!mcp__)(?:[a-z][a-z0-9_]{0,63}|[a-z][a-z0-9-]{0,39}\\.[A-Za-z0-9_.-]{1,128})$"
            },
            "additionalProperties": {}
          },
          "approves": {
            "description": "true: the test user says yes to each approval for a tool of your MCP server, which plays its `tool_results` entry or does not run, so the test can check what Bash does after a yes. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined.",
            "type": "boolean"
          }
        },
        "required": [
          "ask",
          "expect"
        ],
        "additionalProperties": false
      }
    },
    "webhooks": {
      "default": {},
      "description": "Where events go: each https URL, with the events it gets, or `*` for every one.",
      "examples": [
        {
          "https://example.com/vestra": [
            "task.completed",
            "interaction.created"
          ]
        }
      ],
      "type": "object",
      "propertyNames": {
        "type": "string",
        "maxLength": 2048,
        "format": "uri"
      },
      "additionalProperties": {
        "minItems": 1,
        "maxItems": 40,
        "type": "array",
        "items": {
          "anyOf": [
            {
              "type": "string",
              "enum": [
                "message.completed",
                "task.started",
                "task.completed",
                "task.failed",
                "task.stopped",
                "interaction.created",
                "interaction.resolved",
                "interaction.expired",
                "session.started",
                "session.ended",
                "file.created",
                "routine.fired",
                "usage.threshold_reached",
                "usage.cap_reached",
                "conversation.status",
                "user.created",
                "user.deleted",
                "connection.completed",
                "connection.failed",
                "connection.removed",
                "memory.observed",
                "batch.completed",
                "deployment.version_created",
                "deployment.promoted",
                "deployment.rolled_back",
                "test_run.completed",
                "key.created",
                "key.revoked"
              ],
              "description": "An event a webhook carries: every event of the catalog but the six that ride only the stream (message.delta, task.progress, usage.updated, screen.control, error and ping).",
              "examples": [
                "task.completed"
              ]
            },
            {
              "type": "string",
              "const": "*"
            }
          ],
          "description": "An event the endpoint wants, by its type, or * for every event a webhook carries.",
          "examples": [
            "task.completed"
          ]
        }
      }
    }
  },
  "required": [
    "name"
  ],
  "additionalProperties": false
}
