# Vertesia Documentation Generated from the docs MDX source at request time. ## Contents - [Vertesia Documentation: Engineering Reliable AI for the Enterprise](https://docs.vertesiahq.com/llms/index.md) - [Configuration](https://docs.vertesiahq.com/llms/agent-runner/configuration.md) - [Custom Tools](https://docs.vertesiahq.com/llms/agent-runner/custom-tools.md) - [Email Integration](https://docs.vertesiahq.com/llms/agent-runner/email-integration.md) - [Agent Runner - Getting Started](https://docs.vertesiahq.com/llms/agent-runner/getting-started.md) - [MCP Tools](https://docs.vertesiahq.com/llms/agent-runner/mcp-tools.md) - [Agent Message System](https://docs.vertesiahq.com/llms/agent-runner/messaging.md) - [Overview](https://docs.vertesiahq.com/llms/agent-runner/overview.md) - [SDK Integration](https://docs.vertesiahq.com/llms/agent-runner/sdk.md) - [Skills](https://docs.vertesiahq.com/llms/agent-runner/skills.md) - [How Skills Work](https://docs.vertesiahq.com/llms/agent-runner/skills-model.md) - [Built-in Tools](https://docs.vertesiahq.com/llms/agent-runner/tools.md) - [Workstreams](https://docs.vertesiahq.com/llms/agent-runner/workstreams.md) - [Introduction](https://docs.vertesiahq.com/llms/api/introduction.md) - [Installed Application Settings](https://docs.vertesiahq.com/llms/apps/installation-settings.md) - [What are Applications?](https://docs.vertesiahq.com/llms/apps/overview.md) - [MCP Servers](https://docs.vertesiahq.com/llms/apps/tool-collections.md) - [Building an Application UI](https://docs.vertesiahq.com/llms/apps/ui.md) - [Vertesia CLI](https://docs.vertesiahq.com/llms/cli.md) - [Concepts](https://docs.vertesiahq.com/llms/concepts.md) - [Collaborative document editing](https://docs.vertesiahq.com/llms/content/collaborative-editing.md) - [Embeddings Configuration](https://docs.vertesiahq.com/llms/content/embeddings.md) - [Intelligent Document Processing (IDP)](https://docs.vertesiahq.com/llms/content/grounded-extraction.md) - [Intake Configuration](https://docs.vertesiahq.com/llms/content/intake-policies.md) - [Markdown Export Guide](https://docs.vertesiahq.com/llms/content/markdown-export.md) - [Content Indexing Overview](https://docs.vertesiahq.com/llms/content/overview.md) - [Search Configuration](https://docs.vertesiahq.com/llms/content/search.md) - [View Experiences](https://docs.vertesiahq.com/llms/content/view-experiences.md) - [Configuration](https://docs.vertesiahq.com/llms/content/view-experiences/configuration.md) - [Embedding in applications](https://docs.vertesiahq.com/llms/content/view-experiences/embedding.md) - [Quickstart](https://docs.vertesiahq.com/llms/content/view-experiences/quickstart.md) - [API & validation reference](https://docs.vertesiahq.com/llms/content/view-experiences/reference.md) - [Search & agentic planning](https://docs.vertesiahq.com/llms/content/view-experiences/search.md) - [Troubleshooting](https://docs.vertesiahq.com/llms/content/view-experiences/troubleshooting.md) - [Dashboards](https://docs.vertesiahq.com/llms/data-platform/dashboards.md) - [Getting Started](https://docs.vertesiahq.com/llms/data-platform/getting-started.md) - [Overview](https://docs.vertesiahq.com/llms/data-platform/overview.md) - [Skills Reference](https://docs.vertesiahq.com/llms/data-platform/skills.md) - [Tools Reference](https://docs.vertesiahq.com/llms/data-platform/tools.md) - [Configuration](https://docs.vertesiahq.com/llms/environments.md) - [Amazon Bedrock](https://docs.vertesiahq.com/llms/environments/aws.md) - [Microsoft Azure](https://docs.vertesiahq.com/llms/environments/azure.md) - [Google Cloud](https://docs.vertesiahq.com/llms/environments/gcp.md) - [Model Deprecation](https://docs.vertesiahq.com/llms/environments/model-deprecation.md) - [OpenAI](https://docs.vertesiahq.com/llms/environments/openai.md) - [Errors](https://docs.vertesiahq.com/llms/errors.md) - [Event Subscriptions](https://docs.vertesiahq.com/llms/events/subscriptions.md) - [Vertesia MCP Connector](https://docs.vertesiahq.com/llms/integrations/mcp.md) - [Amazon S3 Integration](https://docs.vertesiahq.com/llms/integrations/s3.md) - [Agent nodes](https://docs.vertesiahq.com/llms/processes/agent-nodes.md) - [Authoring processes](https://docs.vertesiahq.com/llms/processes/authoring.md) - [The Process Model](https://docs.vertesiahq.com/llms/processes/model.md) - [Node types](https://docs.vertesiahq.com/llms/processes/node-types.md) - [Observability](https://docs.vertesiahq.com/llms/processes/observability.md) - [Processes](https://docs.vertesiahq.com/llms/processes/overview.md) - [Task Inbox](https://docs.vertesiahq.com/llms/processes/task-inbox.md) - [Tutorial: Contract Review](https://docs.vertesiahq.com/llms/processes/tutorial-contract-review.md) - [Getting Started](https://docs.vertesiahq.com/llms/quickstart.md) - [Attribute-Based Access Control](https://docs.vertesiahq.com/llms/security/abac.md) - [Configure content security](https://docs.vertesiahq.com/llms/security/content-security.md) - [Access control for developers](https://docs.vertesiahq.com/llms/security/developer-guide.md) - [Configure dynamic access rules](https://docs.vertesiahq.com/llms/security/dynamic-rules.md) - [Security & Access Control](https://docs.vertesiahq.com/llms/security/overview.md) - [Configure principal attributes](https://docs.vertesiahq.com/llms/security/principal-attributes.md) - [Configure roles and memberships](https://docs.vertesiahq.com/llms/security/roles-memberships.md) - [SCIM with Microsoft Entra](https://docs.vertesiahq.com/llms/security/scim.md) - [SSO with Microsoft Entra](https://docs.vertesiahq.com/llms/security/sso.md) - [Content Preparation Configuration](https://docs.vertesiahq.com/llms/semantic/configuration.md) - [Markdown Conversion](https://docs.vertesiahq.com/llms/semantic/getting-started.md) - [Content Preparation](https://docs.vertesiahq.com/llms/semantic/overview.md) - [Studio Assistant](https://docs.vertesiahq.com/llms/studio/assistant.md) - [Interactions](https://docs.vertesiahq.com/llms/studio/interactions.md) - [Prompts](https://docs.vertesiahq.com/llms/studio/prompts.md) - [Release Notes Generation](https://docs.vertesiahq.com/llms/use-cases/release-notes-generation.md) - [Workflow Activities](https://docs.vertesiahq.com/llms/workflows/activities-catalog.md) - [Configuration](https://docs.vertesiahq.com/llms/workflows/configuration.md) - [Workflows for Agentic AI](https://docs.vertesiahq.com/llms/workflows/overview.md) - [Workflow DSL](https://docs.vertesiahq.com/llms/workflows/workflow-dsl.md) --- ## Vertesia Documentation: Engineering Reliable AI for the Enterprise Source: https://docs.vertesiahq.com Markdown: https://docs.vertesiahq.com/llms/index.md A Unified, API-First Platform for Architects and Developers to Build, Deploy, and Scale Specialized AI Agents and Applications with Unparalleled Accuracy and Operational Resilience. ## Addressing Critical Challenges in Enterprise AI Enterprise generative AI initiatives often stall in production due to inherent technical complexities. **LLM Hallucinations & Data Fidelity:** Achieving enterprise-grade accuracy (beyond 95%) is critical. Generic LLMs struggle with factual consistency, leading to unreliable outputs that undermine trust in mission-critical applications. **Complex & Time-Consuming Data Preparation**: Up to 50% of GenAI development time is consumed by preparing unstructured enterprise data for Retrieval-Augmented Generation (RAG) pipelines, delaying time-to-market. **Operationalizing at Scale:** Moving from Proof-of-Concept (PoC) to production-ready GenAI solutions is a significant hurdle due to integration complexities, scalability demands, and lack of robust deployment frameworks. **Architectural Resilience & Security:** Designing AI systems that are secure, compliant (e.g., SOC2 Type II), and resilient against model failures or provider lock-in requires sophisticated architectural patterns and robust governance. **Vendor Lock-in & Model Agnosticism:** Enterprises need flexibility to integrate with diverse LLMs and cloud infrastructures without being tied to a single provider." ## Vertesia: A Robust Platform for Enterprise AI Vertesia provides the foundational capabilities and architectural patterns necessary for building reliable, scalable, and secure generative AI applications and agents. ![Architecture](https://vertesiahq.com/hubfs/Vertesia-Platform-Architecture-Diagram.svg) **Content Preparation:** Adaptive processing prepares each document for its actual use: [Intelligent Document Processing](/content/grounded-extraction) extracts cited and verified business data, [Markdown Conversion](/semantic/getting-started) preserves document structure for reading and RAG, and configurable intake policies generate canonical text, transcripts, and embeddings. Processing adapts per content type so downstream agents and systems receive accurate, contextualized data. **Virtualized LLMs:** Resilient & Optimized Inference: Connect to and orchestrate workloads across multiple LLM providers and models (AWS, GCP, Azure, OpenAI, etc.). Our virtualized LLM layer provides dynamic failover for continuous uptime, intelligent load balancing for cost/performance optimization, and continuous fine-tuning capabilities. **Durable AI Orchestration:**: Design and orchestrate long-running AI agents and workflows, from minutes to days. Our platform preserves the state of generative processes and agentic decisions, ensuring reliable resumption after system failures or network interruptions. This capability is critical for maintaining consistency, preventing data loss, and enabling robust, extended AI tasks in production environments. **Enterprise-Grade Security & Governance by Design:** Built with SOC2 Type II compliance, flexible data residency options, bias mitigation tools, and end-to-end auditability. Deploy on-premises, in your private cloud, or via our multi-cloud SaaS for stringent security and regulatory adherence. **Simplified and Accelerated Development:** We abstract away the complexities of infrastructure management, letting you zero in on what matters: designing and executing production-ready Agentic AI workflows. With Vertesia, you'll use straightforward code and configuration to build sophisticated AI pipelines, dramatically accelerating your development cycle. Focus on the logic and intelligence of your agents, not on provisioning services or managing dependencies. --- ## Configuration Source: https://docs.vertesiahq.com/agent-runner/configuration Markdown: https://docs.vertesiahq.com/llms/agent-runner/configuration.md In the previous section, we explored the execution of the `Multipurpose Agent`. This section will guide you through the process of configuring a new AI Agent within Vertesia Studio. ## Agents Utilizing interactions offers a straightforward and efficient method for creating specialized agents. We will revisit the example from the "Getting Started" section to configure an agent whose sole purpose is to generate lease agreements. ### Prompt Creation Before proceeding with interaction configuration, it is essential to create a prompt. 1. Navigate to the Prompts section in Vertesia Studio. 2. Create a new prompt and name it `Agent Lease Agreement`. 3. Configure the prompt parameters by copying and pasting the following JSON structure into the JSON editor: ```json { "type": "object", "properties": { "agreements": { "type": "array", "items": { "type": "string" }, "editor": "textarea", "format": "textarea" }, "collectionName": { "type": "string" }, "documentType": { "type": "string" } }, "required": [ "agreements", "collectionName", "documentType" ] } ``` 4. Copy the following prompt content: ```js return ` Generate ${agreements.length} lease agreement documents for commercial office space, each tailored to different specifications: ${agreements.map((agreement,index) => `${index+1}. ${agreement}`).join('\n')} A lease agreement document must have the following metadata properties: - space size in square feet - term length - monthly cost - deposit amount - state - city - address - property manager If a document type named ${documentType} doesn’t already exit, create it with properties listed above Subsequently, these documents should be organized and stored within a collection named ${collectionName}". Create a collection only if doesn't already exists ` ``` 5. Finally, to validate the prompt configuration, copy the provided test data: ```json { "agreements": [ "A basic, cost-effective option suitable for approximately eight workstations.", "A mid-range option accommodating approximately twenty workstations, three meeting rooms, and two individual phone booths.", "A premium option located on the fortieth floor or above in an office building, designed to accommodate approximately fifty workstations, ten meeting rooms, and ten individual phone booths." ], "collectionName": "Commercial Lease Agreements", "documentType": "CommercialLeaseAgreement" } ``` ### Interaction Configuration With the prompt successfully configured, we can now proceed to set up the interaction. 1. Go to the Interactions section in Vertesia Studio. 2. Create a new interaction and assign it the name `Lease Agreement Agent`. 3. In the "Prompts" tab, you will add two distinct prompts. First, incorporate the pre-configured `Agent Basis` system prompt, which furnishes the agent with general operational guidelines. Subsequently, add the `Agent Lease Agreement` prompt that you configured earlier. ![Interaction Agent Prompts](/agents/interaction_prompts.png) 4. Navigate to the **Agent Runner** tab to configure the agent-specific settings: ![Agent Runner Configuration Tab](/agents/interaction_agent_runner_tab.png) In this tab, you can configure the following settings: - **Executable as Agent**: Check this option to make the interaction available in the Agent Runner selection dialog. When enabled, this interaction will appear in the list of available agents. - **Available as Sub-agent (Tool)**: Check this option to make the interaction available as a tool that other agents can call during execution. - **Default Tools**: Specify a list of default tools that this agent should have access to during execution. These tools can be builtin tools, custom tools, remote mcp server tools or other interactions tagged or configured as tools. Note that this setting can be overridden at execution time. - **Default Content Scope (Collection)**: Optionally select a collection to limit the agent's access to content within that specific collection. When set, the agent can only retrieve and search documents from the specified collection, providing better control over the data the agent can access. This setting can also be changed at execution time. ***Enabling "Executable as Agent" is crucial for the interaction to become discoverable and available within the Agent Runner.*** ### Agent Runner Execution Finally, return to the Agent Runner via the left-hand navigation menu. You can now select the `Agent Lease Agreement` interaction, populate the necessary parameters, and execute your first specialized agent. ![Custom Agent Run](/agents/custom_agent_run.png) ## Further Exploration This section has demonstrated how to configure and execute a specialized agent within Vertesia Studio. In the upcoming section, we will delve into initiating an agent using the Vertesia TypeScript SDK. --- ## Custom Tools Source: https://docs.vertesiahq.com/agent-runner/custom-tools Markdown: https://docs.vertesiahq.com/llms/agent-runner/custom-tools.md Vertesia supports **custom tools** to let you integrate your own logic or connect to external data sources beyond the built-in tools it provides. These tools can be written in any programming language and exposed via a RESTful HTTP interface. Once registered, Vertesia acts as a **broker between the LLM and your custom tools**. When the LLM issues a `tool_use` message, Vertesia identifies whether the requested tool is built-in or custom. If it's a custom tool, Vertesia sends a `POST` request to your tool server, waits for the response, and then passes the result back to the LLM. Your tool server must expose an endpoint that handles both `GET` and `POST` HTTP methods: - `GET` is used to **discover the tools** exposed by your server. - `POST` is used to **invoke a tool** with user-provided input when requested by the LLM. ## Authentication The `POST` request includes a **Vertesia-signed JWT** that contains metadata such as the user ID, roles, project, and organization. Your tool server must validate this token using Vertesia's public key, which is available via a JWKS endpoint: `{vertesia_server}/api/v1/.well-known/jwks` You can find the `{vertesia_server}` location in the token property `endpoints.studio`. To find the correct signing key, use the `kid` (key ID) field from the JWT header to match it with a key in the JWKS. --- ## API Specification ### `GET /path/to/tools/endpoint` This endpoint returns the list of tools available on the server. No authorization is required for this request. The response must be a JSON object describing the tool server and its available tools. Here's the TypeScript interface for the expected response: ```ts interface GetToolsResponse { /** * The URL of the tool server (same as the URL where this response is served) */ src: string; /** * A human-readable title for this tool server */ title: string; /** * A short description of the tool server */ description: string; /** * The list of tools exposed by this server */ tools: { /** * The name of the tool (used in tool_use messages) */ name: string; /** * A short description of what the tool does */ description: string; /** * A JSON Schema describing the expected input for the tool */ input_schema: JSONSchema; }[]; } ``` Where JSONSchema is a standard JSON Schema definition of the tool's input. **Example Response:** ```json { "src": "http://localhost:5173/api/test", "title": "Development Tools", "description": "A collection of test tools for development purposes", "tools": [ { "name": "weather", "description": "Get the current weather for a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The location to get the weather for, e.g., 'New York, NY'." } }, "required": [ "location" ] } } ] } ``` ### `POST /path/to/tools/endpoint` This endpoint is called by Vertesia when the LLM requests the execution of a specific tool. The request includes the tool name, input arguments, and a signed JWT in the `Authorization` header. Your server must verify the JWT before executing the tool logic. ### Request Headers ```http Authorization: Bearer Content-Type: application/json ``` ### Request Body The request body contains information about the tool being used and optional metadata about the execution context. ```ts interface ToolExecutionRequest { /** * Contains the name of the tool to execute and the input arguments. */ tool_use: ToolUse; /** * Optional metadata related to the current execution context. */ metadata?: Record; } interface ToolUse { /** * The unique ID of this tool use request (used for traceability). */ id: string; /** * The name of the tool to execute (must match the name provided in the GET response). */ tool_name: string; /** * The arguments to pass to the tool (must match the tool's input_schema). */ tool_input: unknown; } ``` ### Successful Response On success, your tool server must return a JSON response that includes the tool use ID and the result of the tool execution. ```ts interface ToolExecutionResponse { /** * The tool use id of the tool use request. For traceability. */ tool_use_id: string; /** * The tool result as a string (can be a serialized JSON object). */ content: string; /** * Optional file URLs to attach to the response. Useful for sending images to the LLM. */ files?: string[]; /** * Metadata can be used to return more info on the tool execution like stats or user messages. */ metadata?: Record } ``` ### Error Response If an error occurs during execution, your server must return a non-2xx HTTP status code and a JSON body describing the error. ```ts interface ToolExecutionResponseError { /** * The tool use ID of the request (for traceability). */ tool_use_id: string; /** * The HTTP status code. */ status: number; /** * A short error message. */ error: string; /** * Optional additional details about the error. */ data?: Record; } ``` ### Response Headers Your tool server should include standard HTTP headers in all responses. the `Content-Type` should be set on `application/json`. --- ## Streaming Large Tool Outputs to Artifacts Some tool executions can produce large textual outputs (for example, long reports, detailed logs, or large JSON payloads). To keep responses manageable for the model while still preserving full results, tools can accept an optional `output_artifact` parameter in their input schema. When present, Vertesia will: - Store the full tool output as an artifact in the agent workspace (for example, under a `files/` path). - Return a compact response that includes a short preview and the `artifact_path` pointing to the full content. The agent can then: - Use the artifact tools (`read_artifact`, `list_artifacts`, `grep_artifacts`, `patch_artifact`) to inspect or refine the result. - Use `execute_shell` to run follow-up processing over the artifact inside the Daytona sandbox. This pattern is particularly useful for remote tools and skills that generate large analysis reports or intermediate datasets that would otherwise exceed the model context. --- ## Building a Custom Tool Server ### Quick Start with TypeScript The fastest way to build a custom tool server is using the `@vertesia/create-plugin` scaffold: ```bash npm init @vertesia/plugin@latest ``` The generated project is a unified plugin with both a Hono tool server and a React UI, including example tools and skills. The comprehensive README covers creating resources, local development, building, and deployment. ### Manual Setup If you prefer to build from scratch, use the [`@vertesia/tools-sdk`](https://www.npmjs.com/package/@vertesia/tools-sdk) package directly. The SDK implements the entire protocol for you, including: - Handling GET and POST endpoints - JWT verification and validation against Vertesia's JWKS - Input schema validation - Tool routing and execution handling Using the SDK helps you focus on writing tool logic instead of boilerplate. ## Deploying to Vercel [Vercel](https://vercel.com) is the easiest way to deploy your tool server — its generous free tier is more than enough for development and small-scale production. The generated project includes a `vercel.json` and `api/index.js` serverless adapter. Deploy with: ```bash npm i -g vercel vercel --prod ``` Static files (UI builds) are served from `dist/`, and API requests are routed through the serverless function. After deploying, note the production URL (e.g. `https://my-tools.vercel.app`). **Important**: Disable deployment protection in Vercel project settings so Vertesia can reach your tool server endpoint. ## Registering Custom Tools To make your custom tools available to Vertesia Agent Runner, register your tool server as a Vertesia **Application**. Create a `manifest.json` with your deployed endpoint: ```json { "name": "my-tools", "title": "My Tools", "publisher": "your-org", "visibility": "private", "status": "beta", "endpoint": "https://my-tools.vercel.app/api" } ``` Then use the CLI to create and install it in your project: ```bash vertesia apps create --install -f manifest.json ``` The `endpoint` URL points to your tool server. Vertesia will call `GET` on it to discover available tools and `POST` to execute them. Once registered, your custom tools are available to agents just like built-in ones. For more details on applications, refer to the [Applications](/apps/overview) section. --- ## Email Integration Source: https://docs.vertesiahq.com/agent-runner/email-integration Markdown: https://docs.vertesiahq.com/llms/agent-runner/email-integration.md Vertesia agents support bidirectional email communication, enabling users to interact with agents through their email client. This guide covers the three main email interaction patterns. ## Overview Email integration enables three distinct interaction patterns: | Pattern | Trigger | Description | |---------|---------|-------------| | **Email-started workflow** | User sends email to agent | User initiates a new agent session by sending an email to a project address | | **Agent response email** | Agent completes or waits for input | Agent sends results or questions via email; user can reply to continue | | **Ask user via email** | Agent uses `ask_user` tool | Agent asks questions via email when the user channel is set to email | All three patterns use a reply-to address format that routes responses back to the correct workflow run. ## Prerequisites Email integration is configured in **Project Settings > Integrations > Resend Email**. ### Required Settings | Setting | Description | |---------|-------------| | `API Key` | Your [Resend](https://resend.com) API key for sending emails | | `Inbound Domain` | Domain for sending/receiving email (e.g., `mail.yourcompany.com`) | | `Webhook Secret` | Resend webhook signing secret for verifying inbound emails | ### Security Settings (Optional) | Setting | Default | Description | |---------|---------|-------------| | `Require Project Access` | Enabled | Only users with project access can start agents via email | | `Require Email Authentication` | Enabled | Inbound emails must pass DKIM/SPF authentication | | `Allowed Sender Domains` | Empty (all) | Whitelist of allowed sender email domains | ### DNS Configuration Your inbound domain requires: 1. **MX records** pointing to Resend's inbound servers 2. **SPF and DKIM records** for email authentication 3. **Webhook configuration** in Resend pointing to `https://api.us1.vertesia.io/webhooks/resend` for the US1 region, or similar regional URL ## Pattern 1: Email-Started Workflows Users can start new agent sessions by sending an email to a project-specific address. This enables email-first workflows where users never need to access another UI. ### Email Address Format ``` {project_namespace}+{agent_execution_endpoint}@{resend_integration_email_domain} ``` For example: - `acme+review-contract@mail.vertesia.io` - Start the `review-contract` agent in the `acme` project - `myproject+analyst@mail.vertesia.io` - Start the `analyst` agent in the `myproject` project ### Access Control When a user sends an email to start a workflow, the following checks are performed: 1. **Email authentication** (if enabled): DKIM and SPF are verified 2. **Sender domain whitelist**: If `Allowed Sender Domains` is configured, the sender must match 3. **Project access** (if enabled): The sender's email must belong to a user with access to the project 4. **Interaction access**: The interaction must exist and be configured as an agent ### Interaction Data Fields When a workflow starts via email, the interaction receives the following data fields that can be used in the prompt template: | Field | Type | Description | |-------|------|-------------| | `${message}` | string | The email body (plain text preferred, falls back to HTML) | | `${email.from}` | string | Sender email address | | `${email.subject}` | string | Email subject line | | `${email.source}` | string | Always `"email"` | | `${email.message_id}` | string | Unique email message ID | | `${email.has_attachments}` | boolean | Whether the email has attachments | | `${attachments}` | array | Uploaded attachment metadata (populated async) | **Example interaction prompt:** ``` # Email Task ## Persona You are an assistant that processes requests received via email. ## Email Content **Email from:** ${email.from} **Subject:** ${email.subject} **Request:** {{message}} ## Task - Please analyze this request and respond appropriately. ``` ### How It Works 1. User sends email to `{namespace}+{agent}@{domain}` 2. Resend webhook receives the email and sends it to Vertesia 3. System verifies sender access and email authentication (based on configuration) 4. A new workflow starts with: - Initial message from email body - Email channel automatically configured in `user_channels` (with sender's email as `to_email` and thread info) - Email metadata (subject, from, attachments) available to the agent 5. Agent processes the request and can use `ask_user` to ask follow-up questions via email 6. User receives responses in their email client and can reply to continue 7. **When the workflow completes, the final result is automatically sent to the user via email** ### Attachments Email attachments are automatically: 1. Downloaded from Resend 2. Uploaded to the workflow's artifact storage 3. Made available to the agent for processing ## Pattern 2: Agent Response Emails When a workflow has an email channel configured in `user_channels`, the agent automatically sends emails to the user: - **Completion emails**: When a non-interactive workflow finishes - **Response emails**: When an interactive workflow produces output and waits for input - **Ask user emails**: When the agent needs to ask questions ### How It Works 1. Workflow runs with email channel in `user_channels` (automatic for email-started workflows) 2. Agent processes the request and produces output 3. System sends email with results and a reply-to address: `r+{routeKey}@{inbound_domain}` 4. When user replies, the email is received by the inbound domain 5. Resend sends a webhook to Vertesia 6. The webhook extracts the run ID and sends a `UserInput` signal to the workflow 7. The workflow continues with the user's reply as input ### Email Threading All emails in a conversation maintain proper threading: - Subject line preserves `Re: Original Subject` - `In-Reply-To` and `References` headers enable proper threading in email clients - Users see all messages in a single thread ## Pattern 3: Ask User via Email When the workflow has an email channel configured in `user_channels`, the `ask_user` tool sends questions via email in addition to displaying them in the UI. ### Configuration Configure `user_channels` when starting the agent run, either via the [Agent Runs API](/api/openapi#tag/agent-runs) or as workflow variables: ```typescript const run = await client.store.agents.start({ interaction: "my-agent", interactive: true, user_channels: [ { type: "email", to_email: "user@example.com" } ] }); ``` Or equivalently as workflow variables (for scheduled or rule-triggered workflows): ```json { "vars": { "interaction": "my-agent", "user_channels": [ { "type": "email", "to_email": "user@example.com" } ] } } ``` The email channel supports email threading with optional fields: - `thread_subject`: Subject for the email thread (without "Re:" prefix) - `in_reply_to`: Message ID for the In-Reply-To header - `references`: Array of message IDs for the References header ### How It Works 1. Agent calls `ask_user` with questions 2. System detects email channel in `user_channels` and sends an email to the configured recipient 3. The email contains the questions with context from the conversation 4. User replies to the email with their answers 5. Reply is routed back to the workflow and the agent continues ### Email Formulation When sending ask_user questions via email, the system uses an LLM to formulate a well-contextualized email that includes: - Recent conversation context - The current task being worked on - The questions being asked - Instructions for replying ## Automatic Completion Email When an email channel is configured in `user_channels` (automatically set for email-started workflows), the system will automatically send the workflow result to the user via email when the workflow completes. This ensures users who interact via email always receive their results without needing UI access. The completion email: - Includes the full workflow output - Maintains email threading if part of an existing conversation - Uses the same reply-to address format, allowing users to continue the conversation ## Reply Address Format All email flows use a consistent reply-to address format: ``` r+{routeKey}@{inbound_domain} ``` Where `runId` is the Temporal workflow run ID with dashes removed (to comply with RFC 5321's 64-character local-part limit). When a reply is received: 1. The run ID is extracted from the address 2. Temporal is queried to find the workflow 3. Project and account IDs are retrieved from search attributes 4. The Resend integration settings are used for webhook verification 5. The email content is sent as a `UserInput` signal to the workflow ## Best Practices ### For Email-Started Workflows - Modify the example prompt to specialize agents - Handle email attachments appropriately in your agent logic - Consider user/agent access to content and tools - Consider all security recommendations ### Security Recommendations - **Keep `Require Email Authentication` enabled** to prevent email spoofing - **Keep `Require Project Access` enabled** unless you specifically need public access - Use `Allowed Sender Domains` to restrict access to specific organizations - Regularly rotate your Resend API key and webhook secret ## Troubleshooting ### Emails Not Being Received 1. Verify MX records are correctly configured 2. Check Resend webhook is active and pointing to the correct URL 3. Verify `Webhook Secret` matches between Resend and your Vertesia configuration ### Replies Not Reaching Workflow 1. Check the workflow is still running (not completed or timed out) 2. Verify the reply-to address format is correct 3. Check Resend webhook logs for delivery status ### Email-Started Workflows Failing 1. If `Require Project Access` is enabled, verify the sender has project access in Vertesia 2. If `Require Email Authentication` is enabled, check DKIM/SPF authentication is passing 3. Verify the interaction exists and is configured as an agent 4. Check the Resend integration is enabled in Project Settings > Integrations --- ## Agent Runner - Getting Started Source: https://docs.vertesiahq.com/agent-runner/getting-started Markdown: https://docs.vertesiahq.com/llms/agent-runner/getting-started.md This guide provides a comprehensive, step-by-step walkthrough for executing your initial AI Agent within Vertesia Studio. Vertesia Studio offers an intuitive and efficient environment for experimenting with AI Agents. Users can easily select an interaction, define its parameters, and initiate agent execution. To maintain simplicity, this documentation utilizes basic examples that do not necessitate external configurations or third-party dependencies. ## Prerequisites Agents in Vertesia are designed to accept an **Interaction** as input. If you are unfamiliar with the concept of interactions, please refer to the dedicated [documentation](/studio/interactions). ## Generic Agent Upon project creation, Vertesia Studio provides a sample configuration that includes a pre-configured generic agent interaction, dubbed `Multipurpose Agent` This interaction can be located in Interactions. ## Run an Agent To initiate an agent run, navigate to Agent Runner, and select the `Multipurpose Agent` interaction. The left panel of the Agent Runner interface presents several configurable parameters for agent execution: | Parameter | Description | Default Value | |----------------|---------------------------------------------|--------------------| | Interactive | When set to `true`, the agent will dynamically respond to messages during its execution and remain in a standby state for a period of 7 days. Conversely, if set to `false`, the agent will only respond to messages when a tool explicitly requests user input and will terminate upon task completion. | True | | Environment | Specifies the Vertesia environment utilized for Large Language Model (LLM) inference. | Interaction default environment | | Model | Defines the AI model to be employed by the agent. Only models that support tool integration are selectable. | Interaction default model | | Tools | Comma separated list of tools made available to the agent during its execution. Prefix operators can be used to add or remove some tools: **+** prefix to add a tool **-** prefix to remove a tool Examples: `-web_search` means all the built-in tools except `web_search` `-web_search,+my_custom_tool` means all the built-in tools plus `my_custom_tool` and without `web_search` | empty (all [built-in tools](./tools)) | The `Environment` and `Model` parameters should be prefilled with the `Multipurpose Agent` default configuration. Below these agent-specific parameters, a form for interaction parameters is displayed. For the `Multipurpose Agent`, this form contains a single field labeled `task`. ![Agent Runner Input Form](/agents/agent_runner_input_form.png) Let's proceed by submitting the following task to the agent: ```text Generate three lease agreement documents for commercial office space, each tailored to different specifications: - A basic, cost-effective option suitable for approximately eight workstations. - A mid-range option accommodating approximately twenty workstations, three meeting rooms, and two individual phone booths. - A premium option located on the fortieth floor or above in an office building, designed to accommodate approximately fifty workstations, ten meeting rooms, and ten individual phone booths. A lease agreement document must have the following metadata properties: - space size in square feet - term length - monthly cost - deposit amount - state - city - address - property manager If a document type named “Lease Agreement” doesn’t already exit, create it with properties listed above Subsequently, these documents should be organized and stored within a collection named "Office Leases." ``` This task extends beyond simple content generation via LLM inference. It requires the agent to perform a sequence of operations without a pre-configured structured workflow. Specifically, the agent will: - generate random content using the metadata structure provided in the prompt - create a document type in vertesia - create 3 documents - create a collection - group the documents in the collection Once the task is entered, click **Start** to initiate agent execution. The agent's progress will be dynamically displayed in the chat interface on the right side of the screen. ![Agent Conversation](/agents/agent_conversation.png) By default, only the most critical messages are shown in the conversation log. To view the complete conversation history, click on `Details`. Furthermore, the agent is configured to generate and update a plan, reflecting its progress on the assigned task. During execution, you can click on the provided links within the messages to access the documents created by the agent. To return to the agent conversation, simply use your browser's back button. ## Send Messages If the agent run was initiated in interactive mode, an input box will be visible at the bottom of the chat history. This allows you to send real-time messages to the agent, potentially altering its ongoing execution. If the run was not started in interactive mode, the input box will only appear when the agent explicitly awaits user input. For instance, while the agent is completing its initial task or after its completion, you can send the following message: ```text Create a spreadsheet with the agreement properties and add it to the collection ``` ![Send Message](/agents/agent_send_messages.png) Behind the scenes, the agent now uses skills plus the `execute_shell` and artifact tools to implement spreadsheet creation and analysis, rather than relying on dedicated spreadsheet-specific built-in tools. ## Going Further Having successfully executed a generic agent in Vertesia Studio, we encourage you to experiment with diverse tasks and tools relevant to your specific use cases. A comprehensive list of all built-in tools is available [here](./tools). As an example, consider utilizing the [web_search](./tools#web-and-external-tools) tool to enable the agent to retrieve information from the internet as part of a task. (Note: This activity leverages [Serper](https://serper.dev/) and requires the configuration of an API key.) The subsequent section of this documentation will detail the process of configuring a new agent from scratch. --- ## MCP Tools Source: https://docs.vertesiahq.com/agent-runner/mcp-tools Markdown: https://docs.vertesiahq.com/llms/agent-runner/mcp-tools.md Vertesia agents can use tools provided by external [MCP (Model Context Protocol)](https://modelcontextprotocol.io) servers. MCP servers are configured in [application manifests](/apps/tool-collections) and their tools become available to agents alongside built-in and custom tools. ## How It Works 1. An application manifest declares one or more MCP servers in its `tool_collections` array. 2. When the application is installed on a project, the MCP tools are discovered and made available to agents. 3. Tool names are prefixed with the configured `namespace` to avoid collisions (e.g., a tool named `search` with namespace `crm` becomes `crm_search`). 4. If an MCP server requires OAuth, Vertesia manages that connection through the **remote MCP connections** flow in Studio Server. This is the control-plane API used to discover OAuth metadata, start authorization, exchange codes, check status, and disconnect remote MCP accounts. ## OAuth Authentication Many MCP servers require OAuth 2.0 authentication. Each user must connect their account before an agent can use the MCP tools on their behalf. ### Connecting Users connect from the **new agent form** in the Agent Runner interface. The form displays connect actions for any MCP tools that require authentication. Users must connect before starting the agent. Behind the scenes, Vertesia treats these as **remote MCP connections**: - Vertesia looks up the installed app and target MCP collection. - If the collection is bound to a project-level OAuth Provider, Vertesia uses that provider configuration. - Otherwise, Vertesia can fall back to OAuth discovery on the remote MCP server itself. - Tokens are stored securely and refreshed automatically. If a refresh token expires or is revoked, the user will need to reconnect. ### Connection Modes Remote MCP collections can authenticate in two main ways: - **OAuth Provider-backed**: the MCP collection is linked to a project-level [OAuth Provider](/api/openapi#tag/oauth-providers). This is the preferred path when you want explicit control over client ID, secret, scopes, and lifecycle. - **Remote server discovery**: if no provider is configured, Vertesia can discover OAuth metadata from the remote MCP server and follow that server's authorization flow. For `client_credentials` providers, Vertesia can connect server-side without requiring an interactive browser flow. ## Configuration MCP servers are configured in application manifests, not in the Agent Runner directly. See [MCP Servers](/apps/tool-collections) for the full configuration reference including: - Required and optional fields - OAuth setup with project-level OAuth Providers, embedded `oauth_config`, and remote server discovery - Namespace configuration - Complete manifest examples --- ## Agent Message System Source: https://docs.vertesiahq.com/agent-runner/messaging Markdown: https://docs.vertesiahq.com/llms/agent-runner/messaging.md The agent message system is Vertesia's real-time communication layer between agent workflows and client applications. It handles message creation, delivery, streaming, and rendering through a well-defined pipeline. **Use the `ModernAgentConversation` component** for building agent UIs. It handles streaming, reconnection, plan visualization, file uploads, and message rendering out of the box. See [Embedding the Conversation UI](#embedding-the-conversation-ui) below. ## Architecture Overview Messages flow through the following pipeline: ``` User Input --> Signal (UserInput) --> Temporal Workflow | Agent/Tool Execution | postUpdateMessage activity | POST /runs/:runId/updates | Redis (list + pub/sub) | +--------------------------------------+ | Phase 1: GET /updates (gzip) | | Phase 2: SSE /stream (real-time) | +--------------------------------------+ | ModernAgentConversation UI ``` **Key participants:** - **Temporal Workflows** orchestrate agent execution and tool calls - **Redis** stores message history (up to 1000 messages, 90-day TTL) and delivers real-time updates via pub/sub - **zeno-server** exposes REST and SSE endpoints for message retrieval and streaming - **`@vertesia/client`** provides the `WorkflowsApi` that handles both historical fetch and real-time SSE - **`@vertesia/ui`** provides `ModernAgentConversation`, the recommended UI component ## Message Types Every agent message has a `type` field from the `AgentMessageType` enum (`@vertesia/common`): | Type | Value | Description | |------|-------|-------------| | `SYSTEM` | 0 | Internal system messages (e.g., file processing status) | | `THOUGHT` | 1 | Agent reasoning and intermediate thinking | | `PLAN` | 2 | Plan proposals with structured tasks | | `UPDATE` | 3 | Progress updates during execution | | `COMPLETE` | 4 | Task or workstream completion | | `WARNING` | 5 | Non-fatal warnings | | `ERROR` | 6 | Error messages | | `ANSWER` | 7 | Final answers to the user | | `QUESTION` | 8 | User messages (displayed as questions in the conversation) | | `REQUEST_INPUT` | 9 | Agent requests user input, optionally with structured UX options | | `IDLE` | 10 | Agent is idle, waiting for input | | `TERMINATED` | 11 | Agent has been terminated | | `STREAMING_CHUNK` | 12 | Real-time LLM token streaming | | `BATCH_PROGRESS` | 13 | Progress updates for batch tool executions | ## Message Formats The system uses two message formats for different purposes: - **`CompactMessage`** -- the **wire format**, optimized for bandwidth (~85% smaller). Used between server and client over SSE/WebSocket. - **`AgentMessage`** -- the **UI format**, with readable field names. Used by UI components and application code. The client SDK converts wire messages to `AgentMessage` automatically, so application code always works with the friendlier format. ### CompactMessage (wire format) Used over the wire between server and client. You should not need to work with this directly. ```ts interface CompactMessage { t: AgentMessageType; // Message type m?: string; // Message content w?: string; // Workstream ID (omitted when "main") d?: unknown; // Type-specific details f?: 0 | 1; // Is final chunk (streaming only) ts?: number; // Timestamp i?: string; // Activity ID (for deduplication) } ``` ### AgentMessage (UI format) The format used by UI components and application code. This is what you receive in `streamMessages` callbacks and what `ModernAgentConversation` works with internally. ```ts interface AgentMessage { timestamp: number; workflow_run_id: string; type: AgentMessageType; message: string; details?: any; workstream_id?: string; } ``` ### Conversion Utilities The `@vertesia/common` package provides utilities for working with both formats: - `parseMessage(data)` -- Accepts string or object, returns `CompactMessage` regardless of input format - `toAgentMessage(compact, runId)` -- Converts wire format to UI format (done automatically by the client SDK) - `toCompactMessage(msg)` -- Converts UI format to wire format - `createCompactMessage(type, options)` -- Convenience constructor for server-side code ## Real-Time Streaming The client uses a **two-phase streaming strategy** for optimal performance: 1. **Phase 1 -- Historical fetch:** `GET /runs/{workflowId}/{runId}/updates` returns all past messages as gzip-compressed JSON. This is efficient because HTTP responses support compression while SSE streams do not. 2. **Phase 2 -- Real-time SSE:** `GET /runs/{workflowId}/{runId}/stream?skipHistory=true` opens an SSE connection for live updates only. The `since` parameter ensures no messages are lost between phases. **Reconnection** is handled automatically with exponential backoff (1s base, 30s max, 10% jitter, up to 10 attempts). ### LLM Token Streaming During LLM generation, tokens are batched into `STREAMING_CHUNK` messages at **16ms intervals or 200 characters** before being published to Redis. When the final `THOUGHT` or `ANSWER` message arrives with a matching `activity_id`, streaming chunks are replaced by the final message to avoid duplication. ## Workstreams Messages carry a `workstream_id` (defaults to `"main"`). This enables parallel agent execution where multiple workstreams run concurrently. The conversation only closes when the **main** workstream sends `COMPLETE` or `TERMINATED` — other workstreams can complete independently. For a comprehensive guide on the workstream system, see [Workstreams](/agent-runner/workstreams). ### Non-Blocking Execution Model Workstreams are **non-blocking**: the parent agent launches a sub-agent via `launch_workstream` and immediately receives a `launch_id`. The child runs in the background as a separate Temporal workflow, communicating with the parent through signals. The parent can continue working — launching more workstreams, using other tools, or waiting for results. When a child completes, the parent receives a system message with the result summary. ### Workstream Lifecycle Each workstream moves through the following states: | State | Description | |-------|-------------| | `running` | Sub-agent is actively executing | | `canceling` | Termination requested, awaiting graceful shutdown (60s grace period) | | `completed` | Finished successfully with a summary | | `failed` | Encountered an unrecoverable error | | `timeout` | Exceeded its deadline and was automatically cancelled | | `canceled` | Terminated by the parent via `terminate_workstream` | ### Progress Reporting Running workstreams report progress through phases: | Phase | Description | |-------|-------------| | `planning` | Sub-agent is analyzing the task and forming a plan | | `executing_tool` | Currently running a tool (tool name included) | | `synthesizing` | Combining results and forming a response | | `blocked` | Waiting for input or a dependency | | `done` | Finished processing | Progress updates include the current iteration number, a message from the model, and the percentage of deadline elapsed. ### Deadline Management - **Default deadline:** 5 minutes - **Maximum deadline:** 30 minutes (configurable via `deadline_seconds`) - **Minimum deadline:** 30 seconds - Warnings are sent to the parent at **75%** and **90%** of the deadline - At **80%** of the deadline, the child automatically receives a wrap-up steering directive - If the deadline is exceeded, the workstream is terminated and the parent receives a timeout notification ### Artifact Merging By default (`merge_child_artifacts: true`), artifacts created by a child workstream in its `out/` and `files/` directories are automatically merged back to the parent's artifact space. Artifacts are namespaced by child run ID to avoid conflicts (e.g., `files/{child_id}/output.json`). ### Interactive Workstreams When launched with `interactive: true`, a workstream enters interactive mode: after completing its initial task, it waits for follow-up messages sent via `message_workstream`. This enables multi-turn conversations with specialist sub-agents (used, for example, in the Expert Roundtable pattern). Interactive workstreams **never complete on their own** — the parent must explicitly call `terminate_workstream` to close them. ### Steering The parent can send mid-execution directives to a running workstream via `steer_workstream` without terminating it: | Command | Description | |---------|-------------| | `add_instruction` | Add new instructions to the sub-agent | | `set_priority` | Change the priority or focus of the task | | `add_constraint` | Add constraints to narrow scope | | `pause_requested` | Request the sub-agent to pause | | `resume_requested` | Request the sub-agent to resume | ## Embedding the Conversation UI The `ModernAgentConversation` component from `@vertesia/ui` is the **recommended way** to display agent conversations in your application. It handles all the complexity of message streaming, reconnection, plan visualization, file uploads, and message rendering. Do not implement raw message streaming yourself. The `ModernAgentConversation` component handles streaming, chunk aggregation, deduplication, reconnection, plan extraction, optimistic updates, and many edge cases that are difficult to replicate correctly. ### Basic Usage Display an existing agent conversation: ```tsx import { ModernAgentConversation } from "@vertesia/ui/features"; function MyAgentView({ workflowId, runId }) { return ( ); } ``` ### Starting a New Conversation Provide a `startWorkflow` callback to let users initiate conversations. Use the [Agent Runs API](/api/openapi#tag/agent-runs) (`store.agents.start()`) to create the run: ```tsx import { ModernAgentConversation } from "@vertesia/ui/features"; import { useUserSession } from "@vertesia/ui/session"; function MyAgentView() { const { store } = useUserSession(); const startWorkflow = async (message?: string) => { const run = await store.agents.start({ interaction: "MyAgent", data: { task: message }, interactive: true, }); return { run_id: run.first_workflow_run_id, workflow_id: run.workflow_id, }; }; return ( ); } ``` ### Component Properties An existing workflow run to display. When provided, the component connects to the message stream and renders the conversation. Use `first_workflow_run_id` and `workflow_id` from the AgentRun object. Promise<{ run_id, workflow_id }>"> Callback to start a new workflow. When provided without `run`, the component renders a start view with a message input. The returned IDs are used to connect to the new conversation. Use `store.agents.start()` to create the run. Whether users can send messages to the agent. When `false`, user input is only shown when the agent sends a `REQUEST_INPUT` message. Title displayed in the conversation header. Defaults to the workflow ID. Placeholder text for the message input field. Label for the button that starts a new conversation. An initial message displayed above the start view to give context to the user. void" modifier="optional"> Called when the user clicks the close button. Adjusts layout for modal display (narrower max-width, different close button placement). When `true`, the conversation area uses full width instead of a centered max-width layout. void" modifier="optional"> Callback to reset the workflow. When provided, a reset button is shown in the header. Completely hide the user input area. ### File Upload Support The component supports drag-and-drop and button-based file uploads. Files are uploaded to the workflow's artifact storage and the agent is signaled via `FileUploaded`. ```tsx ``` void" modifier="optional"> Custom handler for file selection. If omitted, the component handles uploads internally using artifact storage. External file upload state to display in the input area. void" modifier="optional"> Called when the user removes an uploaded file. Accepted MIME types / extensions for file uploads. Maximum number of files that can be uploaded simultaneously. Disables the send/start buttons while files are uploading. ### Document Search Integration You can integrate a custom document search UI using a render prop: ```tsx ( )} selectedDocuments={selectedDocs} onRemoveDocument={(docId) => removeDoc(docId)} /> ``` ### Context and Attachments string[]" modifier="optional"> Returns document IDs to include as `store:` references in messages. void" modifier="optional"> Called after attachments are sent, allowing you to clear the attachment list. Record" modifier="optional"> Returns additional metadata to include in every user signal. Useful for passing context like entity IDs. ### Fusion Fragment Support When building data-driven applications, you can provide data for `fusion-fragment` code blocks in agent responses: ```tsx ``` ### Styling Additional Tailwind classes for the input container. Additional Tailwind classes for the input field. ## SDK-Only Integration If you cannot use the React component (e.g., in a Node.js backend or non-React frontend), use the `WorkflowsApi` directly from `@vertesia/client`. See the [SDK page](/agent-runner/sdk) for examples of `streamMessages` and `sendSignal`. The SDK approach requires you to handle message parsing, deduplication, streaming chunk aggregation, and reconnection yourself. Prefer the `ModernAgentConversation` component whenever possible. ## Key Source Files | Component | Package | Path | |-----------|---------|------| | Message types | `@vertesia/common` | `composableai/packages/common/src/store/workflow.ts` | | Conversation state | `@vertesia/common` | `composableai/packages/common/src/store/conversation-state.ts` | | Signals | `@vertesia/common` | `composableai/packages/common/src/store/signals.ts` | | Client streaming API | `@vertesia/client` | `composableai/packages/client/src/store/WorkflowsApi.ts` | | Conversation component | `@vertesia/ui` | `composableai/packages/ui/src/features/agent/chat/ModernAgentConversation.tsx` | --- ## Overview Source: https://docs.vertesiahq.com/agent-runner/overview Markdown: https://docs.vertesiahq.com/llms/agent-runner/overview.md `Agent Runner` is Vertesia's core framework for building powerful AI agents. So, what exactly is an AI agent? Imagine a super-smart system driven by a Large Language Model (LLM). This LLM acts as the agent's brain, making decisions and choosing the right actions. The agent then performs these actions, gets feedback, and uses it to figure out if it needs to do more or if the job's done! It's all about intelligent, self-directed task completion! ![Agent Runner Conversation Overview](/agents/agent-conversation-overview.png) Inside Every Agent: * Large Language Model: The brains of the operation, handling all the logic and decision-making. * Tools: These are the agent's hands and feet, allowing it to interact with the outside world and perform specific tasks. * Agent Executor: This is the conductor, managing the agent's actions and ensuring everything runs smoothly. What makes Agent Runner so powerful? It leverages Vertesia's robust LLM interaction framework and sophisticated workflow engine. This means you can easily configure, run, and orchestrate complex AI agents, seamlessly integrating them into your existing processes. In this documentation, we'll guide you step-by-step through the process: * Getting Started in Studio: You'll learn how to launch and interact with your very first AI agent right within the intuitive Studio environment. * Crafting Specialized Agents: Discover how to configure and fine-tune AI agents for specific, powerful tasks. * Seamless Application Integration: Integrate your custom AI agents directly into your applications using the Vertesia SDK for TypeScript, making them a core part of your solutions * Built-in Tools and Skills: Explore the catalog of built-in tools and reusable skills that agents can use to interact with data, documents, and external systems. To learn more about built-in tools, see the [Built-in Tools](/agent-runner/tools) page. For reusable, code-heavy capabilities exposed as tools (such as data analysis or automation helpers), see [Agent Skills](/agent-runner/skills). --- ## SDK Integration Source: https://docs.vertesiahq.com/agent-runner/sdk Markdown: https://docs.vertesiahq.com/llms/agent-runner/sdk.md In previous sections, we've seen how the **Agent Runner** provides an excellent environment for quickly experimenting with Agents in Studio. To seamlessly integrate Vertesia Agents into your business applications and workflows, Vertesia offers a comprehensive REST API and a robust [TypeScript SDK](/quickstart#vertesia-sdk). This section will guide you through interacting with agents using the TypeScript SDK. No complex configuration is required to get started. ## Client Initialization First, you'll need to get an instance of the Vertesia client. Remember to replace `apiKey` with your actual API key. ```js import { VertesiaClient } from "@vertesia/client"; import { AgentMessageType } from "@vertesia/common"; const vertesia = new VertesiaClient({ site: 'api.vertesia.io', apikey: '', }); ``` ## Initiating Agent Execution Start an agent run using the `store.agents.start()` method. This creates a stable `AgentRun` with its own ID, lifecycle tracking, and artifact storage. ```js const task = ` Generate three lease agreement documents for commercial office space, each tailored to different specifications: - A basic, cost-effective option suitable for approximately eight workstations. - A mid-range option accommodating approximately twenty workstations, three meeting rooms, and two individual phone booths. - A premium option located on the fortieth floor or above in an office building, designed to accommodate approximately fifty workstations, ten meeting rooms, and ten individual phone booths. A lease agreement document must have the following metadata properties: - space size in square feet - term length - monthly cost - deposit amount - state - city - address - property manager If a document type named "Lease Agreement" doesn't already exit, create it with properties listed above Subsequently, these documents should be organized and stored within a collection named "Office Leases." `; const run = await vertesia.store.agents.start({ interaction: "MultipurposeAgent", data: { task: task }, interactive: true, }); console.log(run.id); // stable AgentRun ID ``` ## Streaming Agent Messages You can then stream messages from the agent using the `streamMessages` function. This function uses the AgentRun ID returned in the previous step. ```js await vertesia.store.agents.streamMessages(run.id, (item) => { const { type, message } = item; const date = new Date(item.timestamp); console.log(`${date.toLocaleString()} - ${type}:\n${message}\n`); }); ``` The `streamMessages` function automatically stops when a message of type `AgentMessageType.COMPLETE` is received. This signals that the agent's current task is finished. ## Sending Signals to the Agent Finally, to send a message or "signal" to the agent, use the sendSignal function. ```js await vertesia.store.agents.sendSignal(run.id, "UserInput", { message: "create a spreadsheet with the agreement properties and add it to the collection", }); ``` In the current architecture, agents handle this kind of spreadsheet workflow by orchestrating skills together with the `execute_shell` and artifact tools, rather than using dedicated spreadsheet-specific built-in tools. ## Managing Agent Lifecycle The Agent Runs API also supports lifecycle operations: ```js // Retrieve the current state of an agent run const current = await vertesia.store.agents.retrieve(run.id); console.log(current.status); // 'running' | 'completed' | 'failed' | 'cancelled' // Terminate a running agent await vertesia.store.agents.terminate(run.id, 'No longer needed'); // Restart a completed/failed agent (continues the same conversation) const restarted = await vertesia.store.agents.restart(run.id); // Fork into a new agent run (new ID, loads history from source) const forked = await vertesia.store.agents.fork(run.id); ``` ## Further Exploration - Full [Agent Runs API reference](/api/openapi#tag/agent-runs) - A simple agent cli example is available on [GitHub](https://github.com/vertesia/examples/tree/main/agent-cli) --- ## Skills Source: https://docs.vertesiahq.com/agent-runner/skills Markdown: https://docs.vertesiahq.com/llms/agent-runner/skills.md Skills are reusable, curated capabilities that combine remote tools with ready-to-use code, such as data analysis scripts or automation helpers. They are exposed to agents as tools whose names start with the `learn_` prefix and are executed inside the Daytona sandbox alongside the built-in tools. When an agent uses a skill tool, Vertesia: - Tracks the skill in the conversation state, including its name, source URL, language, and required packages. - Automatically syncs the skill's scripts into the Daytona sandbox under `/home/daytona/skills/{skill_name}/`. - Installs any language and system packages declared by the skill so they are available for subsequent `execute_shell` calls. This makes skills ideal for encapsulating complex logic (for example, spreadsheet analysis or ETL pipelines) without exposing their internal implementation to the model. ## How Skills Work with Agents Skills are discovered and registered as part of the tools available to the agent. From the model's perspective they behave like normal tools: - The tool name has the form `skill__` (for example, `skill_data_analysis_analyze_spreadsheet`). - The tool input schema defines the parameters the model must provide. - The tool response can include both content and artifacts for downstream processing. When a skill tool is executed: 1. The agent invokes the skill by name (for example, `skill_data_analysis_analyze_spreadsheet`). 2. The remote tools infrastructure (see [Custom Tools](/agent-runner/custom-tools)) runs the skill and returns a result. 3. Vertesia records the skill usage in the conversation state and syncs its scripts into the Daytona sandbox. 4. Subsequent `execute_shell` calls can import and reuse those scripts from `/home/daytona/skills/{skill_name}/`, with any declared packages already installed. This pattern lets you keep heavy lifting (for example, Python + pandas data analysis) in versioned skill collections while the LLM focuses on orchestration. ## Typical Skill Use Cases Common categories of skills include: - **Data analysis skills** – Analyze, transform, and summarize structured data or spreadsheets using Python and popular libraries. - **Data platform skills** – Create databases, import data, run SQL queries, and build Vega-Lite dashboards. See the [Data Platform Skills Reference](/data-platform/skills) for details. - **Code and scripting skills** – Execute shell automation or TypeScript scripting tasks that are too complex or specialized to express directly via prompts. - **Document processing skills** – Implement advanced parsing, enrichment, or extraction workflows over documents fetched from the knowledge base. - **Math and utility skills** – Provide robust numerical and unit-conversion helpers beyond what the base model offers reliably. Examples from the built-in skills server include: - `data-analysis/analyze-data`, `etl-pipeline`, `analyze-spreadsheet`, and `create-spreadsheet` for Python + pandas workflows. - `data-analysis/make-chart` for teaching the model how to emit chart JSON specs inside ` ```chart ` code blocks that the UI renders as interactive charts. - `document/zip-archives` for working with zipfiles whose text rendition is an index of contained files, showing how to download the `.zip` into the sandbox, unzip it with `unzip`, and then analyze the extracted contents with other skills. From the agent's perspective, each of these skills is just another tool that can be chained with built-in tools such as `search_documents`, `fetch_document`, `write_artifact`, and `execute_shell`. ## System Skills Vertesia ships with a set of **system skills** — built-in capabilities that come pre-installed on the platform. System skills follow the `learn_*` naming pattern: when an agent calls `learn_`, it receives detailed instructions and unlocks the skill's related tools. System skills are different from custom skills hosted on external tool servers. They are always available and do not require sandbox setup or package installation. ### How System Skills Work 1. The agent sees a `learn_*` tool in its available tools list (e.g., `learn_workstreams`). 2. The agent calls the skill tool when it needs that capability. 3. Vertesia returns detailed instructions and best practices for the skill. 4. The skill's related tools are unlocked and become available for use. This progressive disclosure pattern keeps the agent's initial tool list manageable while making advanced capabilities available on demand. ### Available System Skills | Skill Tool | Title | Description | Unlocked Tools | |------------|-------|-------------|----------------| | `learn_workstreams` | Workstreams | Launch and manage parallel sub-agents that run independently | `launch_workstream`, `check_workstream`, `terminate_workstream`, `steer_workstream`, `message_workstream`, `list_workstreams`, `get_workstream_result` | | `learn_expert_roundtable` | Expert Roundtable | Run multi-perspective debates with specialist sub-agents using different models | `launch_workstream`, `check_workstream`, `steer_workstream`, `message_workstream`, `list_workstreams`, `get_workstream_result`, `terminate_workstream`, `analyze_conversation` | | `learn_agent_scheduling` | Agent Scheduling | Schedule agents for one-time or recurring execution | `schedule_agent`, `schedule_recurring_agent` | | `learn_content_authoring` | Document Authoring | Create, compose, and export documents with PDF/DOCX rendering | `render_markdown`, `render_artifact_officexml`, `preview_artifact_officexml`, `read_artifact_xml`, `edit_artifact_docx`, `inspect_pdf`, `merge_artifacts` | | `learn_docx_editing` | DOCX Editing | Edit DOCX files using structured XPath edits | `read_artifact_xml`, `edit_artifact_docx`, `render_artifact_officexml`, `preview_artifact_officexml`, `inspect_pdf`, `list_artifacts` | | `learn_pptx_editing` | PPTX Editing | Edit PowerPoint PPTX files using structured XPath edits | `read_artifact_xml`, `edit_artifact_pptx`, `render_artifact_officexml`, `preview_artifact_officexml`, `inspect_pdf`, `list_artifacts` | | `learn_document_management` | Document Management | Source-based document creation, updates, revision history, and document merging | `create_document`, `update_document`, `list_revisions`, `diff_revisions`, `merge_documents` | | `learn_conversation_analysis` | Conversation Analysis | Search and analyze past conversations with AI | `search_conversations`, `analyze_conversation` | | `learn_image_analysis` | Image Analysis | Analyze and transform images using ImageMagick | `analyze_image` | | `learn_presentation_authoring` | Presentation Authoring | Create slide decks with markdown, custom SVG slides, and PDF export | `render_slides` | | `learn_send_email` | Send Email | Send emails with markdown formatting and inline images | `send_email` | | `learn_document_search` | Document Search | Advanced search with full-text, vector, and DSL queries | `search_documents` | ## Authoring Custom Skills Skills are hosted by tool servers built with the Vertesia tools SDK and registered as tool collections on applications. The same mechanisms used for [Custom Tools](/agent-runner/custom-tools) apply: - A tool server exposes a set of tools and skills over HTTP or MCP. - Each skill includes metadata about its scripts, language, and required packages. - When the agent uses a skill, Vertesia uses this metadata to sync scripts and install dependencies into the sandbox. For a practical starting point, see the Vertesia tools server examples and the `@vertesia/tools-sdk` documentation used by the platform's own skills. --- ## How Skills Work Source: https://docs.vertesiahq.com/agent-runner/skills-model Markdown: https://docs.vertesiahq.com/llms/agent-runner/skills-model.md Agents with fifty tools available from turn one don't behave well. They forget the rules, misuse the wrong tool, hallucinate arguments. **Skills** are the mechanism that fixes this: a skill is a bundle of *instructions* + a *tool set* that the agent must explicitly opt into. This page explains the skill model: what a skill is, how agents invoke skills, and how skills differ from plain tools. ## The shape Every system skill is a `SystemSkillDefinition`: ```ts interface SystemSkillDefinition { name: string; // e.g. "web_search" title: string; // "Web Search" description: string; // shown in the skill catalog instructions: string | ((ctx) => string); // unlocked on call tools: string[]; // unlocked on call related_tools?: string[]; // complementary, shown in catalog input_schema?: ToolDefinition['input_schema']; } ``` Each skill is surfaced to agents as a tool named `learn_` — so `web_search` becomes `learn_web_search`. The tool's description is `[Skill] `; its input schema is either a generic `{ context: string }` or whatever the skill declares. ## Two phases: unlock, then act Without a skill, an agent sees only its declared tools plus any `learn_*` entries. Domain tools — `web_search_serper`, `fetch_document`, `create_or_update_object_type` — aren't available yet. When the agent calls a `learn_*`: 1. The skill's `tools` list is **unlocked** for the remainder of the conversation. They become callable on subsequent turns. 2. The skill's `instructions` (markdown, often several paragraphs) are returned as the tool result. 3. The agent reads the instructions, then proceeds with the newly-available tools. The forced read matters. Instructions are where the platform encodes "when to use provider A vs B", "which parameter is required", "common pitfalls". Without the unlock, an agent can't stumble into calling a misconfigured tool — it has to ask for the skill first and reads the rules on the way in. This is why you'll often see agent prompts say things like *"if you need to search the web, call `learn_web_search` first"*. It's not a politeness; it's the only path that opens the tools. ## Static vs dynamic instructions Instructions can be a string literal or a function of `SkillContext`: ```ts interface SkillContext { project?: Project; enabledTools: string[]; } ``` Dynamic instructions let a skill adapt to the project — for example, `web_search` can inspect `project.configuration.web_search_providers` and only describe Serper's behavior if Serper is actually configured. The unlocked `tools` list is also typically filtered so the agent doesn't see tools that would fail at runtime. ## `related_tools` A skill's `tools` are the ones **unlocked by calling it**. `related_tools` are tools that complement it but stay hidden — typically rare or heavy tools the skill itself points at conditionally. They show up in the skill catalog (`list_tools`) so an agent can see what's available without unlocking everything at once. ## System skills live in `packages/workflows/src/skills/sys/` Browse the directory for concrete examples. A few worth reading: - **`web-search.ts`** — classic multi-provider skill. Describes three search providers and three fetch providers, returns a comparison table, unlocks all six tools. - **`process-definitions.ts`** — the grammar + authoring rules for process definitions. Unlocks `list_tools`, `list_interactions`, `validate_process_definition`, `create_process_definition`, etc. Used by the Studio Assistant for process authoring. - **`document-search.ts`** — guidance on using `search_documents` in Search Mode vs DSL Mode. - **`artifact-operations.ts`** — reading and writing agent-run artifacts. Registration happens in `packages/workflows/src/skills/sys/index.ts`. ## How to declare which skills an agent gets Skills are tools with a `+` prefix convention, same as any other tool: ```ts agent_runner_options: { is_agent: true, tool_names: [ "+learn_web_search", "+learn_document_management", "+fetch_document", // unlocked already, no learn step ], }, ``` - `+learn_X` **adds** the skill's learn entry to the default tool set. - `learn_X` (no prefix) **replaces** the default set with an explicit list. - `-learn_X` removes a skill the defaults would otherwise include. For process agent nodes, declare skills the same way in `node.tools`: ```json "extract_terms": { "type": "agent", "tools": ["+learn_document_management", "+fetch_document"], ... } ``` ## The skill catalog The `list_tools` builtin returns every tool *and* every skill (annotated as `is_system_skill: true`) that's available in the current project. An agent can call `list_tools` with a grep filter to discover capabilities before unlocking anything. This is how the Studio Assistant browses its own toolkit at runtime. ## Custom skills Custom skills follow the same shape but are authored outside `packages/workflows/src/skills/sys/`. They're typically written as a `SkillDefinition` in the `tools-sdk` pattern (a markdown file with frontmatter plus an `instructions` block). The `toSystemSkill()` helper in `skills/types.ts` converts that shape into the runtime `SystemSkillDefinition`. See the tools-sdk docs for the authoring path. ## When to reach for a skill vs just a tool Make it a tool when: - The tool is always safe to call without context. - One-liner usage; no preambles needed. - It's a primitive the rest of the system composes freely (e.g. `get_current_time`). Make it a skill when: - There's non-trivial guidance — "use A for X, B for Y", pitfalls, required param combinations. - The feature set is broad enough that loading everything at once bloats the tool list and hurts model quality. - You want to enforce a particular ordering ("read the rules before building"). ## When skills misfire Two common failure modes: 1. **Agent never calls the skill.** Usually the prompt didn't mention the skill or didn't explain the forcing function. Fix in the system prompt: state that certain capabilities require `learn_*` first. 2. **Agent calls the skill repeatedly.** Once unlocked, a skill stays unlocked for the conversation — there's no need to re-call. If the agent loops, check that the instructions don't imply re-reading on every use. ## See also - [Studio Assistant](/studio/assistant) — the always-on assistant that is itself skill-driven. - [Authoring Processes](/processes/authoring) — the Studio Assistant's skill-first iteration loop for process authoring. - [Agent Runner — Skills](/agent-runner/skills) — how to list, configure, and invoke skills on an agent run. - [Agent Runner — Built-in Tools](/agent-runner/tools) — the broader tool catalog skills unlock. --- ## Built-in Tools Source: https://docs.vertesiahq.com/agent-runner/tools Markdown: https://docs.vertesiahq.com/llms/agent-runner/tools.md This page documents all the built-in tools available in Vertesia Studio for use with Agents. For reusable, code-centric capabilities that are packaged and exposed as tools (such as spreadsheet analysis or ETL pipelines), see also [Agent Skills](/agent-runner/skills). ## Core Tools Fundamental tools for reasoning, planning, and task organization. These are the essential building blocks for complex agent workflows. ### Think Tool **Name:** `think` A tool for deep thinking and analysis of complex problems step by step. Useful for brainstorming and planning. ### Plan Tool **Name:** `plan` Creates structured, executable plans with tracked progress. ### Update Plan Tool **Name:** `update_plan` Updates multiple tasks in your active plan simultaneously with visual progress tracking. Works in conjunction with the plan tool to maintain live status updates. ## Workstream Tools Non-blocking tools for launching and managing parallel sub-agents (workstreams). Each workstream runs independently in the background while the parent agent continues working. See [Workstreams](/agent-runner/workstreams) for a full conceptual guide. ### Launch Workstream Tool **Name:** `launch_workstream` Launches a dedicated parallel workstream (sub-agent) to independently solve a specific part of a complex problem. Returns immediately with a `launch_id` — the sub-agent runs in the background while you continue working. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | string | Yes | Short identifier for this workstream (ASCII, underscores for spaces) | | `name` | string | Yes | Human-readable name for tracking | | `instruction` | string | Yes | Clear, detailed instructions for what the sub-agent should accomplish | | `allowed_tools` | string[] | Yes | Tools the sub-agent is allowed to use | | `context` | object | No | Additional information or data to help the sub-agent | | `merge_child_artifacts` | boolean | No | Merge child's `out/` and `files/` back to parent (default: `true`) | | `model` | string | No | Model override — if omitted, inherits the parent's model | | `deadline_seconds` | integer | No | Custom deadline in seconds (default: 300, max: 1800) | | `interactive` | boolean | No | If `true`, workstream waits for follow-up messages via `message_workstream` (default: `false`) | ### Check Workstream Tool **Name:** `check_workstream` Check the status and progress of a running workstream by its `launch_id`. Returns current status, elapsed time, remaining time, latest progress, and deadline percentage. Also works for completed workstreams. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `launch_id` | string | Yes | The `launch_id` returned when the workstream was started | ### List Workstreams Tool **Name:** `list_workstreams` List all workstreams (running and completed) with their status, duration, and latest progress. Useful for monitoring and debugging. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `status_filter` | string | No | Filter by status: `all` (default), `running`, `completed`, `failed`, `timeout`, `canceled` | ### Terminate Workstream Tool **Name:** `terminate_workstream` Terminate a running workstream. Sends a cancellation request to the child workflow. Particularly important for interactive workstreams, which never complete on their own. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `launch_id` | string | Yes | The `launch_id` of the workstream to terminate | ### Steer Workstream Tool **Name:** `steer_workstream` Send a mid-execution steering directive to a running workstream to adjust its behavior without terminating it. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `launch_id` | string | Yes | The `launch_id` of the workstream to steer | | `command` | string | Yes | Directive type: `add_instruction`, `set_priority`, `add_constraint`, `pause_requested`, or `resume_requested` | | `message` | string | Yes | The directive message or instruction to send | ### Message Workstream Tool **Name:** `message_workstream` Send a follow-up message to an interactive workstream (one launched with `interactive: true`). Enables multi-turn conversations with a specialist sub-agent. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `launch_id` | string | Yes | The `launch_id` of the interactive workstream | | `message` | string | Yes | The message to send — the sub-agent receives this as user input and responds | ### Get Workstream Result Tool **Name:** `get_workstream_result` Retrieve the full result of a completed workstream. Returns the summary, status, duration, error information, and last progress details. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `launch_id` | string | Yes | The `launch_id` of the completed workstream | ## Document Management Tools Tools for managing documents in the Vertesia knowledge base. These tools handle CRUD operations for documents with full metadata support. ### Search Documents Tool **Name:** `search_documents` A powerful tool for searching and analyzing documents with two distinct modes: **Search Mode** for high-level queries and **DSL Mode** for direct Elasticsearch access. #### Search Mode (Recommended) Use Search Mode for most document searches. It provides a high-level API with automatic processing: **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `query.name` | string | Partial name match (autocomplete-style) | | `query.type` | string | Filter by document type ID | | `query.status` | string | Filter by document status | | `query.full_text` | string | Full-text search with stemming and fuzzy matching | | `query.vector` | object | Vector similarity search (see below) | | `query.weights` | object | Weights for hybrid search (e.g., `{ full_text: 2, vector: 3 }`) | | `query.score_aggregation` | string | Score aggregation method: `rrf`, `rsf`, or `smart` | | `query.dynamic_scaling` | string | Dynamic weight scaling: `on` or `off` | **Vector Search Options:** | Parameter | Type | Description | |-----------|------|-------------| | `query.vector.text` | string | Text to embed and search semantically | | `query.vector.objectId` | string | Reuse embeddings from an existing object | | `query.vector.image` | string | Image URL or base64 for vision embedding | | `query.vector.config` | object | Embedding types to use (`text`, `properties`, `vision`, `code`) | **Example - Hybrid Search:** ```json { "query": { "full_text": "quarterly financial report", "vector": { "text": "company earnings analysis" }, "weights": { "full_text": 2, "vector": 3 }, "score_aggregation": "smart" }, "limit": 20 } ``` #### DSL Mode (Power Users) Use DSL Mode for direct Elasticsearch Query DSL access. Ideal for analytics, complex aggregations, and full control: **DSL Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `dsl.query` | object | Elasticsearch query clause (e.g., `match_all`, `term`, `bool`) | | `dsl.aggs` | object | Aggregations for analytics (e.g., `terms`, `date_histogram`) | | `dsl.size` | number | Results to return (0-10,000; use 0 for aggregations-only) | | `dsl.from` | number | Pagination offset (0-100,000) | | `dsl.sort` | array | Sort order (e.g., `[{ "created_at": "desc" }]`) | **Example - Aggregation:** ```json { "dsl": { "aggs": { "by_status": { "terms": { "field": "status" } }, "by_type": { "terms": { "field": "type.name" } } }, "size": 0 } } ``` #### Shared Options (Both Modes) | Parameter | Type | Description | |-----------|------|-------------| | `limit` | number | Maximum results (default: 100) | | `offset` | number | Skip n documents for pagination | | `format` | string | Output format: `json`, `csv`, or `table` | | `count_only` | boolean | Return only document count | | `all_revisions` | boolean | Include all revisions, not just latest | | `collection_id` | string | Search within specific collection | | `analyze` | boolean | Run LLM analysis on results | | `analyzer_prompt` | string | Custom instructions for LLM analysis | | `facets` | array | Compute aggregated counts (e.g., `[{ name: "types", field: "type.name" }]`) | | `output_artifact` | object | Stream large results to artifact file | For more details on search configuration, see [Search Configuration](/content/search). ### Fetch Document Tool **Name:** `fetch_document` Retrieves a specific document by its identifier. Supports multiple modes (full document, properties only, content, sections, instrumented views, or AI-powered analysis) and can optionally stream large results to a workspace artifact for downstream processing with other tools. ### Create Document Tool **Name:** `create_document` Persists an existing source reference as a new document. Author text or markdown as an artifact first, then call `create_document` with `source: "artifact:"`. Binary artifacts, HTTPS URLs, and cloud storage URLs are preserved as file documents. ### Update Document Tool **Name:** `update_document` Updates existing documents with new content or properties. For structured XPath-based editing of DOCX documents, use the dedicated `edit_artifact_docx` tool instead. ### Create Content Object Tool **Name:** `create_content_object` Creates persistent content objects from external locations such as HTTPS URLs or cloud storage (for example, `https://…`, `s3://…`, `gs://…`). You can attach custom metadata, tags, and an optional collection so that imported files become searchable and available for later analysis. ### Spreadsheet Workflows with Skills Spreadsheet creation and analysis are implemented using a combination of document tools, skills, artifacts, and the Daytona sandbox rather than dedicated spreadsheet-specific built-ins: - Use `search_documents` and `fetch_document` to locate and access spreadsheet files stored in the knowledge base. - Use skills (for example, data-analysis skills) to generate or transform spreadsheets and to declare any required packages. - Use `write_artifact` to create helper scripts and data files, and `execute_shell` to run those scripts inside the sandbox, reading from `/home/daytona/files` and writing results to `/home/daytona/out`. - Use `read_artifact`, `list_artifacts`, and related tools to inspect outputs, and `create_document(source: "artifact:out/")` or `create_content_object` to persist final results. This pattern replaces the legacy spreadsheet tools and gives agents more flexibility and control over how spreadsheet data is processed. ## Document Rendering and Artifact Tools Tools for rendering documents to PDF/DOCX, visually previewing them, and performing structured edits on DOCX files. These tools are unlocked by the `learn_content_authoring` system skill. ### Render Markdown Tool **Name:** `render_markdown` Renders a markdown artifact to PDF or DOCX using the full Pandoc + XeLaTeX pipeline. Requires an `artifact_path` pointing to a markdown file in the artifact store — does **not** accept document IDs directly. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `artifact_path` | string | Yes | Path to a markdown artifact (e.g. `files/report.md`) | | `format` | string | No | Output format: `pdf` (default) or `docx` | | `title` | string | No | Document title for metadata and output filename | | `toc` | boolean | No | Include table of contents (default: `true`) | | `template_path` | string | No | Custom LaTeX template file (artifact path, `artifact:`, or `store:` protocol) | | `logo_path` | string | No | Custom logo file (artifact path, `artifact:`, or `store:` protocol) | | `data_source` | string | No | Source for template data injection (`store:` or `artifact:`) | Output is stored at `out/{title}.{pdf|docx}` in the artifact workspace. ### Render DOCX Tool **Name:** `render_docx` Exports a DOCX file to PDF or Markdown. Accepts either an artifact path or a document store ID as source. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `docx_path` | string | One of | Artifact path to a DOCX file (e.g. `out/report.docx`) | | `document_id` | string | One of | Document store ID of an existing DOCX | | `format` | string | No | Output format: `pdf` (default) or `markdown` | | `output_path` | string | No | Output artifact path (defaults to `out/.pdf` or `out/.md`) | | `title` | string | No | Base name used when `output_path` is omitted | ### Inspect PDF Tool **Name:** `inspect_pdf` Visually inspects a PDF page by page. Supports an overview thumbnail grid (default) and a high-resolution detail mode for specific pages. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `pdf_path` | string | One of | Artifact path to a PDF file (e.g. `out/report.pdf`) | | `document_id` | string | One of | Document store ID of a PDF document | | `pages` | number[] | No | 1-indexed page numbers for detail mode (max 4) | | `from_page` | number | No | Overview mode: start page (default: 1) | | `to_page` | number | No | Overview mode: end page (default: last) | | `columns` | number | No | Overview mode: grid columns (default: 3, max: 4) | ### Edit DOCX Artifact Tool **Name:** `edit_artifact_docx` Applies structured XPath-based edits to a DOCX artifact's `word/document.xml`. Supports insert, replace, delete, append, and move operations with optional tracked changes. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the DOCX artifact (e.g. `out/report.docx`) | | `structured_edits` | array | Yes | XPath edit operations to apply | | `track_changes` | boolean | No | Emit Word tracked changes markup (default: `false`) | | `author` | string | No | Author name for tracked changes | Each edit in `structured_edits` has: | Field | Type | Description | |-------|------|-------------| | `operation` | string | `insert_before`, `insert_after`, `replace`, `delete`, `append`, or `move_to` | | `anchor_type` | string | `xpath` | | `anchor_expr` | string | XPath expression targeting the element | | `content` | string | New XML content (for insert/replace/append operations) | | `target_anchor_expr` | string | Target XPath for `move_to` operations | | `position` | string | `top`, `end`, `before`, or `after` (for `move_to`) | ### Merge Artifacts Tool **Name:** `merge_artifacts` Merges multiple artifact files into a single output file by concatenating them in order. Useful for assembling sectional documents (e.g., after authoring sections independently with workstreams). **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `paths` | string[] | Yes | Ordered list of artifact paths to concatenate | | `output_path` | string | Yes | Destination artifact path for the merged output | | `separator` | string | No | Content inserted between each file (default: `\n\n`) | ## Type Management Tools Tools for managing object type definitions and schemas. These tools control the structure and validation rules for different types of objects in the system. ### Get Object Type Tool **Name:** `get_object_type` Retrieves details about specific object type definitions. ### Create or Update Type Tool **Name:** `create_or_update_object_type` Creates new or updates existing object type definitions. ## Collection Management Tools Tools for organizing and grouping related documents into collections. Collections provide hierarchical organization and bulk operations on document sets. ### Create Collection Tool **Name:** `create_collection` Creates a new collection for organizing related documents. Collections act as containers that group documents together for easier management and access. ### Update Collection Tool **Name:** `update_collection` Modifies an existing collection's properties, such as name, description, or schema definition. This tool updates collection metadata without affecting the documents contained within it. ### Add to Collection Tool **Name:** `add_to_collection` Places one or more existing documents into a collection for organization and grouping. This tool establishes relationships between documents and collections, without modifying the documents themselves. ### Remove from Collection Tool **Name:** `remove_from_collection` Removes documents from a collection while preserving the documents themselves. This tool only breaks the association between documents and a collection; it does not delete the documents from the system. ### Get Collection Tool **Name:** `get_collection` Accesses detailed information about an existing collection, including its name, description, schema, and member documents. This tool retrieves the full definition of a collection along with metadata about contained documents. ### Search Collections Tool **Name:** `search_collections` Finds collections by searching for partial matches in collection names. This tool searches through all existing collections and returns those whose names contain the specified search term using case-insensitive partial matching. ## Temporary Artifact Tools Tools for managing temporary artifacts in the agent workspace. Artifacts are per-run files (scripts, intermediate data, and outputs) that are automatically deleted when the workflow completes. Use these tools together with `execute_shell` for robust code and data workflows. ### Write Artifact Tool **Name:** `write_artifact` Writes a temporary file into the agent workspace. Use `type: "script"` for code (synced to `/home/daytona/scripts/`) or `type: "file"` for data (synced to `/home/daytona/files/`). ### Read Artifact Tool **Name:** `read_artifact` Reads the content of a temporary artifact, with optional line ranges and line numbers for precise inspection. ### List Artifacts Tool **Name:** `list_artifacts` Lists available artifacts, optionally filtered by a path prefix such as `scripts/`, `files/`, or `out/`. ### Grep Artifact Tool **Name:** `grep_artifacts` Searches for a regular-expression pattern across artifacts, useful for finding errors or specific content in generated files. ### Patch Artifact Tool **Name:** `patch_artifact` Applies literal find-and-replace edits inside an artifact, typically after inspecting it with `read_artifact` or `grep_artifacts`. ### View Image Tool **Name:** `view_image` Exposes an image from an artifact (for example, `out/plot.png`) or a stored Vertesia document as an image attachment that the model can see and combine with `analyze_image`. ## Web and External Tools Tools for interacting with external services and executing custom code. These tools extend agent capabilities beyond the core platform functionality. ### Web Search Tool **Name:** `web_search` Searches the web for information using specified queries and options. This activity requires an API key for [serper](https://serper.dev/). Go to Setting in Studio to configure your API key. ### Execute Shell Tool **Name:** `execute_shell` Executes shell commands inside a managed Daytona sandbox. The sandbox is created on first use for a workflow run and reused across calls, preserving installed packages and files until the workflow completes. Use this tool to: - Run Python or other language scripts stored under `/home/daytona/scripts` (for example, data analysis with pandas). - Manipulate files under `/home/daytona/files`, `/home/daytona/documents`, and `/home/daytona/out`. - Install additional packages needed by skills using the tool input (for example, extra Python or system packages). Artifacts created with the temporary artifact tools are automatically synced into the sandbox on each call: - `scripts/*` → `/home/daytona/scripts/` - `files/*` → `/home/daytona/files/` - `skills/*` → `/home/daytona/skills/` - `out/*` ↔ `/home/daytona/out/` for derived outputs that should be reused later. You can also use the `documents` parameter to download Vertesia documents into `/home/daytona/documents/` (as original files or extracted text) before running shell commands that analyze them. ### Ask User Tool **Name:** `ask_user` Requests input from users during workflow execution. ### Analyze Image Tool **Name:** `analyze_image` Executes ImageMagick commands on images and PDFs, storing results to cloud storage. Supports standard image formats and PDF documents with command chaining capabilities. Combine this with the `view_image` tool to first surface images from artifacts or stored documents into the conversation so the model can inspect and transform them. ## Conversation Tools Tools for searching and analyzing past conversations and agent runs. ### Search Conversations Tool **Name:** `search_conversations` Searches workflow runs (conversations) by status, time range, initiator, or interaction name, with pagination support and an optional `output_artifact` setting to stream full result sets into an artifact while returning a small preview. ### Analyze Conversation Tool **Name:** `analyze_conversation` Loads the conversation from another workflow run and analyzes it using an `analyzer_prompt`, optionally constrained by a `result_schema`. Useful for reviewing agent behavior, extracting key outcomes, or monitoring progress of running workflows. ## Communication Tools Tools for sending notifications and messages to external recipients. ### Send Email Tool **Name:** `send_email` Sends emails using the Resend email service. Accepts markdown content which is automatically converted to HTML with a plain text fallback. Supports email conversations where recipients can reply and have their responses routed back to the workflow. **Project Integration Configuration:** This tool reads its configuration from the project's Resend integration (configure in Project Settings > Integrations): | Setting | Required | Description | |---------|----------|-------------| | `enabled` | Yes | Must be true for the tool to run | | `api_key` | Yes | Your Resend API key | | `email_domain` | Yes | Verified Resend domain used for both the from address and inbound reply routing | | `default_from_name` | No | Display name for outgoing emails (defaults to `Vertesia - {project name}`) | | `webhook_secret` | No | Required for receiving email replies via Resend webhooks | The `from` address is constructed automatically from the integration settings and cannot be overridden; see **Email Reply Routing** below for when `reply-to` is set. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `to` | string[] | Yes | Array of recipient email addresses | | `subject` | string | Yes | The email subject line | | `markdown` | string | Yes | The email body in markdown format | | `cc` | string[] | No | Array of CC recipient addresses | | `bcc` | string[] | No | Array of BCC recipient addresses | | `attachments` | object[] | No | File attachments rendered as download links at the end of the email body. Each entry: `{ artifact_path? \| url?, filename, content_type? }` | | `enable_reply` | boolean | No | Enable email reply routing. Defaults to true. Requires `email_domain` on the Resend integration. | **Email Reply Routing:** When `enable_reply` is true (default) and `email_domain` is configured on the Resend integration, the tool sends the email with a generated reply-to address in the format `r+{runId}@{email_domain}`. When a recipient replies: 1. The reply is received by Resend at the email domain 2. Resend sends a webhook to the Vertesia API 3. The webhook extracts the run ID and sends a `UserInput` signal to the workflow 4. The workflow receives the email content as user input and can respond This enables email-based conversations with agents, where users can communicate via email instead of the UI. **Inline artifact:// URLs:** The only custom URL scheme resolved in the markdown body is `artifact://`. Matching links and image references are rewritten to signed download URLs before the markdown is converted to HTML: - `![Chart](artifact://files/chart.png)` — renders as a remote `` with a signed URL - `[Report](artifact://files/report.pdf)` — renders as a clickable link with a signed URL Images are loaded via the recipient's mail client over HTTP at render time — they are not inlined as MIME/CID attachments. Explicit `attachments` entries are appended to the body as a bulleted list of download links rather than attached as files to the outbound email. ## Data Platform Tools Tools for managing data stores, tables, queries, and dashboards. These tools enable agents to work with structured data using DuckDB databases. For comprehensive documentation, see the [Data Platform Tools Reference](/data-platform/tools). ### Key Tools - **data_get_schema** - Get the schema of a data store - **data_list_tables** - List tables with metadata - **data_create_database** - Create a new DuckDB database - **data_create_tables** - Create tables atomically - **data_import** - Import data from files or inline data - **data_preview_dashboard** - Preview Vega-Lite dashboards - **data_create_dashboard** - Create saved dashboards - **data_render_dashboard** - Render dashboards to PNG ## Automation Tools Tools for automating and scheduling recurring tasks. ### Schedule Workflow Tool **Name:** `schedule_workflow` Creates recurring schedules for agent/workflow execution using cron expressions. This tool is not enabled by default and must be explicitly added to an agent's tools. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `name` | string | Yes | Name of the schedule for identification | | `description` | string | No | Description of what this scheduled workflow does | | `interaction` | string | Yes | The interaction/agent ID to execute on schedule | | `cron_expression` | string | Yes | Cron expression defining when to run | | `timezone` | string | No | Timezone for the cron expression (defaults to UTC) | | `vars` | object | No | Variables to pass to the scheduled workflow | | `enabled` | boolean | No | Whether to enable immediately (defaults to true) | **Cron Expression Format:** The cron expression uses 5 fields: `minute hour day month weekday` | Field | Values | Description | |-------|--------|-------------| | minute | 0-59 | Minute of the hour | | hour | 0-23 | Hour of the day | | day | 1-31 | Day of the month | | month | 1-12 or JAN-DEC | Month of the year | | weekday | 0-6 or SUN-SAT | Day of the week (0=Sunday) | Special characters: - `*` - any value - `,` - list separator (e.g., 1,3,5) - `-` - range (e.g., 1-5) - `/` - step (e.g., */15 for every 15) **Common Cron Examples:** | Expression | Description | |------------|-------------| | `0 9 * * *` | Every day at 9:00 AM | | `0 9 * * MON` | Every Monday at 9:00 AM | | `0 9 * * MON-FRI` | Weekdays at 9:00 AM | | `0 0 1 * *` | First day of each month at midnight | | `0 */2 * * *` | Every 2 hours | | `30 8 * * *` | Every day at 8:30 AM | **Important Notes:** - Scheduled workflows run non-interactively but can still use `ask_user` for async input via email, webhooks, or headless UX listening to the stream - Use meaningful names for easy identification in the UI - Consider timezone when scheduling for specific business hours - This tool must be explicitly added to an agent's tool list ## Index Configuration Tools Tools for querying and managing search index configuration. These tools allow agents to inspect and update embedding settings and trigger reindexing operations. ### Get Index Configuration Tool **Name:** `get_index_configuration` Retrieves the current index status and configuration for the project's search infrastructure. **Returns:** - Index existence and health status - Document count and storage size - Embedding dimensions for text, image, and properties - Field mappings and index version **Example Response:** ```json { "enabled": true, "exists": true, "index_name": "content_abc123_v3", "alias_name": "content_abc123", "document_count": 15234, "size_bytes": 52428800, "embedding_dimensions": { "text": 1536, "image": 1536, "properties": 1536 }, "field_mappings": { "text": "text", "updated_at": "date" }, "version": 3 } ``` ### Update Index Configuration Tool **Name:** `update_index_configuration` Updates index configuration settings, including embedding dimensions. Can trigger reindexing when configuration changes require it. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `embedding_dimensions` | object | No | New dimensions for embedding types | | `embedding_dimensions.text` | number | No | Dimensions for text embeddings | | `embedding_dimensions.image` | number | No | Dimensions for image embeddings | | `embedding_dimensions.properties` | number | No | Dimensions for properties embeddings | | `force_reindex` | boolean | No | Trigger a full reindex of all documents | | `user_confirmed` | boolean | Yes | Must be `true` - requires confirmation via `ask_user` first | **Important:** This tool requires user confirmation before making changes. Always use `ask_user` to confirm the operation before calling this tool with `user_confirmed: true`. **Example:** ```json { "embedding_dimensions": { "text": 3072 }, "force_reindex": true, "user_confirmed": true } ``` For more details on index configuration, see [Search Configuration](/content/search). --- ## Workstreams Source: https://docs.vertesiahq.com/agent-runner/workstreams Markdown: https://docs.vertesiahq.com/llms/agent-runner/workstreams.md Workstreams are **non-blocking parallel sub-agents** that run independently alongside the parent agent. They enable agents to delegate research, analysis, computation, or authoring tasks to dedicated child agents that execute concurrently — dramatically speeding up complex multi-part work. Workstreams replace the legacy `execute_parallel_work_streams` tool with a more flexible, non-blocking architecture. The parent agent continues working while children execute in the background. ## How It Works ``` Parent Agent | |-- launch_workstream("research_market") --> Child Agent 1 (running) |-- launch_workstream("analyze_data") --> Child Agent 2 (running) |-- launch_workstream("draft_report") --> Child Agent 3 (running) | | (parent continues working or waits) | |<-- workstream_completed signal <-- Child Agent 1 (done) |<-- workstream_completed signal <-- Child Agent 2 (done) |<-- workstream_completed signal <-- Child Agent 3 (done) | v Synthesize results ``` 1. The parent calls `launch_workstream` — it returns **immediately** with a `launch_id`. 2. A child workflow starts in the background as a separate Temporal workflow. 3. The child communicates progress back to the parent via Temporal signals. 4. When the child completes, the parent receives a system message with the result summary. 5. Artifacts created by the child are automatically merged into the parent's workspace. ## When to Use Workstreams **Use workstreams when:** - The task can be decomposed into **independent subtasks** (e.g., research + analysis + drafting) - You need **multiple perspectives** on the same topic (Expert Roundtable pattern) - A subtask would be a **digression** from the main conversation flow - You want to **parallelize** work that would otherwise be sequential **Use sequential execution when:** - Tasks have **strict dependencies** (output of A is input to B) - The task is simple enough to complete **inline** in a single step - You need **tight control** over every step of the process ## Workstream Lifecycle Each workstream progresses through these states: ``` launched --> running --> completed |-> failed |-> timeout |-> canceled (via terminate_workstream) ``` | State | Description | |-------|-------------| | `running` | Sub-agent is actively executing tools and reasoning | | `canceling` | Termination requested; awaiting graceful shutdown (60-second grace period) | | `completed` | Finished successfully — summary and artifacts available | | `failed` | Encountered an unrecoverable error | | `timeout` | Exceeded its deadline and was automatically terminated | | `canceled` | Terminated by the parent via `terminate_workstream` | ## Progress Tracking Workstreams report rich progress back to the parent, including: - **Phase** — what the sub-agent is doing right now: - `planning` — analyzing the task and forming a plan - `executing_tool` — running a tool (tool name included) - `synthesizing` — combining results into a response - `blocked` — waiting for input or a dependency - `done` — finished processing - **Current iteration** number (0-based) - **Model message** — what the sub-agent is thinking about - **Deadline percentage** — how much time has elapsed Use `check_workstream` to query progress on demand, or `list_workstreams` for an overview of all workstreams. ## Deadline Management Every workstream has a deadline to prevent runaway execution: | Setting | Value | |---------|-------| | Default deadline | 5 minutes | | Maximum deadline | 30 minutes | | Minimum deadline | 30 seconds | Configure the deadline via the `deadline_seconds` parameter on `launch_workstream`. ### Auto Wrap-Up The system automatically manages deadlines: 1. At **75%** of the deadline, the parent receives a warning. 2. At **80%**, the child receives an automatic wrap-up steering directive telling it to finalize. 3. At **90%**, the parent receives a final warning. 4. At **100%**, the workstream is terminated and the parent receives a timeout notification with the last known progress. ## Interactive Workstreams By default, workstreams run in **fire-and-forget** mode: the child executes its instruction and completes. For multi-turn conversations with a sub-agent, launch with `interactive: true`. Interactive workstreams: - Complete their initial task, then **wait** for follow-up messages - Receive messages via `message_workstream` - Support multi-round exchanges (e.g., asking follow-up questions, requesting refinements) - **Never complete on their own** — the parent must call `terminate_workstream` to close the session Interactive mode is the foundation of the [Expert Roundtable](#expert-roundtable-pattern) pattern. ## Artifact Merging When `merge_child_artifacts` is `true` (the default), artifacts created by the child in its `out/` and `files/` directories are automatically copied back to the parent's artifact workspace after completion. Artifacts are **namespaced by child run ID** to avoid conflicts: ``` Parent workspace: files/{child_run_id}/output.json out/{child_run_id}/report.pdf ``` Set `merge_child_artifacts: false` to disable this behavior for workstreams that produce intermediate artifacts you don't need. ## Steering The parent can send directives to a running workstream via `steer_workstream` without terminating it: | Command | Use Case | |---------|----------| | `add_instruction` | "Also include pricing information in your analysis" | | `set_priority` | "Focus on the security aspects first" | | `add_constraint` | "Limit your analysis to the last 3 years" | | `pause_requested` | Request the sub-agent to pause | | `resume_requested` | Resume a paused sub-agent | Steering directives are delivered as system messages that the child processes in its next reasoning turn. ## Best Practices 1. **Decompose clearly** — each workstream should have a single, well-defined objective. Vague instructions produce vague results. 2. **Provide rich context** — the sub-agent does **not** see the parent's conversation history. Include all relevant information in the `instruction` and `context` parameters. 3. **Scope tools carefully** — limit `allowed_tools` to what the task actually needs. A research task might only need `think` and `learn_web_search`, while a data analysis task might need `execute_shell` and artifact tools. 4. **Use meaningful IDs** — the `id` parameter appears in logs, progress messages, and the UI. Use descriptive names like `research_competitors` instead of `ws1`. 5. **Don't duplicate work** — avoid launching workstreams for tasks you can do faster inline. Workstreams have overhead (workflow startup, signal communication). 6. **Set appropriate deadlines** — use longer deadlines (up to 30 minutes) for complex tasks, shorter ones for simple research. 7. **Synthesize results** — when workstreams complete, review and combine their outputs into a coherent response rather than forwarding raw results. 8. **Don't poll** — results are delivered automatically via system messages. Simply end your turn and wait; the system handles notification. ## Expert Roundtable Pattern The Expert Roundtable is an advanced multi-agent pattern where specialist sub-agents with distinct personalities and optionally different models debate a topic from multiple perspectives. This produces richer, less biased analysis than a single-agent approach. ### How It Works 1. **Decompose** the topic into 2-5 specialist perspectives (e.g., security architect, business strategist, technical lead). 2. **Launch** each specialist as an interactive workstream with a specific personality, model, and instructions. 3. **Collect** opening statements using `analyze_conversation` on the running child workflows. 4. **Cross-pollinate** — use `message_workstream` to pass opposing arguments between specialists for multi-round debate. 5. **Synthesize** — after convergence (or max 3 rounds), write a final synthesis that reconciles all perspectives. 6. **Terminate** all interactive workstreams when done. ```json { "id": "security_expert", "name": "Security Architect", "instruction": "You are a skeptical security architect. Analyze this proposal from a security perspective...", "context": { "topic": "..." }, "allowed_tools": ["think", "write_artifact"], "interactive": true, "model": "claude-sonnet-4-20250514", "deadline_seconds": 600 } ``` The Expert Roundtable pattern is available as the `learn_expert_roundtable` system skill, which provides detailed step-by-step instructions. ## Tools Reference For detailed parameter documentation of all workstream tools, see [Built-in Tools — Workstream Tools](/agent-runner/tools#workstream-tools). | Tool | Description | |------|-------------| | `launch_workstream` | Launch a non-blocking child sub-agent | | `check_workstream` | Query status and progress | | `list_workstreams` | List all workstreams with filtering | | `terminate_workstream` | Cancel a running workstream | | `steer_workstream` | Send mid-execution directives | | `message_workstream` | Send messages to interactive workstreams | | `get_workstream_result` | Retrieve completed workstream results | --- ## Introduction Source: https://docs.vertesiahq.com/api/introduction Markdown: https://docs.vertesiahq.com/llms/api/introduction.md Welcome to the Vertesia Platform API documentation! This guide will walk you through the available endpoints and demonstrate how to use them with cURL and the client SDK. ## Platform API The Vertesia Platform offers a comprehensive set of API endpoints, all available from a single URL: [https://api.vertesia.io/api/v1](https://api.vertesia.io/api/v1) ## Authentication Vertesia uses JWT tokens to authenticate API requests. The most convenient way to acquire a JWT token is to use the Vertesia CLI. ## API Documentation [Go to the OpenAPI documentation](/api/openapi) --- ## Installed Application Settings Source: https://docs.vertesiahq.com/apps/installation-settings Markdown: https://docs.vertesiahq.com/llms/apps/installation-settings.md Installed applications can be customized with their own settings. To edit the settings for an installed application, select the "Edit Settings" button within the application card in Vertesia Studio (`Vertesia Studio > Settings > Applications > Installed Apps`). - Note: If you do not have permission to edit the application, you will not be able to make changes. ## OAuth Credentials at Install Time If an app's manifest declares MCP collections with [`oauth_config.required_at_install`](/apps/tool-collections#embedded-oauth-configuration), Vertesia displays a credential form before completing the installation. Required fields may include: | Field | Description | | ----- | ----------- | | **Client ID** | Your OAuth client identifier, if each project registers its own client with the provider. | | **Client Secret** | Your OAuth client secret. Secrets are encrypted at rest and never stored in the manifest. | | **Scopes** | Additional OAuth scopes to request, added on top of the manifest's `default_scopes`. | After install, the OAuth provider created from these credentials can be viewed and updated in **Project Settings > OAuth Providers**. Install-time credentials are associated with each MCP collection by its stable `id`, not its display `name`. ## App Card Color You can customize the colors used for the application card's appearance within the Vertesia App Portal. To do so, use the `color` property in the application settings: ```json { "color": "" } ``` ### Available Colors When configuring application installations, you can choose from a variety of predefined colors to customize your applications's appearance: red orange amber yellow lime green emerald teal cyan sky blue indigo violet purple fuchsia pink rose ### Using Color Gradients Applications support two different gradient formats for enhanced visual appearance: ### Using Gradient Stops Gradients can be specified using color names with numeric gradient stops: - **Format:** `color-number` red-0 red-1 red-2 red-3 red-4 ### Using Two Colors Create gradients that blend between two different colors: - **Format:** `color-color` red-red red-orange red-amber red-yellow red-lime --- ## What are Applications? Source: https://docs.vertesiahq.com/apps/overview Markdown: https://docs.vertesiahq.com/llms/apps/overview.md Applications are the way to customize the Vertesia platform for your projects. By creating an application you can: 1. Contribute UI applications focused on the business model of your organization / project. 2. Contribute custom tools. 3. Contribute content types to your project. Applications are installed at project level. ## Creating an application Applications are created by registering the application manifest in `Vertesia Studio > Settings > Available Apps` page. Each application must have an unique name in kebab case. We recommend prefixing the name of your application using your organization name: `vertesia-review-center`, `foo-contracts` etc. ## Application manifest An application manifest is implementing the following interface: ```ts interface AppManifestData { /** * The name of the app, used as the id in the system. * Must be in kebab case (e.g. my-app). */ name: string; /** * Visibility of the app: "public", "private", or "vertesia". * Private apps are only visible to the owner organization. */ visibility: "public" | "private" | "vertesia"; title: string; description: string; publisher: string; /** * A svg icon for the app. */ icon?: string; status: "beta" | "stable" | "deprecated"; ui?: { /** * The source URL of the app. For iframe isolation this is the standalone * application URL. For legacy plugin modes it is an ESM bundle URL. * The src can be a template which contain * a variable named `buildId` which will be replaced with the current build id. * For example: `/plugins/vertesia-review-center-${buildId}` */ src: string; /** * The isolation strategy. New customer apps should use iframe. * - iframe - run a standalone app with its own dependencies and document. * - shadow - use Shadow DOM to fully isolate the plugin from the host. * - css - use CSS processing (like prefixing or other isolation techniques). Lighter but plugins may conflict with the host */ isolation?: "iframe" | "shadow" | "css"; }, /** * MCP server configurations for external tool integrations. * See the Tool Collections page for full configuration options. */ tool_collections?: ToolCollection[]; /** * The app endpoint URL. Tools, interactions, UI, and settings * are discovered automatically from this URL. */ endpoint?: string; } ``` Applications can be private or public. Private applications can only be installed in projects of the same organization as the one who defined the application. The application will not be visible in other organizations. Public applications are visible in all organizations. In order to use an application you need to install it on a project. ## Installing an application on a project Open the `Installations` tab in the applications settings page to see the list of the installed applications in the current project or to install a new application. After installing the application you must define the users or groups that can access the application. This can be done using the `Manage permissions` button. ## Contributing UI applications Customer applications are loaded as standalone documents in sandboxed iframes. Set `ui.src` to the application URL and `ui.isolation` to `iframe`. The app owns React and its complete dependency graph, so a Studio CDN dependency upgrade cannot change the app at runtime. Studio bootstraps the existing user session through an origin-checked in-memory handshake and reacquires it when the embedded session needs refreshing; direct visits use the normal sign-in flow. The iframe bridge also synchronizes route, account/project, locale, and theme state with Composite Apps. Generated apps advertise both `app_portal` and `composite_app` availability by default. Iframe isolation is a DOM and dependency boundary, not a credential boundary. The trusted application origin receives the viewing user's Studio JWT in memory, so `ui.src` must point to an origin trusted with that user's Vertesia access. It must also differ from the Studio origin; that separation is required for the iframe sandbox to remain an effective DOM boundary. The `shadow` and `css` modes remain available for legacy and first-party platform extensions that are deliberately compiled as host-React plugin modules. Installed Applications that contribute an UI application are visible on the Vertesia Studio UI landing page for users who have the permission to access the application. To learn how to develop an application UI, go to the [Application UI](/apps/ui) section. ## Contributing custom tools Custom tools are contributed via the `endpoint` property — Vertesia automatically discovers tools from the endpoint URL. These servers can expose both regular tools and skills. Skills appear to agents as tools whose names start with the `learn_` prefix and cooperate with the Daytona sandbox and `execute_shell` for advanced code and data workflows. To learn how to write a tools server, see [Custom Tools](/agent-runner/custom-tools). **Note** that only the users that can access the application can use these tools. **Example:** ```json { "name": "my-app", "endpoint": "https://my-tools-server.com/api/vertesia" } ``` ## Contributing MCP Servers To connect external [MCP servers](/apps/tool-collections), use the `tool_collections` property. MCP servers support OAuth authentication. See [MCP Servers](/apps/tool-collections) for full configuration options. **Example:** ```json { "name": "my-app", "tool_collections": [ { "type": "mcp", "url": "https://mcp.my-crm-provider.com", "name": "My CRM", "description": "CRM tools for contacts and deals", "namespace": "crm", "auth": "oauth", "oauth_app": "my-crm-oauth" } ] } ``` ## Contributing content types Work is in progress for this feature. --- ## MCP Servers Source: https://docs.vertesiahq.com/apps/tool-collections Markdown: https://docs.vertesiahq.com/llms/apps/tool-collections.md [MCP (Model Context Protocol)](https://modelcontextprotocol.io) servers connect external tools to your Vertesia applications. Agents can then use the tools provided by these servers during execution. For custom HTTP tool servers built with the Vertesia Tools SDK, use the [`endpoint`](/apps/overview) field in your app manifest instead — tools are discovered automatically from the endpoint URL. ## Quick Start Add an MCP server to your app manifest's `tool_collections` array: ```json { "name": "my-app", "tool_collections": [ { "type": "mcp", "id": "example_tools", "url": "https://mcp.example.com/v1", "name": "Example Tools", "description": "Tools from Example service", "namespace": "example", "auth": "oauth" } ] } ``` ## MCP Configuration Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `type` | `string` | Yes | Must be `"mcp"`. | | `url` | `string` | Yes | The MCP server URL. | | `name` | `string` | Yes | Human-readable name for the tool collection. Must be unique within the manifest. | | `description` | `string` | Yes | Human-readable description of the tools provided. | | `namespace` | `string` | Yes | Prefix added to tool names to avoid collisions (e.g., `example_search`). | | `id` | `string` | Yes | Stable identifier for this collection. Must be `snake_case`. Used for install-time bindings and should not change after publish. | | `auth` | `string` | No | Authentication type. Set to `"oauth"` for OAuth-protected servers. | | `oauth_app` | `string` | No | Name of a manually created [OAuth Provider](/api/openapi#tag/oauth-providers). Use this legacy/manual path when you need full control over the provider lifecycle. | | `oauth_config` | `object` | No | OAuth configuration embedded directly in the manifest. Vertesia auto-creates an OAuth Provider when the app is installed. See [Embedded OAuth Configuration](#embedded-oauth-configuration) below. Mutually exclusive with `oauth_provider`. | | `oauth_provider` | `string` | No | Key of a shared provider declared in the top-level `oauth_providers` map. Mutually exclusive with `oauth_config` and `oauth_app`. Requires `auth: "oauth"`. See [Shared OAuth Providers](#shared-oauth-providers). | | `oauth_scopes` | `string[]` | No | Additional OAuth scopes for this collection when using `oauth_provider`. Merged with the provider's `default_scopes` and any installer-supplied scopes at install time. | The distinction between `id` and `name` matters: - `id` is the stable machine identifier. Vertesia uses it for install-time OAuth bindings and related runtime lookups. - `name` is the human-readable label shown in the UI. ### Namespacing The `namespace` field is prepended to all tool names from the MCP server. For example, if the server exposes a tool named `search` and the namespace is `crm`, the agent sees it as `crm_search`. This prevents name collisions when multiple MCP servers are configured. ## OAuth Authentication Some MCP servers require OAuth 2.0 authentication. Vertesia handles the full OAuth flow including token storage and automatic refresh. There are four ways to configure OAuth for an MCP collection, in order of preference: 1. **Shared `oauth_providers` map** — Declare a named OAuth provider once at the manifest level. Multiple collections reference it by key. One `OAuthProvider` is created per provider at install time. Best when multiple MCP servers share the same OAuth client (e.g. Microsoft Graph and Microsoft Teams). 2. **Embedded `oauth_config`** — Declare OAuth parameters directly on the collection. Vertesia auto-creates an `OAuthProvider` when the app is installed. Best for distributing apps to multiple projects where each MCP server has its own OAuth client. 3. **`oauth_app` reference** — Point to a manually created OAuth Provider by name. Best for full lifecycle control or sharing a single provider across multiple collections. 4. **Remote server discovery** — If no provider is configured, Vertesia can discover OAuth metadata from the remote MCP server and follow that server's flow. ## Shared OAuth Providers Use `oauth_providers` when multiple MCP servers in the same app share a single OAuth client — for example, Microsoft Graph API and Microsoft Teams both use the same Azure OAuth provider configuration. Declare providers once at the manifest level, then reference them from each collection using `oauth_provider`: ```json { "name": "my-microsoft-app", "oauth_providers": { "microsoft": { "grant_type": "authorization_code", "client_id": "your-azure-app-client-id", "use_pkce": true, "default_scopes": ["offline_access"], "required_at_install": ["client_secret"] } }, "tool_collections": [ { "type": "mcp", "id": "ms_graph", "url": "https://graph.microsoft.com/mcp", "name": "Microsoft Graph", "description": "Access Microsoft 365 data", "namespace": "graph", "auth": "oauth", "oauth_provider": "microsoft", "oauth_scopes": ["User.Read", "Mail.Read"] }, { "type": "mcp", "id": "ms_teams", "url": "https://teams.microsoft.com/mcp", "name": "Microsoft Teams", "description": "Send messages and manage channels", "namespace": "teams", "auth": "oauth", "oauth_provider": "microsoft", "oauth_scopes": ["ChannelMessage.Send"] } ] } ``` At install time, Vertesia: 1. Creates **one** `OAuthProvider` named `my-microsoft-app-microsoft`. 2. Computes the final scope set: `offline_access` (provider default) ∪ `User.Read Mail.Read` (graph) ∪ `ChannelMessage.Send` (teams). 3. Stores a `provider_bindings` entry on the installation linking the `microsoft` key to the created OAuth Provider. Both collections then share the same token at runtime. ### `oauth_providers` Fields Each entry in the `oauth_providers` map uses the same fields as `oauth_config`: | Field | Type | Description | | ----- | ---- | ----------- | | `grant_type` | `string` | OAuth grant type: `"authorization_code"` (user OAuth flow) or `"client_credentials"` (machine-to-machine, no user consent). | | `authorization_endpoint` | `string` | OAuth authorize URL. Optional — auto-discovered from `.well-known/oauth-authorization-server` when omitted. | | `token_endpoint` | `string` | OAuth token exchange URL. Optional — auto-discovered when omitted. | | `revocation_endpoint` | `string` | Token revocation URL. Optional. | | `client_id` | `string` | Pre-configured client ID shared across all installs. Omit if each project registers its own OAuth client. | | `use_pkce` | `boolean` | Enable PKCE (Proof Key for Code Exchange). Defaults to `true`. | | `default_scopes` | `string[]` | Base scopes applied to **all** collections referencing this provider. | | `required_at_install` | `string[]` | Installer-supplied credentials. See [`required_at_install`](#required_at_install) below. | | `name` | `string` | Name for the created `OAuthProvider`. Defaults to `{app-slug}-{providerKey}` (e.g. `my-microsoft-app-microsoft`). | | `display_name` | `string` | Display name for the created `OAuthProvider`. | ### Scope Union The final scope set on the created `OAuthProvider` is: ```text provider.default_scopes ∪ collection₁.oauth_scopes ∪ collection₂.oauth_scopes ∪ installer_scopes ``` Duplicates are removed. The unified scope list is stored on the `OAuthProvider` and used for every collection that references this provider. ### Scope Changes After Install Changing `oauth_providers[key].default_scopes` or a collection's `oauth_scopes` in the manifest **does not automatically update already-installed `OAuthProvider` documents**. To apply updated scopes, the installer must uninstall and reinstall the app — this deletes the old provider and creates a fresh one from the updated manifest. ### Mutual Exclusivity A collection may use at most one OAuth configuration method. These combinations are rejected at manifest save time: | Invalid combination | Error | | ------------------- | ----- | | `oauth_provider` + `oauth_config` | Mutually exclusive | | `oauth_provider` + `oauth_app` | Mutually exclusive | | `oauth_scopes` without `oauth_provider` | Not meaningful outside a shared provider | | `oauth_provider` without `auth: "oauth"` | `auth: "oauth"` is required | | `oauth_provider` referencing a key not in `oauth_providers` | Unknown provider | ## Embedded OAuth Configuration Use `oauth_config` to embed OAuth parameters directly in the manifest. When a user installs the app, Vertesia automatically creates an `OAuthProvider` in their project using the configuration you provide, optionally merged with credentials the installer supplies at install time. This is the recommended approach for apps distributed to multiple projects — each project gets its own OAuth Provider with project-scoped credentials. `oauth_config` is install-time configuration only. At runtime, Vertesia resolves OAuth through the installation's stored binding from `collection.id` to the created OAuth Provider. ### `oauth_config` Fields | Field | Type | Description | | ----- | ---- | ----------- | | `grant_type` | `string` | OAuth grant type: `"authorization_code"` (user OAuth flow) or `"client_credentials"` (machine-to-machine, no user consent). | | `authorization_endpoint` | `string` | OAuth authorize URL. Optional — auto-discovered from `.well-known/oauth-authorization-server` when omitted. | | `token_endpoint` | `string` | OAuth token exchange URL. Optional — auto-discovered when omitted. | | `revocation_endpoint` | `string` | Token revocation URL. Optional. | | `client_id` | `string` | Pre-configured client ID shared across all installs. Omit if each project registers its own OAuth client. | | `use_pkce` | `boolean` | Enable PKCE. Defaults to `true`. | | `default_scopes` | `string[]` | Scopes to request by default. | | `required_at_install` | `string[]` | Parameters installers must provide at install time. Accepted values: `"client_id"`, `"client_secret"`, `"scopes"`. See below. | | `name` | `string` | Name for the created `OAuthProvider`. Defaults to the collection `id` converted to kebab-case. | | `display_name` | `string` | Display name for the created `OAuthProvider`. | ### `required_at_install` Use `required_at_install` to prompt the person installing the app for credentials that cannot be stored in a shared manifest: | Value | When to use | | ----- | ----------- | | `"client_id"` | Each project registers its own OAuth client with the provider (e.g., users bring their own app). | | `"client_secret"` | The OAuth provider requires a client secret — secrets must never be embedded in a shared manifest. | | `"scopes"` | Allow installers to specify additional scopes beyond the `default_scopes`. | When `required_at_install` is set, Vertesia displays a credential form during the install flow before creating the OAuth Provider. `client_secret` must never be stored in the manifest. If a secret is required, declare `"client_secret"` in `required_at_install` so the installer provides it at install time. ### Example: Shared Client ID, Secret Required at Install ```json { "tool_collections": [ { "type": "mcp", "id": "my-crm", "url": "https://mcp.my-crm-provider.com", "name": "My CRM", "description": "CRM tools for contacts and deals", "namespace": "crm", "auth": "oauth", "oauth_config": { "grant_type": "authorization_code", "client_id": "shared-client-id-from-provider", "use_pkce": true, "default_scopes": ["read", "write"], "required_at_install": ["client_secret"] } } ] } ``` ### Example: Each Installer Brings Their Own OAuth Client ```json { "tool_collections": [ { "type": "mcp", "id": "my-crm", "url": "https://mcp.my-crm-provider.com", "name": "My CRM", "description": "CRM tools for contacts and deals", "namespace": "crm", "auth": "oauth", "oauth_config": { "grant_type": "authorization_code", "authorization_endpoint": "https://auth.my-crm-provider.com/oauth/authorize", "token_endpoint": "https://auth.my-crm-provider.com/oauth/token", "use_pkce": true, "required_at_install": ["client_id", "client_secret"] } } ] } ``` ### How Auto-Provisioning Works At install time, Vertesia: 1. Merges `oauth_config` with the credentials the installer provided. 2. Creates an `OAuthProvider` in the installing project. 3. Stores a binding (`collection id` → `OAuth Provider id`) on the installation record. Credentials can be updated later from **Project Settings > OAuth Providers**. If an OAuth provider with the same generated name already exists, install will fail until the name conflict is resolved or `oauth_config.name` is changed. ## Manual OAuth Provider For full control over the OAuth provider lifecycle, create one manually and reference it by name: ### Step 1: Create an OAuth Provider Go to **Project Settings > OAuth Providers** and click **Create**. Fill in: | Field | Description | |-------|-------------| | **Name** | Kebab-case identifier (e.g., `my-crm-oauth`). Used to reference the app in manifests. | | **Display Name** | Human-readable label shown in the UI. | | **Client ID** | OAuth client identifier from the provider. | | **Client Secret** | OAuth client secret (encrypted at rest). Optional for public clients using PKCE. | | **Default Scopes** | Space-separated OAuth scopes (e.g., `read write`). | | **Use PKCE** | Enable PKCE (Proof Key for Code Exchange). Enabled by default. | | **Authorization Endpoint** | OAuth authorize URL. Optional — auto-discovered from the MCP server's `.well-known/oauth-authorization-server` metadata when omitted. | | **Token Endpoint** | OAuth token exchange URL. Optional — auto-discovered when omitted. | | **Revocation Endpoint** | Token revocation URL. Optional. | For MCP servers that publish OAuth metadata at `.well-known/oauth-authorization-server`, you only need to provide the **Client ID** (and **Client Secret** if required by the provider). The authorization and token endpoints are discovered automatically. See the [OAuth Providers API reference](/api/openapi#tag/oauth-providers) for programmatic management. ### Step 2: Reference the OAuth Provider in the Manifest Add the `auth` and `oauth_app` fields to the MCP collection: ```json { "tool_collections": [ { "type": "mcp", "id": "my_crm", "url": "https://mcp.my-crm-provider.com", "name": "My CRM", "description": "CRM tools for contacts and deals", "namespace": "crm", "auth": "oauth", "oauth_app": "my-crm-oauth" } ] } ``` The `oauth_app` value must match the **Name** of the OAuth Provider you created in Step 1. ### Step 3: Connect Each user must authenticate individually before agents can use the MCP tools on their behalf. **Admins** can verify the configuration works from **Project Settings > Apps**: find the installed app and click **Connect** next to the OAuth-enabled MCP collection to complete the OAuth consent flow. **End users** connect from the **new agent form** in the Agent Runner interface before starting the agent. The form displays connect actions for any OAuth-enabled MCP collections that require authentication. Tokens are stored securely and refreshed automatically. If a refresh token expires or is revoked, the user will need to reconnect. ## Complete Example An app manifest combining embedded OAuth configuration and a public MCP server: ```json { "name": "my-sales-app", "visibility": "private", "title": "Sales Integration", "description": "Connect sales tools to Vertesia agents", "publisher": "My Organization", "status": "stable", "endpoint": "https://my-app.example.com/api/vertesia", "tool_collections": [ { "type": "mcp", "id": "crm", "url": "https://mcp.my-crm-provider.com", "name": "My CRM", "description": "CRM tools for contacts, deals, and companies", "namespace": "crm", "auth": "oauth", "oauth_config": { "grant_type": "authorization_code", "client_id": "shared-client-id", "use_pkce": true, "default_scopes": ["crm.read", "crm.write"], "required_at_install": ["client_secret"] } }, { "type": "mcp", "id": "internal_tools", "url": "https://mcp.internal.example.com/v1", "name": "Internal Tools", "description": "Internal MCP tools", "namespace": "internal" } ] } ``` This example configures: - An `endpoint` for Vertesia SDK tools (discovered automatically) - An MCP server with embedded OAuth configuration — the installer is prompted for a client secret at install time, then an `OAuthProvider` is auto-created in their project - An unauthenticated MCP server --- ## Building an Application UI Source: https://docs.vertesiahq.com/apps/ui Markdown: https://docs.vertesiahq.com/llms/apps/ui.md Applications extend Vertesia with custom React pages. Each app is a unified project containing a **standalone React UI** (frontend) and a **Hono tool server** (backend for custom tools, skills, and interactions), built and deployed as a single unit. Studio embeds customer UIs in sandboxed iframes, so the app owns React and all other browser dependencies. ## Prerequisites - Node.js 24.x and pnpm 11.8.0 through Corepack - [Vertesia CLI](https://www.npmjs.com/package/@vertesia/cli) installed and authenticated (`vertesia auth login`) ## 1. Scaffold Your Plugin ```bash pnpm dlx @vertesia/create-plugin my-plugin ``` You will be prompted for the app name (kebab-case), version, and description. The generated project includes everything needed: React UI, Hono tool server, Vite build configuration, Vercel deployment configuration, and example tools/skills. ## 2. Develop Locally ```bash cd my-plugin npm install npm run dev ``` The generated `dev` script runs Vite in app mode (`vite dev --mode app`), so local development reads `.env.app` and `.env.app.local`. Open `https://localhost:5173`. You'll see: - **`/app/*`** -- your plugin UI with hot module replacement - **`/*`** -- the tool server admin UI (manage tools, skills, interactions) - **`/api`** -- the tool server API endpoint HTTPS is required for authentication. The dev server uses a self-signed certificate. The generated project includes an `.env.app` file: ```bash VITE_APP_NAME=my-plugin ``` This value must match the `name` field in your Vertesia app manifest. It is public Vite build-time configuration, so it is safe to commit. Use `.env.app.local` for local overrides. Generic Vite development env files such as `.env.local` are not required by the generated template. ## 3. Register Your App in Vertesia Create a `manifest.json`: ```json { "name": "my-plugin", "title": "My Plugin", "description": "What this plugin does", "publisher": "your-org", "visibility": "private", "status": "beta" } ``` Register and install in one step: ```bash vertesia apps create --install -f manifest.json ``` This creates the app manifest, installs it in your current project, and grants you access. Verify that `.env.app` uses the same app name: ```bash VITE_APP_NAME=my-plugin ``` Restart `npm run dev` and navigate to `/app/` to see your plugin running with full Vertesia authentication. ## 4. Deploy to Vercel [Vercel](https://vercel.com) is the easiest way to deploy your plugin — its generous free tier is more than enough for development and small-scale production. ```bash npm i -g vercel vercel --prod ``` The template includes a `vercel.json` and `api/index.js` adapter that handles routing: the standalone app is served at `/app`, `/` redirects to `/app`, and API requests go through the serverless function. Vercel builds with `vite build --mode app`, so it reads `.env.app` and `.env.app.local` if present. Do not rely on `.env.local` for deployed builds. If you override `VITE_APP_NAME` in Vercel project settings, keep it equal to the manifest `name`. After deploying, update your app manifest with the production endpoint: ```bash vertesia apps update my-plugin --manifest '{ "endpoint": "https://my-plugin.vercel.app/api" }' ``` The `endpoint` URL tells Vertesia where to find your plugin's tools, skills, interactions, and UI configuration. It replaces the older `ui.src` and `tool_collections` fields. **Important**: Disable deployment protection in Vercel project settings for the plugin to be publicly accessible. ### Isolation Strategies The manifest `ui.isolation` field controls how Studio hosts the application: - **`iframe`** (recommended for customer apps) -- runs the standalone app with its own dependencies and document - **`shadow`** -- legacy host-React plugin mode with Shadow DOM style isolation - **`css`** -- legacy host-React plugin mode in the Studio document; styles may conflict with the host Iframe apps do not receive Studio React contexts. Use Vertesia APIs and the app's own router normally. The iframe bridge keeps the embedded route, active account/project, locale, and light/dark theme synchronized with the Composite App without reloading the iframe. Newly generated apps are available from both the App Portal and Composite Apps; set `ui.available_in` explicitly if an app should appear in only one surface. Studio supplies and refreshes the active user session through an in-memory, source- and origin-checked handshake. The host origin is passed explicitly so authentication survives hard reloads and sign-in redirects. The credential is the viewing user's Studio JWT, not an app-specific reduced-scope token. Consequently, iframe isolation protects Studio's DOM and JavaScript dependency graph, but it is not a credential trust boundary: only configure `ui.src` to an application origin you trust with the viewing user's Vertesia access. Tokens are never placed in the URL or persisted by the bridge. The iframe must be hosted on an origin separate from Studio. This is security-critical because the sandbox enables both scripts and same-origin behavior for a normal standalone application; a same-origin frame could otherwise escape the intended DOM boundary. ## Using Vertesia UI Components For a complete catalog of available components with live examples, props tables, and usage snippets, see the [Components reference](/components/buttons). The `@vertesia/ui` package provides ready-to-use components. Import from subpaths: ```tsx // Core components and hooks import { Button, Card, Input, Spinner, VModal, VTabs, useFetch, useToast } from '@vertesia/ui/core'; // Router import { useNavigate, useParams, NavLink, NestedRouterProvider } from '@vertesia/ui/router'; // Session and auth import { useUserSession } from '@vertesia/ui/session'; // Layout import { FullHeightLayout } from '@vertesia/ui/layout'; ``` ### Fetching Data Use the `useFetch` hook with the Vertesia client: ```tsx import { useFetch, Spinner } from '@vertesia/ui/core'; import { useUserSession } from '@vertesia/ui/session'; function MyPage() { const { client } = useUserSession(); const { data, error } = useFetch( () => client.store.collections.list(), [] ); if (error) return
Failed to load
; if (!data) return ; return
{data.map(item => ...)}
; } ``` ### Using the Vertesia Client ```tsx const { client } = useUserSession(); // Collections and objects const collections = await client.store.collections.list(); await client.store.objects.create( { content: file, name: file.name }, { collection_id: collectionId } ); // Launch an agent await client.runs.create({ interaction: 'app:my_interaction', data: { /* payload */ }, tags: ['my-tag'], }); ``` ### Styling For a complete catalog of available styles with live examples, see the [Styling reference](/components/semantic). Use Tailwind CSS with Vertesia's semantic colors: `primary`, `secondary`, `success`, `attention`, `destructive`, `done`, `info`, `muted`. **Write the bare name — never a `-foreground` or `-background` suffix.** `@vertesia/ui/css/utilities.css` overrides Tailwind's `bg-*` and `text-*` so each bare name resolves to the right role on its own. `bg-success` is not the dark green `--success` variable it appears to name; it resolves to `--success-background`. Reading `color.css` alone will make a correct class look wrong, so read this table instead: | You write | Resolves to | Renders | | --------- | ----------- | ------- | | `bg-success` | `--success-background` | light green surface | | `text-success` | `--success` (no `--success-foreground` exists) | dark green ink | | `border-success`, `fill-success`, `stroke-success`, `ring-success` | `--success` | dark green ink | | `bg-primary` | `--primary` (no `--color-primary-background` is registered in `@theme`) | solid dark blue | | `text-primary`, `text-link` | `--primary-foreground` | **white** | The rule behind the table: `bg-X` prefers `--X-background` and falls back to `--X`; `text-X` prefers `--X-foreground` and falls back to `--X`; every other color utility takes `--X` directly. `bg-success-background` and `text-muted-foreground` still compile, but they bypass the convention and most `*-foreground` variables no longer exist. The bare name is the whole API. **Pair the same base name.** A surface and the text on it take the same semantic. Each utility resolves its own side, so the pair is always legible even though both classes name one token: ```tsx
``` **Blue is `info`, not `primary`.** `primary` is only the solid brand treatment: `bg-primary text-primary` is the primary button — a dark blue block with white text. Neither half stands on its own. `text-primary` and `text-link` render white, so blue text on an ordinary background is `text-info`. `bg-primary` is a solid block rather than a tinted surface, so a readable blue background is `bg-info` paired with `text-info`. Both mistakes fail silently: white on white, no build error. Available utilities are `bg-*`, `bg-mixer-*/[n]`, `text-*`, `text-disabled-*`, `text-mixer-*/[n]`, `border-*`, `border-disabled-*`, `border-mixer-*/[n]`, plus `decoration-*`, `outline-*`, `shadow-*`, `ring-*`, `accent-*`, `caret-*`, `fill-*` and `stroke-*`. The `*-mixer-*/[n]` forms blend the token toward white (light mode) or black (dark mode) by n%. There is no `bg-disabled-*` — `utilities.css` defines only `text-disabled-*` and `border-disabled-*`. For these classes to be generated by Tailwind, your plugin must pull the Vertesia design tokens into its main Tailwind entry CSS so the `@theme` directives are visible at compile time: ```css {{ title: 'src/styles/tailwind.css' }} @import 'tailwindcss'; /* * Pull in the Vertesia design tokens so the Tailwind compilation knows * about the semantic palette (--color-primary, --color-foreground, etc.). * Without this, classes like `bg-primary` and `text-muted` never get * generated as utilities. * * Skip @vertesia/ui/css/base.css if you have your own base layer — it * body-applies selection styles that may conflict with your palette. */ @import '@vertesia/ui/css/color.css'; @import '@vertesia/ui/css/theme.css'; @import '@vertesia/ui/css/utilities.css'; @import '@vertesia/ui/css/custom-tooltips.css'; ``` If the import lives inside a component file (e.g. `import '@vertesia/ui/css/index.css'` from a TSX module) instead of the Tailwind entry CSS, the CSS variables are bundled but Tailwind never sees the `@theme` block at compile time, so the semantic utility classes are silently missing. ## Next Steps The generated project includes a comprehensive README with detailed guides for: - **Creating resources** -- tools, skills, interactions, content types, templates - **Tool server configuration** -- registering collections, settings schema, org restrictions - **Build system** -- dual Rollup/Vite architecture, import hooks - **Debugging with the platform** -- Cloudflare tunnel for local testing with real agents - **Theme customization** -- overriding CSS custom properties in `index.css` --- ## Vertesia CLI Source: https://docs.vertesiahq.com/cli Markdown: https://docs.vertesiahq.com/llms/cli.md The Vertesia CLI provides a set of commands to manage and interact with the Vertesia Platform. This documentation covers all available commands, grouped by their logical functionality, and provides examples on how to use each command. ## Installation To install the CLI, follow these steps: 1. **Ensure Node.js is installed**: The Vertesia CLI requires Node.js. You can download and install it from [nodejs.org](https://nodejs.org/). You can verify Node.js is installed by running: ```bash node --version ``` 2. **Install the Vertesia CLI globally**: Open your terminal and run the following command to install the Vertesia CLI globally using npm: ```bash npm install -g @vertesia/cli ``` 3. **Verify the installation**: After installation, you can verify that the Vertesia CLI is installed correctly by running: ```bash vertesia --version ``` This command displays the version of the CLI installed on your system. ## Help To get help with the CLI. ```bash vertesia --help ``` To get help with any CLI command. ```bash vertesia help [command] ``` Option to get help with any CLI command. ```bash vertesia --help ``` ## Authentication Commands to manage authentication. ### Commands - `auth token`: Get a JWT token for the API key used in the authentication. ```bash vertesia auth token ``` - `auth refresh`: Refresh the JWT token. ```bash vertesia auth pk --name --ttl ``` ## Profiles Commands to manage configuration profiles which enable using the CLI with different Vertesia accounts, projects, and infrastructure. ### Commands - `profiles show [name]`: Show the configured profiles or the profile with the given name. ```bash vertesia profiles show [name] ``` - `profiles use [name]`: Switch to another configuration profile. ```bash vertesia profiles use [name] ``` - `profiles add [name] [options]`: Create a new configuration profile. ```bash vertesia profiles add [name] --target ``` - `profiles edit [name]`: Edit an existing configuration profile. ```bash vertesia profiles edit [name] ``` - `profiles refresh`: Refresh token for the current configuration profile. ```bash vertesia profiles refresh ``` - `profiles delete `: Delete an existing configuration profile. ```bash vertesia profiles delete ``` - `profiles file`: Print the configuration file path. ```bash vertesia profiles file ``` ## Projects Command to list projects. ### Command - `projects`: List the projects you have access to. ```bash vertesia projects ``` ## Environments Command to list environments. ### Command - `envs [envId]`: List the environments you have access to. ```bash vertesia envs [envId] ``` ## Interactions Commands to list interactions, generate test data, run, and search interactions. ### Commands - `interactions`: List the interactions available in the current project. ```bash vertesia interactions ``` - `interactions `: List the details of an interaction given its ID. ```bash vertesia interactions ``` - `datagen [options]`: Generate test data for an interaction given its ID. ```bash vertesia datagen --env --model --temperature --output --count ``` - `run [options]`: Run an interaction by ID. ```bash vertesia run --input --output --data --tags --temperature --model --env --no-stream --count --verbose --jsonl --data-only ``` - `runs [options]`: Search the run history by interaction ID. ```bash vertesia runs --tags --status --env --model --query --limit --page --format --output --before --after ``` ## Content Objects Commands to manage content objects. ### Commands - `content post [options]`: Post a new object to the store. The path to the file can include wildcards by using `*`. ```bash vertesia content post --name --type --mime --path --recursive ``` - `content delete `: Delete an existing object given its ID. ```bash vertesia content delete ``` - `content get `: Get an existing object given its ID. ```bash vertesia content get ``` - `content list [options]`: List the objects inside a folder. ```bash vertesia content list --limit --skip ``` ## Events Commands to manage event subscriptions. Event subscription payloads currently use `scope: "project"`. Account-scoped subscriptions are reserved for a future account-wide automation model. ### Commands - `events subscriptions list`: List event subscriptions visible to the current project. ```bash vertesia events subscriptions list ``` - `events subscriptions get [options]`: Get an event subscription by ID. ```bash vertesia events subscriptions get --file ``` - `events subscriptions create [options]`: Create an event subscription from a JSON file. ```bash vertesia events subscriptions create --file ``` - `events subscriptions apply [subscriptionId] [options]`: Create or update an event subscription from a JSON file. ```bash vertesia events subscriptions apply --file ``` - `events subscriptions update [options]`: Update an event subscription from a JSON file. ```bash vertesia events subscriptions update --file ``` - `events subscriptions delete `: Delete an event subscription by ID. ```bash vertesia events subscriptions delete ``` ## Workflow Commands to manage workflow definitions. ### Commands - `workflows definitions transpile [options]`: Transpile a TypeScript workflow definition to JSON. ```bash vertesia workflows definitions transpile --out ``` - `workflows definitions create [options]`: Create a new workflow definition. ```bash vertesia workflows definitions create --file ``` - `workflows definitions apply [workflowId] [options]`: Apply a workflow definition. ```bash vertesia workflows definitions apply [workflowId] --file --skip-validation ``` - `workflows definitions list`: List all workflow definitions. ```bash vertesia workflows definitions list ``` - `workflows definitions get [options]`: Get a workflow definition given its ID. ```bash vertesia workflows definitions get --file ``` - `workflows definitions delete `: Delete a workflow definition given its ID. ```bash vertesia workflows definitions delete ``` ## Process Tests Commands to run deterministic process test scenarios against an in-code process (an `app:` or `sys:` process) or an inline process definition. Scenarios live in a JSON file next to the process definition in the app repository, so they can be run from CI. The file mirrors the API payload: ```json { "process": { "id": "app:acme:invoice-review", "app_version": "1.4.2" }, "scenarios": [ { "id": "happy-path", "name": "Happy path", "timeout_seconds": 60, "initial_context": {}, "actors": [], "fixtures": [], "human_actions": [], "assertions": { "status": "completed" } } ] } ``` Use `"process": { "definition": { ... } }` instead of `"process": { "id": ... }` to test a definition body that is not published yet. `process-tests run` waits for the run to finish by default and exits with a CI-friendly status code: `0` when every scenario passed, `1` on a failed or cancelled run (including a scenario timeout), and `2` when the tests could not be started at all — an unreadable file, an invalid payload, or an API error. ### Commands - `process-tests run [options]`: Submit the scenarios in the file and wait for the result. ```bash vertesia process-tests run process-tests.json --app-version 1.4.2 --json ``` - `process-tests get [options]`: Show a run and its scenario results, optionally waiting for it to reach a terminal status. ```bash vertesia process-tests get --watch ``` - `process-tests cancel `: Cancel a pending or running test run. ```bash vertesia process-tests cancel ``` ## Package Management Commands to manage the Vertesia CLI package. This command checks if there is a newer version of the CLI available. If a new version is found, it prompts the user to confirm the upgrade. If the user confirms, the Vertesia CLI will update itself to the latest version. If no updates are available, it will notify the user. ### Commands - `upgrade`: Upgrade to the latest version of the CLI. ```bash vertesia upgrade ``` --- ## Concepts Source: https://docs.vertesiahq.com/concepts Markdown: https://docs.vertesiahq.com/llms/concepts.md This page maps the Vertesia platform: what each building block is, and where to read more. Each section of the documentation covers one of these areas in depth. ## The building blocks | Concept | What it is | Learn more | | --- | --- | --- | | **Prompts** | Reusable text templates with variable injection, assembled into the prompt for a task. | [Prompts](/studio/prompts) | | **Interactions** | A task you ask a model to perform: prompts, an optional result schema, and a model configuration. | [Interactions](/studio/interactions) | | **Agents** | An interaction that runs as an autonomous loop, calling tools and reasoning over their results until it reaches an answer. | [Agents](/agent-runner/overview) | | **Processes** | Durable state-machine workflows with named steps, deterministic routing, human pauses, and child subprocesses. | [Processes](/processes/overview) | | **Content Types & Collections** | Structured document storage: JSON Schema validation for document shape, collections for organization. | [Intake Configuration](/content/intake-policies) | | **Content Preparation** | Turning uploaded documents into data your agents can use — extraction, markdown conversion, embeddings. | [Content Preparation](/semantic/overview) | | **Views** | Reusable content navigation, faceted or agentic search, and result layouts, rendered in Studio or your own app. | [View Experiences](/content/view-experiences) | | **Events** | Platform events and the subscriptions that route them to a workflow, agent, process, or webhook. | [Event Subscriptions](/events/subscriptions) | | **Environments** | Connections to the LLM inference providers that run your models. | [Environments](/environments) | | **Data Platform** | SQL data stores and dashboards for structured data and analytics. | [Data Platform](/data-platform/overview) | | **Access Control** | Roles and RBAC grants for platform actions; ABAC rules for per-document access. | [Access Control](/security/overview) | | **Custom Applications** | Apps, MCP servers, and UI plugins that extend Studio or embed it elsewhere. | [Custom Applications](/apps/overview) | The [Studio Assistant](/studio/assistant) can build most of the above for you, and is the fastest way to get a first version of something working. ## Choosing building blocks Two decisions come up early, and getting them right saves the most rework. ### Direct call or an agent? An interaction always has a prompt and usually a schema. The question is whether the model needs to **act** or just **respond**. Use direct inference — a single model **call** — when everything the model needs is already in the input. Classify, extract, summarize, translate, rewrite. It is faster, cheaper, and predictable. Use an **agent** when the task needs tool use or iteration: - it must look things up — search, fetch documents it cannot name in advance, call an external API; - each step depends on the previous one; - the reasoning is too hard to land reliably in one pass and benefits from planning and revision; - a person needs to steer it mid-run. Start with direct inference and promote to an agent when the task clearly needs it. "It might occasionally need to search" is not a reason — that is just a larger prompt. ### An agent or a process? Use an **agent** when one autonomous worker can reason its way to the result. Use a **process** when the workflow itself is part of what you are delivering and needs explicit named steps, deterministic routing, durable pauses for human work, reusable subprocesses, or fixed parallel branches. Business workflows of the kind you would normally draw as a flowchart map to processes, not agents. ## Agent capabilities An agent's abilities come from the tools it can call. Tools are granted through **skills** — a skill bundles a whole domain's tools together with instructions on how to use them, so you grant one skill rather than a list of individual tools. For example, a research agent needs web search and document search; a document-authoring agent needs the authoring and file-editing skills. See [Skills](/agent-runner/skills). A single agent run can also fan out into parallel **[workstreams](/agent-runner/workstreams)**, each a child run with its own task. ## Runs A run is one execution of an interaction call — both the request to the model and the response from it. The run exists but has not started. Typically waiting for a client to begin streaming. The run is executing. The run finished successfully. The run failed. The reason is in the `error` field. ## Prompt template syntax Each prompt template has a `content_type` that determines how variables are injected into its content. [Handlebars](https://handlebarsjs.com/) syntax with double curly braces. Supports `{{variable}}` references, conditionals (`{{#if}}`/`{{else}}`/`{{/if}}`), loops (`{{#each items}}`), and helpers such as `{{_now}}` and `{{stringify obj}}`. Set `content_type` to `"handlebars"`. A JavaScript template engine in a sandbox, for composition beyond what Handlebars can express. Standard interpolation (`${var}`), control blocks, and array functions, plus a `_` utility object with `_.loadCsv()`, `_.jsonToCsv()`, `_.stringify()`, `_.addLineNumbers()` and `_.dayjs()`. The template must return a string. Set `content_type` to `"jst"`. Use it for data transformations, CSV processing, or programmatic prompt construction. ## Inference providers Environments connect to the platforms that run generative models. Vertesia supports: - `anthropic` - [Anthropic](https://platform.claude.com/) - `azure_foundary` - [Microsoft Foundary](https://azure.microsoft.com/en-us/products/ai-foundry) - `bedrock` - [Amazon Bedrock](https://aws.amazon.com/bedrock/) - `groq` - [Groq Cloud](https://groq.com) - `huggingface_ie` - [Hugging Face Inference Endpoint](https://huggingface.co/inference-endpoints/dedicated) - `mistralai` - [Mistral AI](https://docs.mistral.ai/deployment/laplateforme/overview/) - `openai` - [OpenAI](https://platform.openai.com) - `openai_compatible` - Any OpenAI-compatible provider such as [OpenRouter](https://openrouter.ai/) or [Cerebras](https://www.cerebras.ai/inference) - `replicate` - [Replicate](https://replicate.com) - `togetherai` - [TogetherAI](https://www.together.ai) - `vertexai` - [Google Agent Platform (Vertex AI)](https://cloud.google.com/vertex-ai) - `watsonx` - [IBM watsonx.ai](https://www.ibm.com/products/watsonx-ai) - `xai` - [xAI (Grok)](https://x.ai/api) Virtual providers assemble several models into one synthetic model with a balancing or mediation strategy: - `virtual_lb` - load balancing and failover across multiple models - `virtual_mediator` - multi-head execution with LLM mediation See [Environments](/environments) for provider setup. ## Language model background If generative AI is new to you, these are the ideas the rest of the documentation assumes. Large language models interpret and produce human language — including programming languages. They are pre-trained on very large content sets and can be fine-tuned afterwards for specific knowledge, though Retrieval Augmented Generation (RAG) is usually the better way to give a model access to your own data. Chatbots were the first well-known application, but a model is more usefully thought of as a processing entity you can call automatically, which is what opens up the range of use cases this platform is built for. | Term | Description | | --- | --- | | Context Window | How much text the model can consider at once, measured in tokens. Like a notebook with a fixed number of pages: once it is full, only what is written there can be seen. Each model has its own limit. | | Token | The unit of text a model processes — a word, part of a word, a character, or punctuation. Splitting text into tokens is called tokenization. | | Max tokens | The maximum number of tokens the model may generate in its response. On some models this counts against the context window. | | Embeddings | High-dimensional vectors representing text in a way that captures meaning, learned during training. They are what makes semantic search possible. | | Similarity | How close two pieces of text are in meaning. "Happy" and "joyful" sit near each other in embedding space; "happy" and "fast" sit far apart. | --- ## Collaborative document editing Source: https://docs.vertesiahq.com/content/collaborative-editing Markdown: https://docs.vertesiahq.com/llms/content/collaborative-editing.md Collaborative editing gives you and an AI agent a shared Markdown working copy. You can edit the document directly, ask the agent for changes, review the result, and decide when to publish it back to the content object. The canonical document does not change while you collaborate. Studio creates a working-copy artifact for the editing session and publishes it only when you select **Save to document**. ## Start an editing session 1. Open a Markdown document under **Content > Objects**. 2. Select **Edit with AI** (the sparkle action). 3. Choose an environment and model if you do not want the project's default agent model. 4. Start the editing conversation and describe the first change. Studio hydrates `drafts/.md` from the current document revision before the agent's first turn. The agent reads and edits that artifact, not the canonical document. If you reopen the same document, Studio reattaches to the active editing session when one exists for you and the document. The environment and model are fixed after a conversation starts. Start a new conversation to use another configuration. ## Choose an editing view The workspace keeps the working copy on the left and the editing conversation on the right. | View | Use it for | |---|---| | **Document** | Read the rendered Markdown, select a block, edit or comment on it, and insert content near it. | | **Full editor** | Make broader rich-text changes, format content, edit tables, and use the change ruler to jump to modified regions. | | **Review changes** | Compare the working copy with the document revision that started the session. | ### Edit document components In **Document** view, move over a block to show its contextual actions. Depending on the block, you can: - edit the selected text with the rich-text editor - comment on the selection so the agent can improve it - insert a paragraph, heading, list, link, chart, code block, table, or other supported component - add or remove table rows and columns from the table menu A direct edit is applied to the working copy first. The agent receives the applied change as context and continues from the updated artifact; it is not asked to repeat your edit. ### Edit the complete document The **Full editor** saves changes to the working-copy artifact after a short debounce. Its change ruler marks regions that differ from the original document. Select **Send changes to agent** when you want the agent to receive a compact diff of your direct edits and continue from them. Vertesia-specific Markdown elements that have a rich editor representation remain interactive. Unsupported custom blocks are kept as opaque Markdown so saving the document does not silently rewrite them. Before opening the full rich-text editor, Studio checks whether parsing and serializing the Markdown would change the source. If it would, Studio asks you to choose between **Edit Markdown source**, which preserves the exact text, and **Continue with rich text**, which accepts the displayed normalization. YAML frontmatter is preserved as an opaque document header, but other valid Markdown styles, such as setext headings, can still be normalized by the rich-text serializer. ## Collaborate with the agent Ask for changes in normal language, or attach files containing review comments to apply several requests together. The agent is instructed to preserve unrelated content and edit only the working-copy artifact. Artifact update events refresh the document view as the agent works. Changed blocks briefly highlight after a refresh. Use the refresh action in the workspace header to reload the artifact if the view appears stale. You can also select **Summarize changes** from **Review changes** to ask the agent for a concise changelog without modifying the working copy. ## Editing lock Direct editing is locked while the agent is changing the working copy. A visible banner explains that the AI is working, and the editor, **Send changes to agent**, and **Save to document** remain disabled until the turn finishes. The workspace reloads the final artifact when the agent returns to idle. Select **Unlock editing** if you must make a change before the agent finishes. The warning remains visible and offers **Lock editing** so you can restore the guard. Concurrent edits can conflict, so manual unlock is best used as a recovery control rather than the normal workflow. ## Review and publish Before publishing: 1. Open **Review changes** and inspect additions and removals against the session's original revision. 2. Return to **Document** or **Full editor** for any final adjustments. 3. Select **Save to document**. 4. Choose **Update current version** or **Create new version**. A new version can include an optional label. Creating a version preserves the previous revision and navigates Studio to the new one after the save. Updating the current version keeps the same revision identity. Publishing also persists supported run-local artifact links, such as images, into document storage and rewrites the links so the saved document does not depend on the editing session. ## Conflicts and recovery Every publish checks that the canonical document is still the revision on which the session started. If another user or workflow updated it, the save is rejected without discarding your working copy. Select **Ask agent to merge** to fetch the latest canonical revision and merge its changes into the working copy. Review the result, then save again. Other recovery controls: - **Refresh** reloads the working-copy artifact when streamed updates appear incomplete. - Starting a new conversation resets the working copy; Studio asks for confirmation when it contains unpublished changes. - A failed direct-editor merge keeps your local text visible and offers a retry instead of replacing it with the remote artifact. ## Working-copy boundaries The working copy is the source of truth during the session. Agent tools that directly update or create canonical documents are excluded from the editing conversation, and the agent cannot bypass **Save to document**. This keeps review and publication under user control while still allowing full-control artifact editing inside the session. --- ## Embeddings Configuration Source: https://docs.vertesiahq.com/content/embeddings Markdown: https://docs.vertesiahq.com/llms/content/embeddings.md Embeddings are numerical representations of content that capture semantic meaning, enabling powerful similarity search and content understanding. Vertesia supports multiple embedding types to index different aspects of your documents. ## Understanding Embeddings An embedding converts content (text, images, or structured data) into a high-dimensional vector. Documents with similar meanings have vectors that are close together in this space, enabling semantic search that goes beyond keyword matching. For example, a document about "machine learning algorithms" would have an embedding similar to one about "AI model training" because they share semantic meaning, even though they use different words. ## Embedding Types Vertesia supports three types of embeddings, each capturing different aspects of your documents: ### Text Embeddings Text embeddings capture the semantic meaning of document content. When enabled, Vertesia: 1. Extracts text content from documents (including OCR for images/PDFs) 2. Chunks text to fit within model token limits 3. Generates embeddings using the configured model 4. Stores vectors for similarity search **Use cases:** - Finding documents with similar content - Natural language queries - Content recommendations ### Image Embeddings Image embeddings (also called vision embeddings) capture visual features of images and document pages. When enabled: 1. Document pages/images are processed by a vision embedding model 2. Visual features are encoded as vectors 3. Enables similarity search based on visual content **Use cases:** - Finding visually similar documents - Image-based search - Visual content analysis ### Properties Embeddings Properties embeddings index the structured metadata of documents. This includes: - Custom properties defined in your schema - Document type information - Tags and categories **Use cases:** - Finding documents with similar metadata - Structured data similarity - Schema-aware search ## Configuration Options Each embedding type can be configured independently with these settings: | Setting | Description | |---------|-------------| | **environment** | The AI environment to use for generating embeddings | | **model** | The specific embedding model (e.g., `text-embedding-ada-002`, `text-embedding-3-small`) | | **dimensions** | The number of dimensions for the embedding vectors | | **max_tokens** | Maximum tokens per chunk (for text embeddings) | | **enabled** | Whether this embedding type is active | ### Environment Selection Select an environment that provides access to embedding models. Common choices include: - **OpenAI**: `text-embedding-ada-002` (1536 dimensions), `text-embedding-3-small` (1536 dimensions), `text-embedding-3-large` (3072 dimensions) - **Google Vertex AI**: Various embedding models - **AWS Bedrock**: Titan and other embedding models ### Dimension Selection Embedding dimensions affect both quality and performance: | Dimensions | Trade-offs | |------------|------------| | **Lower (256-512)** | Faster search, less storage, potentially lower quality | | **Medium (1024-1536)** | Good balance of quality and performance | | **Higher (2048-3072)** | Higher quality, more storage, slower search | Most projects work well with 1536 dimensions (OpenAI's default). ## Enabling Embeddings via API ### Get Embeddings Status Check the current status of an embedding type: ```bash {{title: 'cURL'}} curl --location --request GET \ 'https://api.vertesia.io/api/v1/commands/embeddings/text/status' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ site: 'api.vertesia.io', apikey: '', }); const status = await client.commands.embeddings.status('text'); console.log(status); ``` **Example Response:** ```json { "status": "idle", "embeddingRunsInProgress": 0, "totalRunsInProgress": 0, "embeddingsModels": ["text-embedding-ada-002"], "vectorIndex": { "status": "READY", "name": "vsearch_text", "type": "hnsw" } } ``` ### Enable Embeddings Activate embeddings for a specific type: ```bash {{title: 'cURL'}} curl --location --request POST \ 'https://api.vertesia.io/api/v1/commands/embeddings/text/enable' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ "environment": "", "max_tokens": 500, "dimensions": 1536, "model": "text-embedding-ada-002" }' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ site: 'api.vertesia.io', apikey: '', }); const response = await client.commands.embeddings.activate('text', { environment: '', max_tokens: 500, dimensions: 1536, model: 'text-embedding-ada-002', }); console.log(response); ``` ### Disable Embeddings Deactivate embeddings for a specific type: ```bash {{title: 'cURL'}} curl --location --request POST \ 'https://api.vertesia.io/api/v1/commands/embeddings/text/disable' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ site: 'api.vertesia.io', apikey: '', }); const response = await client.commands.embeddings.disable('text'); console.log(response); ``` ## Recalculating Embeddings You may need to recalculate embeddings when: - Switching to a different embedding model - Changing dimension settings - Fixing corrupted or missing embeddings - Upgrading to a newer model version ### Trigger Recalculation ```bash {{title: 'cURL'}} curl --location --request POST \ 'https://api.vertesia.io/api/v1/commands/embeddings/text/recalculate' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ site: 'api.vertesia.io', apikey: '', }); const response = await client.commands.embeddings.recalculate('text'); console.log(response); ``` Recalculation runs as a background process. Monitor progress through the status endpoint. ## Vector Index Status The vector index status indicates the state of your search index: | Status | Description | |--------|-------------| | **READY** | Index is operational and searchable | | **PENDING** | Index is being created or updated | | **FAILED** | Index creation failed (check configuration) | | **DOES_NOT_EXIST** | No index exists (enable embeddings first) | ## Best Practices ### Model Selection - **Start with standard models**: `text-embedding-ada-002` or `text-embedding-3-small` work well for most use cases - **Consider costs**: Higher-dimension models cost more to generate and store - **Test with your data**: Different models perform better on different content types ### Token Limits - **500-1000 tokens**: Good for short documents, faster processing - **2000-4000 tokens**: Better for longer documents, captures more context - **Consider chunking**: Very long documents are automatically chunked ### Performance Optimization - Enable only the embedding types you need - Use appropriate dimensions for your quality requirements - Monitor index status to ensure embeddings are being generated ## Troubleshooting ### Embeddings Not Generating 1. Check that the embedding type is enabled in project settings 2. Verify the environment has a valid API key 3. Ensure the model is available in your environment 4. Check for quota limits on your AI provider ### Dimension Mismatch Errors If you change embedding dimensions, you may encounter mismatches: 1. Recalculate all embeddings with the new dimensions 2. Or recreate the vector index (requires reindexing all documents) ### Slow Embedding Generation - Large backlogs process over time - Check `embeddingRunsInProgress` in status - Consider enabling parallel processing in workflows ## Next Steps - [Search Configuration](/content/search) - Configure search backends - [Content Overview](/content/overview) - Understand the full search architecture - [Commands API](/api/openapi#tag/commands) - API reference for embedding commands --- ## Intelligent Document Processing (IDP) Source: https://docs.vertesiahq.com/content/grounded-extraction Markdown: https://docs.vertesiahq.com/llms/content/grounded-extraction.md Vertesia IDP extracts structured data from digital, scanned, and handwritten documents. Every extracted value keeps a citation to its source, and the result records how each value was verified before it is used by downstream systems. IDP is designed for documents such as purchase orders, invoices, order acknowledgements, applications, and forms where structured fields are more useful than a page-by-page transcription. ## What IDP produces An IDP run can produce: - properties shaped by the content type schema - page and region citations for extracted values - a verification breakdown for digital, AI-verified, and unverified values - a document-level `good_to_go` or `needs_review` verdict - applied reviewer corrections and unresolved issues - downloadable source, citation, and annotated proof artifacts - a `document_processed` event for processes, webhooks, and operational reporting The result updates the content object's properties through standard intake. It can also render those properties into concise markdown for search, embeddings, and agents. ## How it works 1. **Prepare the pages.** Vertesia uses the document text layer where it is reliable. Scanned or handwritten pages are prepared for visual extraction or OCR according to the policy. 2. **Extract against the schema.** The extraction model returns structured data and identifies the source location for every value. 3. **Verify citations.** Values are matched against digital text or OCR evidence where possible. 4. **Review when required.** A review model examines values that are not fully verified, difficult pages, and possible omissions. Confident source-grounded corrections are applied automatically; unresolved uncertainty remains an issue. 5. **Issue a verdict.** After corrections, the reviewer decides whether the result is faithful and complete enough for downstream processing. Business rules can still run in downstream systems. 6. **Publish the outcome.** The object stores the extraction result and Vertesia publishes an operational event that can start the next process. ## Verification states The verification breakdown counts values, not model confidence claims: | State | Meaning | |---|---| | **Digitally verified** | The extracted value matches source text from the digital document or its OCR representation. | | **AI verified** | The reviewer confirmed the value directly against the page image. This is the primary verification path for handwriting and other content without dependable digital text. | | **Unverified** | The value was read from the page but has not been confirmed by text matching or the reviewer. | Studio displays the counts separately so operators can distinguish deterministic text verification from visual model verification. Selecting a value highlights its citation on the source page. The document also carries a verification score and a separate complexity score. Complexity describes how difficult the source was to read; it can route difficult content to a stronger extraction model without being confused with the quality of the final result. ## The reviewer verdict When review is enabled, the reviewer evaluates source fidelity, semantic meaning, and material completeness after applying confident corrections: - `good_to_go` means the extracted data is faithful and complete enough to enter downstream processing. - `needs_review` means material uncertainty or an unsafe correction remains and a person or another process should inspect the result. The reviewer is not a replacement for domain validation. A downstream order process can still check product identifiers, totals, account status, or other business rules. ## Configure IDP in Studio IDP is part of standard intake and is configured per content type: 1. Open **Content > Types** and select the type. 2. Open the **Intake** tab. 3. In **Extraction**, enable property extraction and choose the evidence source. 4. Open **Grounding** and enable IDP grounding. 5. Select the primary, hard-content, and review models as needed. 6. Enable review for documents that should receive an AI verification pass. 7. Save the policy and reprocess existing documents, or upload new documents normally. Project-wide defaults live under **Settings > Project > Intake**. A content type inherits those defaults and overrides only the fields configured on the type. See [Intake Configuration](/content/intake-policies) for the full policy hierarchy and combined IDP/markdown examples. ## Example policy ```json { "text_conversion": { "enabled": false }, "extraction": { "enabled": true, "source": "mixed", "grounding": { "enabled": true, "use_vision": true, "raster_mode": "vision", "hardness_threshold": 0.5, "min_citation_density": 0.3, "review": { "enabled": true, "coverage_threshold": 0.2 } } }, "rendering_template": "# Order {{properties.order_number}}\n\n**Vendor:** {{properties.vendor_name}}\n**Total:** {{properties.total_amount}}" } ``` In this setup, IDP produces the structured properties and a rendering template produces the object's markdown text. Enable [Markdown Conversion](/semantic/getting-started) as well when the complete document narrative must remain available. ### Common grounding settings | Setting | Purpose | |---|---| | `enabled` | Enables IDP for the type. | | `use_vision` | Lets extraction use the page image in addition to document text. | | `raster_mode` | Uses direct visual reading or OCR for image-only pages. | | `config` | Primary extraction model configuration. | | `hard_config` | Model configuration used for difficult content. | | `hardness_threshold` | Complexity level that activates the hard-content model. | | `window_pages` | Limits pages per extraction call for long documents. | | `min_citation_density` | Rejects results that cite too few extracted values. | | `review.enabled` | Enables post-extraction verification and correction. | | `review.config` | Review model configuration. | | `review.force` | Reviews every result, regardless of complexity. | | `review.coverage_threshold` | Reviews results when a page appears under-cited. | The structured form covers the common fields. The **JSON** tab remains available for advanced settings and validates the policy against the current schema. ## Control what is extracted The content type's `object_schema` defines the result shape. Mark fields that are populated by downstream systems with `"x-extract": false` so they are not sent to the extraction model. ```json { "type": "object", "properties": { "order_number": { "type": "string" }, "erp_order_id": { "type": "string", "x-extract": false }, "line_items": { "type": "array", "items": { "type": "object", "properties": { "sku": { "type": "string" }, "quantity": { "type": "number" }, "catalog_match_id": { "type": "string", "x-extract": false } } } } } } ``` ## Inspect the result Open the object and select its IDP result view. The page viewer and structured data are shown together: - select a field to highlight its cited region - switch between the original and instrumented page when visual grid grounding was used - inspect whether the value was digitally verified, AI verified, or remains unverified - review the final verdict, reason, corrections, and unresolved issues - download the complete result JSON and proof artifacts when deeper inspection is required The full result is stored as `grounded-extraction.json`. It contains the extracted data, citations, page locations, verification information, complexity, verdict, and review record. ## SDK access Normal processing should use standard intake so the complete project and content type policies are applied. The analyze API is also available for a deliberate IDP rerun on an existing object: ```ts await client.objects.analyze(objectId).startGroundedExtraction(); ``` Read the completed operational result: ```ts const result = await client.objects.analyze(objectId).getGroundedExtractionResult(); console.log(result.verdict); console.log(result.verification); console.log(result.data); ``` ## Trigger downstream processing After IDP completes, Vertesia publishes a `document_processed` event containing the object, run, verdict, verification counts, model information, result location, and workflow identifiers. Event Bus subscriptions can: - start a process only for `good_to_go` documents - route `needs_review` documents to a task inbox - call a signed webhook - feed operational dashboards and audit reporting See [Event Subscriptions](/events/subscriptions#idp-document-events) for the event contract and subscription examples. --- ## Intake Configuration Source: https://docs.vertesiahq.com/content/intake-policies Markdown: https://docs.vertesiahq.com/llms/content/intake-policies.md Intake configuration controls how uploaded content is classified, converted, extracted, verified, rendered, and embedded. Configure sensible defaults once for the project, then override only the settings that differ for a content type. For example: - contracts can be converted to complete markdown and have selected terms extracted - purchase orders can use IDP to produce verified structured fields without converting every page - scanned forms can use page images for extraction and a review model for difficult handwriting - media types can disable transcription or selected embedding kinds ## Configuration hierarchy Vertesia resolves intake settings from most specific to least specific: 1. explicit settings supplied for one processing run 2. the document's content type policy 3. the project's default intake policy 4. platform defaults An unset field inherits the next level. A content type does not need to repeat the complete project policy. If intake assigns a content type while processing a document, the policy is resolved again so the newly assigned type controls conversion, extraction, IDP, output, and embeddings in the same run. ## Project intake settings Open **Settings > Project > Intake** to configure project-wide behavior. The **Intake processing** section controls: | Setting | Purpose | |---|---| | **Automatic processing** | Runs standard intake for new uploads. When disabled, uploads remain unprocessed until intake is enabled and they are reprocessed. | | **Content type assignment** | Selects an existing active content type during intake. | | **Default content type** | Used when no content type matches. If unset, Vertesia uses `GenericDocument`. | | **Property extraction** | Extracts properties defined by the resolved content type schema. | | **Table of contents** | Generates document sections for prepared text. An optional size limit skips this step for very large documents. | The **Default intake policy** below those controls uses the same structured editor as content types. It defines the baseline conversion, extraction, IDP, output, and embedding behavior for the project. ## Content type policies Open **Content > Types**, select a type, and open its **Intake** tab. The editor is organized by processing stage: | Tab | Configures | |---|---| | **Overview** | Processing mode, type-identification guidance, page location, table of contents, and the preferred object view. | | **Conversion** | Whether and how the source becomes text or markdown, including instructions and page selection. | | **Extraction** | Structured property extraction, evidence source, page selection, and visual evidence limits. | | **Grounding** | IDP citations, OCR or vision behavior, extraction models, complexity escalation, and review. | | **Output** | Markdown rendering from properties and per-type embedding switches. | | **JSON** | The complete policy for advanced settings, with schema validation and autocomplete. | Use **Examples** to start from a common policy and then adjust it. The form and JSON tabs edit the same policy; JSON is a fallback for advanced configuration, not a separate mode. ## Conversion The conversion policy determines whether the original source becomes document text. | Field | Purpose | |---|---| | `text_conversion.enabled` | Enables conversion or transcription. Defaults to enabled. | | `text_conversion.method` | `auto`, `basic`, `llm`, or `custom`. | | `text_conversion.output_format` | `markdown` or `text`. | | `text_conversion.instructions` | Describes what the converter should preserve or omit. | | `text_conversion.scope` | Uses all pages or pages selected by the locate pass. | | `text_conversion.page_ranges` | Static inclusive page ranges; negative positions count from the end. | Use `basic` for mechanical text extraction and `llm` for [Markdown Conversion](/semantic/getting-started) when page layout, tables, and reading order matter. Disabling conversion is useful for extraction-first documents. Intake can still create useful document text by rendering the extracted properties with an output template. ## Property extraction Extraction shapes document properties using the content type's schema. | Field | Purpose | |---|---| | `extraction.enabled` | Enables or disables property extraction. | | `extraction.source` | Uses `auto`, `text`, `vision`, or `mixed` evidence. | | `extraction.instructions` | Adds type-specific extraction guidance. | | `extraction.scope` / `page_ranges` | Limits extraction to relevant pages. | | `extraction.max_pages` | Caps the pages considered by standard extraction. | | `extraction.vision` | Controls the allowed image detail levels and evidence budget. | | `extraction.grounding` | Enables [IDP](/content/grounded-extraction) with citations and verification. | Choose `text` for clean digital content, `vision` when the page image is authoritative, and `mixed` when both text and layout matter. `auto` selects the available evidence for the source. ## Locate relevant pages Long documents can use a locate pass before conversion or extraction. The pass maps the document and selects pages relevant to the content type, avoiding the cost and prompt size of sending every page to later model calls. ```json { "locate": { "instructions": "Find commercial terms, payment schedules, and signature pages.", "detail": 16, "min_pages": 8 }, "text_conversion": { "scope": "located" }, "extraction": { "scope": "located" } } ``` Use static `page_ranges` when the document layout is fixed. Static ranges take precedence over a located scope. ## IDP for structured documents Enable grounding when extracted values need citations, verification, and an operational verdict: ```json { "text_conversion": { "enabled": false }, "extraction": { "enabled": true, "source": "mixed", "grounding": { "enabled": true, "use_vision": true, "review": { "enabled": true } } }, "rendering_template": "# Order {{properties.order_number}}\n\n**Vendor:** {{properties.vendor_name}}\n**Total:** {{properties.total_amount}}" } ``` This setup extracts verified properties and creates concise markdown from those properties. Configure extraction, hard-content, and review models in the **Grounding** tab when they should differ from the inherited defaults. See [Intelligent Document Processing](/content/grounded-extraction) for verification tiers, reviewer behavior, output artifacts, and document processing events. ## Markdown conversion with extraction Enable conversion and extraction together when users need the complete document narrative and selected structured fields: ```json { "text_conversion": { "enabled": true, "method": "llm", "output_format": "markdown" }, "extraction": { "enabled": true, "source": "text" }, "embeddings": { "text": true, "properties": true } } ``` ## Rendering and embeddings `rendering_template` is a Handlebars template evaluated against extracted properties. It produces canonical markdown when a document has no converted or manually authored text. ```handlebars # Invoice {{properties.invoice_number}} **Vendor:** {{properties.vendor_name}} **Due:** {{properties.due_date}} **Total:** {{properties.currency}} {{properties.total_amount}} ``` If no template is configured, intake can generate a default property rendering. Converted, transcribed, or manually edited text is not overwritten by property rendering. The `embeddings` policy controls `text`, `properties`, and `image` embeddings for the type. A type can disable an embedding enabled by the project, but it cannot enable an embedding kind that the project has not configured. ## Type identification Identification guidance helps automatic type assignment distinguish similar documents: ```json { "identification": { "guidance": "European furniture import invoices with EUR amounts, incoterms, and VAT.", "distinguish_from": "Credit notes and domestic invoices use different content types." } } ``` Only **active** content types participate in automatic assignment. Keep a type in **draft** while its schema and policy are being prepared. ## Configure with the SDK Update a content type policy: ```ts await client.store.types.update(typeId, { intake: { text_conversion: { enabled: false }, extraction: { enabled: true, source: 'mixed', grounding: { enabled: true, use_vision: true, review: { enabled: true }, }, }, }, }); ``` Update project defaults: ```ts await client.projects.updateConfiguration(projectId, { intake: { enabled: true, generate_content_type: true, generate_properties: true, default_policy: { text_conversion: { enabled: true, method: 'auto' }, extraction: { enabled: true, source: 'auto' }, }, }, }); ``` When updating an existing policy, include the other values you want to preserve in the submitted object. ## Reprocessing and events Reprocessing a document through standard intake resolves the current project and content type policies again. Use this after changing a type, schema, conversion strategy, or IDP configuration. Event Bus subscriptions are optional. They are useful after intake, for example to start a process when IDP emits `document_processed` with `good_to_go`, or to send `needs_review` outcomes to a webhook or human task. See [Event Subscriptions](/events/subscriptions#idp-document-events). --- ## Markdown Export Guide Source: https://docs.vertesiahq.com/content/markdown-export Markdown: https://docs.vertesiahq.com/llms/content/markdown-export.md This guide explains how to export markdown to `PDF` or `DOCX` with Vertesia rendering jobs. Use this when you need: - report exports from stored markdown documents - workflow message exports from inline markdown - non-blocking UX (start + poll + download) TOC behavior: - The renderer adds a native Table of Contents automatically for both PDF and DOCX. - Do not add manual TOC tables in markdown (for example `TABLE DES MATIÈRES` with a `Page` column). ## Choose Input Mode ### Object mode (recommended for documents) Use `object_id` when the markdown already exists as a content object. Benefits: - reproducible exports - easier auditing and reruns - no large payloads in request body ### Inline mode (recommended for workflow messages) Use `content` when rendering ephemeral markdown generated during a run (for example message summaries or final answers). Benefits: - no need to create an object first - fast export path for UI message actions ## Rendering Flow Rendering is asynchronous: 1. `POST /api/v1/rendering/jobs` starts a job. 2. Poll `GET /api/v1/rendering/jobs/status` until terminal status. 3. On `COMPLETED`, download from `download_url` (or fallback to `file_uri`). Terminal statuses: - `COMPLETED` - `FAILED` - `CANCELED` - `TERMINATED` - `TIMED_OUT` ## SDK Usage (Recommended) The SDK already performs start + polling for you: ```typescript import { VertesiaClient, MarkdownRenditionFormat } from "@vertesia/client"; const client = new VertesiaClient({ site: "api.vertesia.io", apikey: "", }); // Object mode const pdf = await client.store.rendering.render({ object_id: "", format: MarkdownRenditionFormat.pdf, title: "Portfolio Report", }); // Inline mode (workflow message export) const docx = await client.store.rendering.render({ content: "# Message Export\n\nRendered from inline markdown.", format: MarkdownRenditionFormat.docx, title: "Message Export", }); ``` ## Manual Polling (Advanced) Use manual polling when you need custom behavior (cancel buttons, global queue, telemetry): ```typescript import { WorkflowExecutionStatus } from "@vertesia/common"; const started = await client.store.rendering.start({ object_id: "", format: "pdf", }); while (true) { const status = await client.store.rendering.getStatus( started.workflow_id!, started.workflow_run_id!, ); if (status.status === WorkflowExecutionStatus.COMPLETED) { console.log(status.download_url); break; } if ( status.status === WorkflowExecutionStatus.FAILED || status.status === WorkflowExecutionStatus.CANCELED || status.status === WorkflowExecutionStatus.TERMINATED || status.status === WorkflowExecutionStatus.TIMED_OUT ) { throw new Error(status.error || "Rendering failed"); } await new Promise((r) => setTimeout(r, 1500)); } ``` ## UX Recommendations - Disable export buttons while a render is running. - Show a compact “Exporting…” status. - Surface clear error messages from terminal failure statuses. - Prefer `object_id` for content views; keep inline only for workflow message export actions. ## Template and Options Both modes support: - `template_url` - `template_logo_url` (studio-hosted URL for PDF template logo override) - `use_default_template` - `pandoc_options` - `metadata` (PDF header/footer fields) Notes: - TOC is enabled by default for both PDF and DOCX. - If needed, disable TOC with `pandocOptions: ["--toc=false"]`. Example: ```json { "object_id": "", "format": "pdf", "metadata": { "document_id": "REP-2026-02", "agent_name": "Research Agent" } } ``` ## API Reference For complete request/response details, see: - [Rendering API](/api/openapi#tag/rendering) --- ## Content Indexing Overview Source: https://docs.vertesiahq.com/content/overview Markdown: https://docs.vertesiahq.com/llms/content/overview.md Vertesia provides powerful content indexing and search capabilities that enable AI agents and applications to efficiently find and retrieve relevant documents from your knowledge base. ## What is Content Indexing? Content indexing is the process of analyzing, embedding, and organizing your documents to make them searchable. Vertesia automatically indexes content when documents are created or updated, maintaining search indexes that support multiple search strategies. When you upload documents to Vertesia, the platform: 1. **Extracts content** from various file formats (PDF, Word, images, etc.) 2. **Generates embeddings** using AI models to capture semantic meaning 3. **Indexes metadata** including document properties, types, and relationships 4. **Maintains search indexes** for fast retrieval Each step of this pipeline can be tuned per content type — markdown conversion, IDP extraction and review, rendered text, and embeddings — using [Intake Configuration](/content/intake-policies). ## Search Infrastructure Vertesia search runs on Elasticsearch, which backs every search capability: - **Vector search** using document embeddings for semantic similarity - **Full-text search** with advanced text analysis, stemming, and fuzzy matching - **Hybrid search** combining full-text and vector search with configurable weights - **Complex aggregations** for analytics and faceted navigation - **DSL queries** for complete control over search behavior - **High-volume indexing** with zero-downtime reindexing Elasticsearch must be enabled for your project before documents can be indexed and searched. See [Search Configuration](/content/search) for prerequisites and setup. ## Search Types Vertesia supports multiple search strategies that can be used individually or combined: ### Semantic Search (Vector) Semantic search uses embeddings to find documents based on meaning rather than exact keywords. When you search for "quarterly financial report," semantic search can find documents about "Q3 earnings summary" even without matching words. **Best for:** - Finding conceptually similar documents - Natural language queries - Cross-language search (when embeddings support it) ### Full-Text Search Full-text search matches documents based on keywords with support for: - **Stemming**: Matching "running" with "run," "runs," "ran" - **Fuzzy matching**: Finding documents despite typos - **Phrase matching**: Exact phrase requirements - **Field-specific search**: Targeting specific metadata fields **Best for:** - Known keyword searches - Exact phrase matching - Technical terminology ### Hybrid Search Hybrid search combines semantic and full-text search for the best of both approaches. Vertesia supports multiple score aggregation methods: | Method | Description | |--------|-------------| | **RRF** (Reciprocal Rank Fusion) | Combines result rankings, good when relevance scores aren't comparable | | **RSF** (Relevance Score Fusion) | Combines normalized relevance scores for direct score comparison | | **Smart** | Automatically selects the best method based on available search types | You can also configure weights to prioritize one search type over another: ```json { "full_text": "quarterly report", "vector": { "text": "financial analysis" }, "weights": { "full_text": 2, "vector": 3 } } ``` ## Getting Started 1. **Enable Elasticsearch** for the project. See [Search Configuration](/content/search). 2. **Configure embeddings** to enable semantic search. See [Embeddings Configuration](/content/embeddings). 3. **Use the search_documents tool** to search from agents. See [Built-in Tools](/agent-runner/tools). ## Next Steps - [Embeddings Configuration](/content/embeddings) - Configure text, image, and properties embeddings - [Search Configuration](/content/search) - Enable and manage the Elasticsearch backend - [Built-in Tools](/agent-runner/tools) - Learn about the search_documents tool --- ## Search Configuration Source: https://docs.vertesiahq.com/content/search Markdown: https://docs.vertesiahq.com/llms/content/search.md Vertesia search runs on Elasticsearch. This guide covers how to configure and manage it for optimal search performance. ## Elasticsearch Elasticsearch backs every search capability: full-text search, vector search over your embeddings, hybrid search combining the two, and aggregations. ### What it provides - **Full-text search** with stemming, fuzzy matching, and phrase queries - **Vector search** using document embeddings for semantic similarity - **Hybrid search** combining vector and full-text with configurable weights - **Aggregations** for analytics, facets, and document statistics - **DSL queries** for complete control over search behavior - **Zero-downtime reindexing** for high-volume document sets ### Monitoring index status Check the search index status through the embeddings status endpoint: ```json { "vectorIndex": { "status": "READY", "name": "…", "type": "elasticsearch" } } ``` **Status values:** | Status | Description | |--------|-------------| | **READY** | The index is operational | | **PENDING** | The index is being built | | **DELETING** | The index is being removed | | **ABSENT** | No index — Elasticsearch is not enabled for this project, or indexing has not run | ### Prerequisites Elasticsearch requires: 1. Elasticsearch infrastructure enabled for your account 2. Embeddings configured (for vector search capabilities) 3. Project settings permission (`project:settings_write`) ### Enabling Elasticsearch #### Get Status Check the current Elasticsearch status for your project: ```bash {{title: 'cURL'}} curl --location --request GET \ 'https://api.vertesia.io/api/v1/indexing/status' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ serverUrl: 'https://api.vertesia.io', apikey: '', }); const status = await client.store.indexing.status(); console.log(status); ``` **Example Response:** ```json { "infrastructure_enabled": true, "indexing_enabled": true, "query_enabled": true, "index": { "exists": true, "alias_name": "content_abc123", "index_name": "content_abc123_v1", "version": 1, "created_at": "2024-01-15T10:00:00Z", "document_count": 15234, "size_bytes": 52428800 }, "mongo_document_count": 15234, "reindex_in_progress": false, "reindex_progress": null } ``` #### Enable Indexing Enable Elasticsearch indexing for your project. This creates the index and starts syncing documents: ```bash {{title: 'cURL'}} curl --location --request POST \ 'https://api.vertesia.io/api/v1/indexing/enable-indexing' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ serverUrl: 'https://api.vertesia.io', apikey: '', }); const response = await client.store.indexing.enableIndexing(); console.log(response); ``` #### Enable Queries Queries are now automatically enabled when indexing is enabled. This endpoint is kept for backward compatibility. ```bash {{title: 'cURL'}} curl --location --request POST \ 'https://api.vertesia.io/api/v1/indexing/enable-queries' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ serverUrl: 'https://api.vertesia.io', apikey: '', }); const response = await client.store.indexing.enableQueries(); console.log(response); ``` #### Disable Queries Queries are now automatically enabled when indexing is enabled. To disable queries, disable indexing instead. ```bash {{title: 'cURL'}} curl --location --request POST \ 'https://api.vertesia.io/api/v1/indexing/disable-queries' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ serverUrl: 'https://api.vertesia.io', apikey: '', }); const response = await client.store.indexing.disableQueries(); console.log(response); ``` #### Disable Indexing Disable Elasticsearch indexing entirely: ```bash {{title: 'cURL'}} curl --location --request POST \ 'https://api.vertesia.io/api/v1/indexing/disable-indexing' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ serverUrl: 'https://api.vertesia.io', apikey: '', }); const response = await client.store.indexing.disableIndexing(); console.log(response); ``` ### Reindexing Reindexing rebuilds the Elasticsearch index from MongoDB. This may be needed when: - Enabling Elasticsearch for an existing project with documents - Changing embedding dimensions - Index corruption or sync issues - Recovering from failures #### Trigger Reindex ```bash {{title: 'cURL'}} curl --location --request POST \ 'https://api.vertesia.io/api/v1/indexing/reindex' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ "recreate_index": false }' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ serverUrl: 'https://api.vertesia.io', apikey: '', }); const response = await client.store.indexing.reindex(false); console.log(response); ``` **Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `recreate_index` | boolean | If `true`, drops and recreates the index. Use when changing dimensions or mappings. | #### Zero-Downtime Reindexing Vertesia uses alias-based reindexing for zero downtime: 1. A new index is created with updated mappings 2. Documents are batch-indexed to the new index 3. The alias is atomically swapped from old to new 4. The old index is deleted During reindexing, queries continue to work against the existing index. #### Monitoring Progress Check reindex progress through the status endpoint: ```json { "reindex_progress": { "status": "running", "processed": 5000, "total": 15234, "current_batch": 10, "total_batches": 31, "percent_complete": 33 } } ``` ### Drift Analysis Use drift analysis to measure how Elasticsearch has diverged from MongoDB without rebuilding the index. The analyzer compares documents by `_id` and `updated_at`. #### Start Drift Analysis ```bash {{title: 'cURL'}} curl --location --request POST \ 'https://api.vertesia.io/api/v1/indexing/analyze-drift' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ serverUrl: 'https://api.vertesia.io', apikey: '', }); const response = await client.store.indexing.analyzeDrift(); console.log(response); ``` #### Check Drift Analysis Status ```bash {{title: 'cURL'}} curl --location --request GET \ 'https://api.vertesia.io/api/v1/indexing/drift-analysis' \ --header 'Authorization: Bearer ' ``` ```typescript {{title: 'Vertesia SDK'}} import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ serverUrl: 'https://api.vertesia.io', apikey: '', }); const status = await client.store.indexing.getDriftAnalysis(); console.log(status); ``` **Example Completed Response:** ```json { "workflow_id": "analyzeElasticsearchDriftWorkflow:project-abc123", "workflow_run_id": "019d1edd-3ac9-7dbf-b78d-470c49771180", "status": "COMPLETED", "result": { "total": 15234, "processed": 15234, "missing": 8, "stale": 3, "sample_missing_ids": [ "65f0c4d7a8f9e7a6e7d3b101" ], "sample_stale_ids": [ "65f0c4d7a8f9e7a6e7d3b202" ], "completed_at": "2026-03-24T08:22:11.000Z" } } ``` ## Hybrid Search Hybrid search combines full-text and vector search for optimal relevance. When both search types return results, scores are aggregated using configurable methods. ### Score Aggregation Methods | Method | Algorithm | Best For | |--------|-----------|----------| | **RRF** | Reciprocal Rank Fusion | When relevance scores from different sources aren't directly comparable | | **RSF** | Relevance Score Fusion | When you want to combine normalized scores directly | | **Smart** | Automatic selection | General use, automatically picks the best method | ### Weight Configuration Control the relative importance of each search type: ```json { "query": { "full_text": "quarterly report", "vector": { "text": "financial analysis" }, "weights": { "full_text": 2, "vector": 3 } } } ``` Higher weights give more influence to that search type. With the above configuration, vector search results are weighted 1.5x more than full-text results. ### Dynamic Scaling When enabled, dynamic scaling adjusts weights automatically if one search type is unavailable: ```json { "query": { "full_text": "quarterly report", "vector": { "text": "financial analysis" }, "dynamic_scaling": "on" } } ``` ## Index Configuration Tools Agents can query and update index configuration using built-in tools: ### get_index_configuration Retrieves the current index status and configuration. **Returns:** - Index status (exists, healthy) - Document count and size - Embedding dimensions for each type - Field mappings ### update_index_configuration Updates index configuration with options to change embedding dimensions or trigger reindexing. **Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `embedding_dimensions` | object | New dimensions for `text`, `image`, or `properties` | | `force_reindex` | boolean | Trigger a full reindex | | `user_confirmed` | boolean | Required confirmation (must use `ask_user` first) | ## Troubleshooting ### Documents Not Appearing in Search 1. Check that indexing is enabled (`indexing_enabled: true`) 2. Verify embeddings are configured and generating 3. Allow time for async indexing to complete 4. Check for sync issues in status endpoint ### Dimension Mismatch Errors If you changed embedding dimensions: 1. Recalculate embeddings with new dimensions 2. Trigger reindex with `recreate_index: true` ### Search Returns No Results 1. Verify documents exist in MongoDB (`mongo_document_count`) 2. Check Elasticsearch document count matches 3. Test with broader queries or `match_all` 4. Verify query syntax is correct ### Reindex Stuck or Failed 1. Check workflow status in the Vertesia UI 2. Look for errors in workflow history 3. Ensure sufficient permissions 4. Try triggering a new reindex (will cancel stuck one) ## Best Practices ### Index Management - Enable queries only after initial indexing completes - Monitor document counts between MongoDB and Elasticsearch - Schedule reindexing during low-traffic periods ### Search Configuration - Start with `smart` score aggregation - Tune weights based on search quality feedback - Use facets for navigation and filtering - Enable `analyze` for complex queries that benefit from LLM summarization ### Performance - Use appropriate `limit` values (avoid fetching more than needed) - Use `count_only` for pagination totals - Stream large results to artifacts with `output_artifact` - Consider DSL mode for complex aggregations ## Next Steps - [Content Overview](/content/overview) - Understanding the full search architecture - [Embeddings Configuration](/content/embeddings) - Configure embeddings for vector search - [Built-in Tools](/agent-runner/tools) - Learn about search_documents and index tools - [Commands API](/api/openapi#tag/commands) - Full API reference for indexing endpoints --- ## View Experiences Source: https://docs.vertesiahq.com/content/view-experiences Markdown: https://docs.vertesiahq.com/llms/content/view-experiences.md A **View Experience** is a named, reusable configuration for finding content in a Vertesia project. Instead of building search UI and writing Elasticsearch queries by hand, you describe *what* users should be able to browse and search — the content scope, the facets, the search behavior, and the result layouts — and Vertesia renders and executes it for you. Persist a View once, then open it through the generic `/view/` route or embed it in an application with the `ViewExperience` React component. View execution uses Elasticsearch. Enable it for the project before opening or embedding a View. See [Search Configuration](/content/search). ## Anatomy of a View Every View Experience is four parts. Most of the documentation is organized around them: ```text View Experience ├── scope which content the View covers — content types, locations, collections, a fixed filter ├── navigation how users narrow down — property facets, hierarchies, ranges, collections, location ├── search how users search — deterministic (keywords + key terms) or agentic (natural language) └── results how matches are shown — list, table, cards, gallery, or board displays, plus sorting ``` A View author configures these four sections; the runtime turns them into a secure Elasticsearch query and a normalized result set that the reusable UI renders. ## How a View runs, end to end ```text AUTHOR PERSIST EXECUTE RENDER ┌──────────┐ client.views ┌───────────────┐ resolve ┌──────────┐ execute() ┌──────────┐ │ Studio │ ───────────────▶ │ studio-server │ ────────▶ │ Zeno │ ────────────▶ │ your │ │ editor │ create/update │ (MongoDB) │ by id │ (ES) │ /view/:id or │ app │ │ + Agent │ or an app's │ versioned │ │ secure │ ViewExperience│ UI │ └──────────┘ in-code View └───────────────┘ └──────────┘ └──────────┘ ``` 1. **Authoring** happens in the Studio editor (with help from Studio Assistant), or an application ships a View **in code**. See [Configuration](/content/view-experiences/configuration). 2. **Persistence** is owned by **studio-server**: project-scoped, separately revisioned View resources under `/api/v1/views`. The platform includes read-only `sys:*` Views, and apps can contribute in-code definitions that resolve as `app::`. 3. **Execution** is owned by **Zeno** at `POST /api/v1/view-executions/:id/execute`. It loads the authoritative configuration, composes the query, and returns normalized results. 4. **Rendering** uses the generic `/view/` route or the `ViewExperience` component. See [Embedding in applications](/content/view-experiences/embedding). The client only ever sends **user-controlled state** — the text query, selected facets, chosen display, sort, and page. It never sends a raw Elasticsearch request. ## Security model View execution is safe to expose to end users because the server, not the client or the model, owns the dangerous parts of the query: - The View's **scope** (content types, locations, collections, fixed filter) and the caller's **content-security** and project filters are always composed on the server, *outside* anything a user or an agent supplied. - Agentic search asks a model to produce a query, but the result is checked against a **validated allowlist** of query types and fields before the immutable scope and security filters are added around it. - Only **head revisions** are searched by default, and callers can only select **authored** sort options — never arbitrary Elasticsearch sort clauses. - Generated Elasticsearch DSL and security filters are **never** placed in the URL. Execution diagnostics may expose the validated model-authored query subtree, but never the server-owned scope or content-security filters. ## When to use a View Use a View Experience when you want a reusable, shareable, permission-aware way to browse and search project content — an internal document library, an operational worklist, a customer-facing search page, or an agent-authored browsing surface. Prefer a direct [content search](/content/search) call when you need a one-off query with no reusable UI, and a [collection](/content/overview) when you only need to group a fixed set of objects without search or navigation. ## In this section - **[Quickstart](/content/view-experiences/quickstart)** — create your first View in three steps. - **[Configuration](/content/view-experiences/configuration)** — scope, navigation, search, and result displays in depth, with a complete example. - **[Search & agentic planning](/content/view-experiences/search)** — deterministic versus natural-language search, and the typed-indexing prerequisite for numeric and date facets. - **[Embedding in applications](/content/view-experiences/embedding)** — the route, the `ViewExperience` component, deep links, custom renderers, and in-code (app-contributed) Views. - **[API & validation reference](/content/view-experiences/reference)** — REST endpoints, draft preview, permissions, and configuration validation. - **[Troubleshooting](/content/view-experiences/troubleshooting)** — common problems and how to fix them. --- ## Configuration Source: https://docs.vertesiahq.com/content/view-experiences/configuration Markdown: https://docs.vertesiahq.com/llms/content/view-experiences/configuration.md A View Experience configuration has four sections — `scope`, `navigation`, `search`, and `results` — plus an optional `layout`. This page covers scope, navigation, and results in depth; search behavior has its own page, [Search & agentic planning](/content/view-experiences/search). ## A complete example The following configuration creates a Sales Orders browser. It scopes to the Sales Order content type, exposes the `sold_to_state`, `customer`, and `status` properties as count-bearing facets, offers deterministic search with structured key terms, and provides table and card displays with sorting. ```typescript {{ title: 'sales-orders-view.ts' }} import type { ViewExperienceConfiguration } from '@vertesia/common'; export const salesOrdersView = { name: 'Sales Orders', description: 'Browse and search sales orders by customer, destination state, and status.', enabled: true, layout: { mode: 'worklist', navigation_position: 'sidebar', }, scope: { type_ids: [''], head_only: true, }, navigation: [ { id: 'sold_to_state', label: 'Sold-to state', source: 'terms', field: 'properties.sold_to_state', presentation: 'chips', multi_select: true, size: 50, sort: 'label', }, { id: 'customer', label: 'Customer', source: 'terms', field: 'properties.customer', presentation: 'list', multi_select: true, size: 50, sort: 'count', }, { id: 'status', label: 'Status', source: 'terms', field: 'properties.status', presentation: 'chips', multi_select: true, size: 20, sort: 'count', }, ], search: { mode: 'deterministic', placeholder: 'Search sales orders', fields: [ { field: 'name', description: 'Sales order or source file name.', type: 'text', mode: 'full_text', boost: 2 }, { field: 'text', description: 'Full ingested and OCR sales-order text.', type: 'text', mode: 'full_text' }, ], key_terms: [ { id: 'customer', label: 'Customer', field: 'properties.customer', type: 'keyword', multiple: true, operator: 'term' }, { id: 'status', label: 'Status', field: 'properties.status', type: 'keyword', multiple: true, operator: 'term' }, ], }, results: { default_display: 'table', allow_display_switch: true, selection: { mode: 'multiple', select_all: 'page', }, actions: { include_defaults: true, }, drop: { handler: 'upload', params: { type_id: '', location: '/Sales Orders/Inbox', properties: { source: 'sales-order-view' }, allow_folders: true, }, }, default_sort: 'recent', sort_options: [ { id: 'recent', label: 'Recently updated', sort: [{ field: 'updated_at', order: 'desc' }] }, { id: 'customer_asc', label: 'Customer', sort: [{ field: 'properties.customer', order: 'asc' }] }, ], displays: [ { id: 'table', label: 'Table', type: 'table', page_size: 25, columns: [ { field: 'name', label: 'Sales order' }, { field: 'properties.customer', label: 'Customer' }, { field: 'properties.sold_to_state', label: 'State' }, { field: 'properties.status', label: 'Status', format: 'badge' }, { field: 'updated_at', label: 'Updated', format: 'date' }, ], }, { id: 'cards', label: 'Cards', type: 'cards', page_size: 24, columns: 3, title: { field: 'name' }, description: { field: 'description' }, fields: [ { field: 'properties.customer', label: 'Customer' }, { field: 'properties.sold_to_state', label: 'State' }, ], badges: [{ field: 'properties.status', format: 'badge' }], }, ], }, } satisfies ViewExperienceConfiguration; ``` Replace `` with the ID of the Sales Order content type. Confirm indexed field names and values before exposing a property as a facet or sort field. On current indexes, property strings are keyword fields, so use a path such as `properties.customer` directly. ## Scope `scope` fixes the content a View covers. It is applied on every execution and cannot be overridden by the caller. | Field | Meaning | | --- | --- | | `type_ids` | Restrict to specific content types. | | `locations` | Restrict to absolute content location subtrees. | | `collection_ids` | Restrict to members of specific collections (a hard membership boundary). | | `include_collection_descendants` | Extend `collection_ids` to descendant collections. | | `fixed_filter` | An author-provided Elasticsearch query subtree, validated against the supported allowlist. | | `head_only` | Search only head revisions (the default); set `false` to include historical revisions. | Use `scope.collection_ids` or `scope.locations` when membership or location is a hard boundary for the whole View, and a *navigation* item (below) when the user should choose a collection or subtree interactively. A `fixed_filter` that uses an unsupported query type is rejected by create, update, preview, and execution validation — see [Troubleshooting](/content/view-experiences/troubleshooting). ## Layout `layout` controls the frame. `navigation_position` is `sidebar` (default) or `top`; `mode` is `browse` (default) or `worklist`, which reorders the results-first layout for operational queues. ## Navigation Navigation items share one result contract: each item returns the selected values and count-bearing nodes. The `source` determines how those nodes are produced. All active navigation items intersect with each other and with search input. | Source | Use it for | Key configuration | | --- | --- | --- | | `terms` | Bounded property values such as customer, state, or status | `field`, `size`, `sort` | | `hierarchy` | Ordered property drill-down such as State → City | two or more `levels`, each with a `field` | | `range` | Authored numeric or date buckets | `field` and labeled `ranges` | | `collection` | Curated or dynamic collection membership | `roots`, `include_descendants` | | `location` | Hierarchical content paths | absolute `roots`, `depth` | Navigation can be rendered as `tree`, `list`, `select`, or `chips`. Facets and collection/location navigation can be single- or multi-select; a property hierarchy always represents one path. ### Property facets A `terms` navigation item runs an aggregation on its configured field and returns values with result counts. Use facets for normalized, bounded values such as `properties.customer`, `properties.status`, or `properties.sold_to_state`. Avoid placing identifiers, free text, or mostly unique values in a visible terms facet. Range facets must target a genuinely numeric or date-mapped field. Because JSON has no date scalar, date-looking strings remain keyword fields unless the project explicitly maps the path as `date`, and any explicit mapping change requires a full reindex first. See [the typed-indexing prerequisite](/content/view-experiences/search#typed-property-indexing). ### Property hierarchies A property hierarchy turns any ordered set of mapped fields into drill-down navigation. It does not require the properties themselves to be nested: ```json { "id": "geography", "label": "Geography", "source": "hierarchy", "presentation": "tree", "levels": [ { "id": "state", "label": "State", "field": "properties.sold_to_state", "sort": "label" }, { "id": "city", "label": "City", "field": "properties.sold_to_city", "sort": "label" } ] } ``` The first execution returns State buckets. Selecting Florida applies its exact typed value and returns only City buckets beneath Florida. The response includes `breadcrumbs` for the selected path; **treat returned node IDs as opaque** and send them back unchanged in `navigation.geography`. A property hierarchy always represents one path and cannot be multi-select. Add a separate `terms` facet when users also need to combine several values at one level. Order levels from broadest to most specific, and choose bounded, aggregatable fields. The same mechanism works for Brand → Product line → Model, Department → Team, or any meaningful sequence. ### Collection browsing Collection navigation follows collection identity rather than content location: ```json { "id": "sales_order_collections", "label": "Collections", "source": "collection", "presentation": "tree", "roots": [""], "include_descendants": true } ``` When no `roots` are supplied, the browser starts from visible top-level collections. Static collections resolve to their member IDs; dynamic collections resolve through their stored query. With `include_descendants`, selecting a collection also includes members of descendant collections. Resolution happens on the server, so collection visibility and content security remain enforced. One selected collection returns a `breadcrumbs` path. If a level has more than 50 collections, the response is marked `truncated`; use `navigation_queries.` to filter collection names server-side and reach nodes outside the first page. ### Location hierarchy Location navigation browses slash-delimited content paths: ```json { "id": "location", "label": "Location", "source": "location", "presentation": "tree", "roots": ["/Sales Orders"], "depth": 6 } ``` Selecting `/Sales Orders/West` includes that path and its descendants. Roots must be absolute. Location browsing returns immediate children in the current context and may report a truncation warning for a very high-cardinality hierarchy. When exactly one location is selected, the response includes clickable `breadcrumbs` from the configured root to the selected path. ## Result displays A View can offer multiple declarative displays. Set `allow_display_switch` to let the user move between them without losing the current query or navigation state. | Display | Best for | | --- | --- | | `list` | Compact universal results with title, subtitle, description, media, and badges | | `table` | Operational inventories with typed columns and configured sorting | | `cards` | Rich summaries with media, fields, and badges | | `gallery` | Media-first content | | `board` | Cards grouped by a property such as `properties.status` | Result fields use dot-separated paths such as `properties.customer`, `type.name`, or `updated_at`. Available formats are `text`, `date`, `number`, `badge`, `user`, `content_type`, and `location`. Media can come from a content thumbnail, a property, or the content type icon. Only **authored** sort options can be selected by callers. A table column can reference one of those options through `sort_option`; callers do not submit arbitrary Elasticsearch sort clauses. ## Selection and actions Selection is part of the result experience, not the search request. It stays client-side and is cleared when the query or navigation changes. Multiple selection can expose a page-level checkbox: ```json { "selection": { "mode": "multiple", "select_all": "page" }, "actions": { "include_defaults": true, "exclude_defaults": ["delete"], "items": [ { "id": "approve", "label": "Approve", "handler": "approve-orders", "placement": "selection", "requires_selection": "any", "confirm": true, "params": { "status": "approved" } } ] } } ``` When selection is enabled, `include_defaults` defaults to `true`. The built-in actions are export and, for users with delete permission, delete. Use `exclude_defaults` to hide either action. An item in `actions.items` only declares its label, placement, selection requirement, confirmation, and parameters; the embedding application must register code for its `handler`. If the app does not provide that handler, the action is hidden. `export` and `delete` are reserved handler and action IDs. ## Drop to upload Persisted JSON can turn the result area into the standard content uploader: ```json { "drop": { "handler": "upload", "accept": ["files"], "params": { "type_id": "", "collection_id": "", "location": "/Incoming/Sales Orders", "properties": { "source": "sales-order-view" }, "allow_folders": true } } } ``` All parameters are optional. They preselect the content type, collection, and location, apply default properties to new objects, and control folder upload while retaining the normal upload review flow. The runtime checks content-write permission before enabling this target. Custom drop behavior is deliberately **code-only**. It cannot be named or selected in persisted JSON. Register it on the embedded `ViewExperience`; the application then owns validation, authorization, side effects, and error handling. See [Embedding in applications](/content/view-experiences/embedding#actions-and-drop-contributions). ## Configuration checklist Before publishing a View: 1. Verify that Elasticsearch is enabled and the scoped content is indexed. 2. Confirm content type, collection, location, and property field IDs against the project. 3. Inspect representative property values before choosing visible facets. 4. Validate the configuration and preview deterministic searches — see [API & validation reference](/content/view-experiences/reference). 5. If agentic search is enabled, test successful planning, deterministic fallback, low-confidence behavior, and — when configured — reranking order, why-match annotations, and Elasticsearch-order fallback. 6. Open a copied deep link and verify repeated selections, display, and sort. 7. Test every display and any custom renderer with missing fields and empty results. 8. Test selection, each enabled action, and upload defaults with the permissions of the intended audience. --- ## Embedding in applications Source: https://docs.vertesiahq.com/content/view-experiences/embedding Markdown: https://docs.vertesiahq.com/llms/content/view-experiences/embedding.md A persisted View can be opened with no application code, embedded as a React component, or executed directly through the client. Applications can also **contribute Views in code** so they install alongside the app. ## The generic route The generic Studio route renders a persisted View without application-specific page code: ```text /view/sales-orders ``` Use it for internal tools, quick links, and anywhere you do not need custom page chrome. ## The `ViewExperience` component Most applications embed `ViewExperience`, which loads the authoritative configuration, executes the View, renders navigation and results, manages pagination, and synchronizes state with the URL. Inside a Vertesia session it self-fetches — pass only a `viewId`: ```tsx {{ title: 'SalesOrdersPage.tsx' }} import type { ViewHit } from '@vertesia/common'; import { ViewExperience } from '@vertesia/ui/features'; import { useNavigate } from '@vertesia/ui/router'; export function SalesOrdersPage() { const navigate = useNavigate(); const openSalesOrder = (hit: ViewHit) => { navigate(`/store/objects/${encodeURIComponent(hit.id)}`, { isBasePathNested: false }); }; return ; } ``` `ViewExperience` uses `client.views.execute` from the active Vertesia session. Outside a session provider, supply an `execute` function: ```tsx {{ title: 'EmbeddedSalesOrders.tsx' }} client.views.execute('sales-orders', request)} showHeader={false} /> ``` ## Built-in system Views Two built-in Views are available without creating a project resource: | ID | Purpose | | --- | --- | | `sys:ContentLibrary` | Deterministic content browsing with folder, collection, type, and status navigation; list, table, and card displays; selection, export/delete, and upload-on-drop. | | `sys:AgenticDocumentExplorer` | Demonstrates natural-language Elasticsearch query, adaptive presentation planning, bounded result reranking, and why-match guidance over the always-available `sys:GenericDocument` type, while retaining folder and collection navigation. | Open them through the generic route (the colon may be URL-encoded) or embed them exactly like a persisted View: ```tsx ``` The agentic system View inherits the project model configured for the Content Search system interaction and reuses it for reranking. It does not pin a model or environment. It scopes results to `sys:GenericDocument`; try queries such as `compare contracts about termination clauses`, `show documents by Alice as cards`, or `group presentations updated last month`. System Views are read-only in-code definitions; create a project View when you need to customize their configuration. ## Execute directly The underlying content API is `POST /api/v1/view-executions/:id/execute`. Clients send only user-controlled state, never a complete Elasticsearch request: ```typescript {{ title: 'Execute directly' }} const result = await client.views.execute('sales-orders', { query: 'orders delayed by weather', key_terms: { customer: ['Acme'] }, navigation: { sold_to_state: ['CA', 'NV'], status: ['open'] }, navigation_queries: { collections: 'renewals' }, display: 'table', sort: 'recent', offset: 0, limit: 25, }); ``` The response contains the authoritative View definition, normalized content hits, total count, navigation nodes and counts, the applied display and sort, search mode and warnings, and execution time. ## Shareable deep links With URL synchronization enabled, `ViewExperience` serializes the user-visible state: | Parameter | Meaning | | --- | --- | | `q` | Natural-language or deterministic text query | | `t.` | Structured key-term value | | `n.` | Navigation selection | | `nq.` | Server-side navigation-node filter, currently supported for collections | | `display` | Configured display ID | | `sort` | Configured sort ID | | `offset` | Result offset | Multiple values use repeated parameters. For example: ```text /view/sales-orders?q=orders+delayed+by+weather&t.customer=Acme&n.sold_to_state=CA&nq.collections=renewals&display=table&sort=recent ``` The component preserves unrelated host query parameters and responds to browser back/forward navigation. After execution it removes stale key-term or navigation IDs and uses the display and sort accepted by the authoritative runtime. Generated Elasticsearch DSL and security filters are never placed in the URL. Set `syncUrl={false}` for a preview, modal, or embedded surface that should not modify the host route. ## Custom renderers The built-in renderers cover common search, navigation, and result layouts. An embedded application can replace any of them with a typed React component: 1. Set a stable `renderer` name on `search`, a navigation item, or a result display. 2. Pass components under matching names in the `renderers` registry. 3. Keep data access in the View runtime; custom renderers receive normalized state and callbacks. First, declare the renderer names in the View configuration: ```typescript {{ title: 'sales-order-view.ts' }} import type { ViewExperienceConfiguration } from '@vertesia/common'; export const salesOrderView = { name: 'Sales orders', description: 'Find and manage sales orders.', search: { mode: 'deterministic', renderer: 'sales-order-search', }, navigation: [ { id: 'status', label: 'Status', source: 'terms', field: 'properties.status', renderer: 'status-pipeline', }, ], results: { default_display: 'cards', displays: [ { id: 'cards', label: 'Cards', type: 'cards', renderer: 'sales-order-cards', title: { field: 'name' }, }, ], }, } satisfies ViewExperienceConfiguration; ``` Then register components under those exact names in the application: ```tsx {{ title: 'Renderer registry' }} import type { ViewNavigationRendererProps, ViewResultsRendererProps, ViewSearchRendererProps, } from '@vertesia/ui/features'; import { ViewExperience } from '@vertesia/ui/features'; function SalesOrderSearch(props: ViewSearchRendererProps) { // Render inputs, then call props.onQueryChange and props.onSubmit. return ; } function StatusPipeline(props: ViewNavigationRendererProps) { // Render props.result.nodes, then call props.onChange with selected node IDs. return ; } function SalesOrderCards(props: ViewResultsRendererProps) { // Render props.result.hits and use props.onOpenHit for host navigation. return ; } const renderers = { search: { 'sales-order-search': SalesOrderSearch }, navigation: { 'status-pipeline': StatusPipeline }, results: { 'sales-order-cards': SalesOrderCards }, }; export function SalesOrders() { return ; } ``` Persisted configuration stores renderer **names**, not executable code. When a renderer name is missing from the application registry — including on the generic `/view/` route — the component safely falls back to the built-in renderer for that section. Treat renderer names as an application compatibility contract: deploy the new renderer before updating a persisted View to reference it, and keep the old name registered while older View revisions or deep links may still use it. ## Actions and drop contributions The built-in result renderer supports single or multiple selection. Persisted View JSON may enable the standard export and delete actions, and may declare app action metadata by a stable `handler` name. The application supplies the executable handler through `contributions.actions`: ```tsx {{ title: 'Contributed View actions' }} import { ViewExperience } from '@vertesia/ui/features'; export function SalesOrders() { return ( result.total > 0, run: async ({ action, hits, refresh, clearSelection }) => { await approveOrders({ ids: hits.map((hit) => hit.id), status: action.params?.status, }); clearSelection(); await refresh(); }, }, }, }} /> ); } ``` The contribution receives normalized hits, the authoritative View definition and result, the current request, and `refresh`/`clearSelection` callbacks. No executable code is stored in the View. Custom result renderers receive the selection controller in `props.selection` and can use the exported `useViewActions` hook for app-specific action UI. Persisted JSON supports one drop handler: the standard `upload` flow. For any other behavior, contribute drop code directly from the application: ```tsx {{ title: 'Code-only custom drop' }} { await importCaseBundle({ files, view: definition.name, query: request.query }); await refresh(); }, }, }} /> ``` A code contribution works even when the View has no `results.drop` entry and overrides the built-in upload target when both are present. Because it is application code, the app is responsible for checking any domain-specific permissions. The generic `/view/` route has no app contribution registry, so it only runs the persisted built-in upload behavior. ## In-code (app-contributed) Views An application or plugin can ship a View in its own code so it installs with the app, instead of being persisted through the Studio API. Declare `InCodeViewDefinition` entries in the tool server's `views` configuration: ```typescript {{ title: 'plugin: an in-code View' }} import type { InCodeViewDefinition } from '@vertesia/common'; export const documentLibraryView = { id: 'document-library', name: 'document_library', title: 'Document Library', description: 'Browse and search indexed documents by type with full-text search.', definition: { name: 'Document Library', navigation: [{ id: 'type', label: 'Type', source: 'terms', field: 'type.name' }], search: { mode: 'deterministic' }, results: { default_display: 'list', displays: [{ id: 'list', label: 'List', type: 'list', title: { field: 'name' } }], }, }, } satisfies InCodeViewDefinition; ``` When the app is installed, Studio resolves the View as `app::` — for example `app:my-plugin:document-library`. It renders through the same `/view/` route and `ViewExperience` component as a persisted View; the app-scoped ID is the only difference: ```tsx ``` App-contributed Views are read-only at runtime: edit them in the app's source, not through the Studio CRUD API. --- ## Quickstart Source: https://docs.vertesiahq.com/content/view-experiences/quickstart Markdown: https://docs.vertesiahq.com/llms/content/view-experiences/quickstart.md This guide creates the smallest useful View — a searchable list of one content type with a single facet — and opens it. It takes a few minutes. For the full configuration surface, see [Configuration](/content/view-experiences/configuration). You need a project with Elasticsearch enabled and at least one content type that already has indexed objects. See [Search Configuration](/content/search). ## 1. Create a View in Studio Open **Studio > View Experiences**, select **New View**, and enter a stable ID such as `documents`. The ID is an immutable, project-unique, lowercase URL slug; the display name and everything else can change later. The Studio editor opens with the canonical View JSON Schema loaded into Monaco, so you get structural diagnostics as you type. Paste the minimal configuration below and adjust the content type ID and facet field to match your project. ```typescript {{ title: 'minimal-view.ts' }} import type { ViewExperienceConfiguration } from '@vertesia/common'; export const minimalView = { name: 'Documents', description: 'Browse and search project documents by status.', enabled: true, scope: { type_ids: [''], }, navigation: [ { id: 'status', label: 'Status', source: 'terms', field: 'properties.status', presentation: 'list', multi_select: true, size: 20, sort: 'count', }, ], search: { mode: 'deterministic', placeholder: 'Search documents', }, results: { default_display: 'list', displays: [ { id: 'list', label: 'List', type: 'list', title: { field: 'name' }, description: { field: 'description' }, }, ], }, } satisfies ViewExperienceConfiguration; ``` Every persisted View requires a `description`. Studio shows it to users and gives it to Studio Assistant when the assistant discovers or explains project Views, so make it a real sentence about the View's purpose and audience — not just the name repeated. ## 2. Validate and preview The editor applies structural diagnostics while you type and full semantic validation before saving. Once the View is created you can **preview** it in place: the editor runs the current configuration against real content and shows results without persisting your latest edits, so you can iterate before committing. Select **Explain** at any point to open Studio Assistant on the current configuration, including unsaved changes — see [Search & agentic planning](/content/view-experiences/search) and [the Studio Assistant guide](/studio/assistant). Save the View when the results look right. ## 3. Open it The generic route renders a persisted View with no application code: ```text /view/documents ``` That is the whole loop: define scope, one facet, search, and a display; save; open. From here: - Add more facets, hierarchies, ranges, or collection and location navigation — [Configuration](/content/view-experiences/configuration). - Turn on natural-language search — [Search & agentic planning](/content/view-experiences/search). - Embed the View in your own application instead of the generic route — [Embedding in applications](/content/view-experiences/embedding). If a facet comes back empty or a numeric range does nothing, check [Troubleshooting](/content/view-experiences/troubleshooting) — it is almost always a field-name or property-mapping issue. --- ## API & validation reference Source: https://docs.vertesiahq.com/content/view-experiences/reference Markdown: https://docs.vertesiahq.com/llms/content/view-experiences/reference.md Studio owns project-scoped View persistence under `/api/v1/views`; Zeno owns execution and draft preview. The Vertesia client exposes both through `client.views`, delegating execution to Zeno. The lower-level `client.store.views` API remains available when a content-service client is used directly. ## Persistence endpoints | Method | Endpoint | Purpose | | --- | --- | --- | | `GET` | `/api/v1/views` | List Views in the current project | | `POST` | `/api/v1/views` | Create a View with an immutable ID | | `GET` | `/api/v1/views/:id` | Retrieve a View and its current revision | | `PUT` | `/api/v1/views/:id` | Replace the complete mutable configuration | | `DELETE` | `/api/v1/views/:id` | Permanently delete a View | ```typescript {{ title: 'Create a View' }} import { VertesiaClient } from '@vertesia/client'; import { VIEW_EXPERIENCE_SCHEMA_VERSION } from '@vertesia/common'; import { salesOrdersView } from './sales-orders-view'; const client = new VertesiaClient({ site: 'api.vertesia.io', apikey: process.env.VERTESIA_API_KEY }); const view = await client.views.create({ id: 'sales-orders', version: VIEW_EXPERIENCE_SCHEMA_VERSION, ...salesOrdersView, }); ``` Updates are full replacements and use optimistic concurrency. Retrieve the current View, include its `version` and `revision`, and handle HTTP `409` by reloading instead of overwriting another author's changes: ```typescript {{ title: 'Update a View' }} import { VIEW_EXPERIENCE_SCHEMA_VERSION, getViewExperienceConfiguration } from '@vertesia/common'; const current = await client.views.retrieve('sales-orders'); const updated = await client.views.update(current.id, { version: VIEW_EXPERIENCE_SCHEMA_VERSION, revision: current.revision, ...getViewExperienceConfiguration(current), description: 'Sales orders for the fulfillment and customer success teams.', }); ``` Persisted Views require a non-empty `description`. The Studio migration backfills older Views from their names so they remain editable. Replace that fallback with a useful purpose-and-audience description the next time you edit the View. ## Execution and draft preview | Method | Endpoint | Purpose | | --- | --- | --- | | `POST` | `/api/v1/view-executions/:id/execute` | Execute a persisted (or app-contributed) View | | `POST` | `/api/v1/view-executions/preview` | Validate and execute an **unsaved** configuration without persisting it | Preview is the fast authoring loop: send a draft configuration plus the same execution inputs as `execute`, and get real results back without saving. Studio Assistant and the Studio editor use it to iterate before `create`/`update`. ```typescript {{ title: 'Preview a draft' }} const preview = await client.views.preview({ configuration: draftView, query: 'orders delayed by weather', navigation: { status: ['open'] }, navigation_queries: { collections: 'renewals' }, limit: 25, }); ``` Preview validates the configuration and returns `400` with the specific issues if it is invalid, so it doubles as a validate-and-run check. Agentic preview may invoke the configured model, so it requires interaction execute access (below). ## Permissions | Operation | Required permission | | --- | --- | | List / retrieve Views | interaction **read** | | Create / update / delete Views | interaction **write** | | Execute or preview a View | content **read** | | Execute or preview an **agentic** View with a query | additionally interaction **execute** | Normal project and content-security filters are always applied by the server on execution and preview, regardless of these permissions. ## Configuration validation `@vertesia/common` exports the JSON Schemas and semantic validators. Use the **persisted** variants for a custom authoring surface that saves Views through the API: ```typescript {{ title: 'Validation' }} import { PERSISTED_VIEW_EXPERIENCE_CONFIGURATION_JSON_SCHEMA_ID, PersistedViewExperienceConfigurationJsonSchema, validateViewConfiguration, validateViewExperienceId, } from '@vertesia/common'; const issues = [ ...validateViewExperienceId('sales-orders'), ...validateViewConfiguration(salesOrdersView, 'persisted'), ]; if (issues.length > 0) { throw new Error(issues.map((issue) => `${issue.path || 'configuration'} ${issue.message}`).join('; ')); } monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: true, schemas: [ { uri: PERSISTED_VIEW_EXPERIENCE_CONFIGURATION_JSON_SCHEMA_ID, fileMatch: [model.uri.toString()], schema: PersistedViewExperienceConfigurationJsonSchema, }, ], }); ``` `validateViewConfiguration(config, mode)` runs structural JSON-Schema checks (shapes, enums, limits, required fields) **and** the semantic rules JSON Schema cannot express — unique IDs, and references from `default_display`, `default_sort`, and table columns to configured displays and sort options. Pass `'draft'` for an in-progress configuration or `'persisted'` for one about to be saved (which additionally requires `description`). ## Schema version Include `VIEW_EXPERIENCE_SCHEMA_VERSION` from `@vertesia/common` on create and update. The version pins the configuration contract so the server can reject payloads written against an incompatible schema. --- ## Search & agentic planning Source: https://docs.vertesiahq.com/content/view-experiences/search Markdown: https://docs.vertesiahq.com/llms/content/view-experiences/search.md A View's `search` section chooses how users search. There are two modes: **deterministic** (the default — keyword search plus structured key terms) and **agentic** (natural language, translated into a validated Elasticsearch query by a model). Both compose with the View's fixed scope, active navigation, project boundary, and content-security filters. Numeric ranges, date facets, and typed sorts depend on how the project indexes properties — see [Typed property indexing](#typed-property-indexing) at the end of this page. ## Deterministic search Deterministic search is the default. It combines the text query with the View's fixed scope, active navigation selections, structured key terms, configured sort, project boundary, and content-security filters. The default text query searches content name, description, full text, and properties. Key terms add predictable domain controls, each declaring a concrete `field`: - `match` performs analyzed text matching; - `term` performs an exact value filter; and - `range` accepts a `from..to` value, with either bound allowed to be empty. Use exact property terms for fields such as `properties.customer` and `properties.status`, while leaving the main query available for descriptions or order text. Omit the `search` section entirely for a browse-only experience; the runtime then rejects query and key-term input instead of silently ignoring it. ## Agentic query and presentation planning Set `search.mode` to `agentic` when users should express domain intent in natural language. The default `sys:ContentSearchAgent` interaction converts the request into a supported Elasticsearch query. The server limits the model to built-in fields and the fields disclosed by the View configuration, validates the returned query, then adds the immutable scope and content-security filters. Agentic Views support two planning modes: - `query` generates only the Elasticsearch query and uses the configured View presentation. - `query_and_view` generates the query plus a safe, ephemeral presentation suited to that question. A View has one model execution configuration. Set it under `search.agentic.config`, or omit it to inherit the system interaction and project defaults: ```json { "mode": "agentic", "placeholder": "Describe the sales orders you need", "fields": [ { "field": "text", "description": "Full ingested and OCR sales-order text.", "type": "text", "mode": "full_text" }, { "field": "properties.instructions", "description": "Customer instructions associated with the order.", "type": "text", "mode": "full_text", "boost": 2 }, { "field": "properties.order_total", "description": "Numeric order total.", "type": "number", "mode": "exact" } ], "agentic": { "interaction": "sys:ContentSearchAgent", "config": { "environment": "", "model": "" }, "mode": "query_and_view", "instructions": "Treat customer and sold-to state as exact filters. Prefer open orders.", "timeout_ms": 12000, "minimum_confidence": 0.6, "rerank": { "max_candidates": 30, "include_why_match": true, "timeout_ms": 15000 } } } ``` `query_and_view` uses one model call and the same model configuration as query-only planning. Alongside the validated query, the model may choose a title, description, browse/worklist layout, up to six property facets, one built-in list/table/cards/gallery/board display, and up to three sort clauses. For example, “compare orders over $10,000” can produce a numeric comparison table, while “browse product photos by brand” can produce a gallery with a brand facet. The generated presentation is returned as the execution response's `definition`; it is not persisted and the existing `ViewExperience` component renders it without a second API or component. The model cannot change the View's scope, search configuration, actions, selection, upload/drop behavior, or app-contributed renderers. Folder and collection navigation, active selections, and those configured behaviors remain server-owned and are merged into the ephemeral definition. ## Agentic result reranking Add `search.agentic.rerank` when Elasticsearch should retrieve the candidates and the model should make a bounded second pass over the results. Reranking is independent of `mode`: it works with both `query` and `query_and_view` and uses the same `search.agentic.config` model. By default it calls `sys:ContentSearchReranker`; set `interaction` inside `rerank` only when the project provides a purpose-built reranking interaction. The server sends at most `max_candidates` authorized hits from the current result page (default 30, maximum 50), with only fields from the View's validated field catalog. The interaction must return every candidate ID exactly once. Zeno rejects invented, omitted, or duplicate IDs, so reranking can change order but cannot change the candidate set, scope, total count, facets, or canonical content. Hits beyond the bounded prefix retain their Elasticsearch order. When `include_why_match` is true, the response may include a grounded explanation under `hit.annotation.why_match`; the built-in result layouts display it automatically. The execution response also reports `search.rerank.status` as `applied`, `fallback`, or `skipped` and includes the candidate count. Any applied configured sort—including an explicit user sort or a generated `query_and_view` sort—skips reranking. Query-planning fallback also skips it. A timeout, interaction failure, or invalid permutation returns the original Elasticsearch order with an `agentic_rerank_failed` warning. Reranking applies to each requested page independently. It is intended to improve findability inside a strong, bounded Elasticsearch candidate set, not to produce one global model-ranked order across every result page. `search.fields` is the **semantic field catalog** the model plans against. The active Elasticsearch mapping remains authoritative: a field configured as `full_text` participates in deterministic fallback only when it is actually mapped as `text`. An explicit field list replaces the default lexical fields and fails closed when none is actively mapped as text. When a natural-language plan contains scoring text clauses, results use Elasticsearch relevance unless the request explicitly selects a configured sort. Browse and structured-only plans continue to use `results.default_sort`. If query planning times out, fails validation, or does not meet `minimum_confidence`, the runtime **falls back to deterministic search** over the View's mapped text fields and returns an explicit warning. If only the generated presentation is invalid, the validated agent query still runs with the configured presentation and an `agentic_presentation_failed` warning. The fallback never applies fuzzy queries outside the validated field catalog. When no explicit `search.fields` list is configured, the broad lexical default searches `name`, `description`, full document `text`, and `properties.*`. Reranking preserves every candidate. Model-driven curation that removes results, query-specific answers, and generated excerpts remain future runtime work. ## Typed property indexing By default, indexed property strings are **keyword** fields — exact-match and aggregatable, which is what property facets and `term` key terms need. But a `range` facet, a date facet, or a numeric sort needs the field to be mapped with a **typed** Elasticsearch type. This is the most common reason a range or date control appears to do nothing. Two rules govern how properties are typed: - **JSON numbers and booleans** are inferred as native numeric/boolean types automatically, so store numbers as JSON numbers (not strings) if you want ranges and numeric sorts to work. - **Strings** map to `keyword`. Date-looking strings stay keywords too — JSON has no date scalar — so a date facet or date sort requires an **explicit** `date` mapping for that path. Declare explicit types in the project's `indexing.property_mappings`, keyed by dotted property path: ```json { "indexing": { "property_mappings": { "order_total": { "type": "double", "ignore_malformed": true }, "delivery_date": { "type": "date", "format": "strict_date_optional_time||epoch_millis", "ignore_malformed": true }, "customer_code": { "type": "keyword", "ignore_above": 256 } } } } ``` Supported `type` values are `keyword`, `text`, `boolean`, `long`, `double`, and `date`. `ignore_malformed` lets a single bad value be skipped instead of rejecting the whole document; `date` accepts a `format`; `keyword` accepts `ignore_above`. Changing `property_mappings` does **not** rewrite the active index. Elasticsearch cannot change the type of an existing field, so the new mapping only takes effect after a **full reindex** of the project. Until then, the field keeps its old type and typed Views over it will not work. See [Search Configuration](/content/search) for reindexing. New indexes store property leaves with `subobjects: false`, so a scalar path such as `properties.bill_to` can coexist with a more specific path such as `properties.bill_to.name`, even when source documents use nested JSON objects. The same leaf should still have a consistent scalar type across documents. --- ## Troubleshooting Source: https://docs.vertesiahq.com/content/view-experiences/troubleshooting Markdown: https://docs.vertesiahq.com/llms/content/view-experiences/troubleshooting.md Most View problems come down to a field name that does not match the index, a property type that was never mapped, or a configuration that saved but cannot execute. This page lists the common cases. ## A facet is empty A `terms` facet returns no values. - **Wrong field path.** The `field` must match the indexed path exactly, for example `properties.status`. Inspect real values before configuring a facet — the Studio editor's preview and Studio Assistant's `inspect_view_data` both show actual buckets. - **Not a keyword field.** Facets aggregate on keyword-typed fields. Property strings are keywords by default, but a field that was mapped as `text` (or is numeric) will not aggregate as a term facet. - **Nothing in scope matches.** The View's `scope` (or an active navigation selection) may exclude every document that has the property. Widen the scope or clear other selections to confirm. ## A numeric range or date facet does nothing A `range` facet or a numeric/date sort has no effect. This is almost always a **typed indexing** problem. Property strings map to `keyword`, and date-looking strings stay keywords — so a range or date control over them cannot work until the field is mapped with a numeric or `date` type and the project is **reindexed**. Store numbers as JSON numbers, declare explicit types in `indexing.property_mappings`, and run a full reindex. See [Typed property indexing](/content/view-experiences/search#typed-property-indexing). Remember: changing `property_mappings` does not rewrite the active index. The new type only applies after a full reindex. ## Validation returns HTTP 400 for a query Creating, updating, previewing, or executing a View returns a `400` that names an unsupported query clause. The most common cause is a `scope.fixed_filter` that uses an Elasticsearch query type outside the supported allowlist. Simplify it to supported clauses (`bool`, `term`, `terms`, `range`, `match`, `exists`, `prefix`, and similar), or move the constraint into `scope.type_ids`, `scope.collection_ids`, or `scope.locations`. Agentic plans go through the same query validator before they can reach Elasticsearch. A `400` can also come from an out-of-range execution input (for example a non-numeric `limit`); the returned path and message identify the invalid value. ## An old View cannot be saved Editing an existing View returns a `400` complaining that `description` is required. Persisted Views require a non-empty `description`. Current Studio deployments backfill older records from the View name, but a mixed-version deployment or record that has not yet been migrated can still surface this error. Supply a description in the update payload (or in the Studio editor) and save again. ## Agentic search fell back to deterministic The response `search` block reports `applied_mode: 'deterministic'` and a warning even though the View is configured as agentic. The runtime falls back to deterministic search when query planning **times out**, **fails validation**, or does not meet `minimum_confidence`. Check the warning code in the response. To reduce fallbacks: give the planner a clearer `search.fields` catalog with good `description` text, raise `timeout_ms`, lower `minimum_confidence` if it is too strict, or refine `search.agentic.instructions`. The fallback is safe by design — it never runs fuzzy queries outside the validated field catalog. ## Full-text search does not match extracted property text A query that should match text inside a property returns nothing. Property strings are indexed as `keyword`, which is exact-match only. For a property whose value is meaningful prose (for example extracted instructions), map the path as `text` in `indexing.property_mappings`, reindex, and list it in `search.fields` with `mode: 'full_text'`. See [Typed property indexing](/content/view-experiences/search#typed-property-indexing). ## Nothing loads / "Elasticsearch is not enabled" View execution requires Elasticsearch. Enable it for the project and ensure the scoped content is indexed. See [Search Configuration](/content/search). ## A custom renderer is not used You passed a `renderers` registry but the built-in renderer still shows. The `renderer` name on the `search`, navigation item, or display must match a key in the registry exactly. When a name is missing — including on the generic `/view/` route, which has no application registry — the component falls back to the built-in renderer for that section. See [Custom renderers](/content/view-experiences/embedding#custom-renderers). --- ## Dashboards Source: https://docs.vertesiahq.com/data-platform/dashboards Markdown: https://docs.vertesiahq.com/llms/data-platform/dashboards.md Create interactive data visualizations using Vega-Lite, a high-level grammar for building charts. Dashboards are backed by SQL queries and support cross-panel interactivity. ## Dashboard Structure A dashboard consists of: - **query**: Single SQL query returning all data needed - **spec**: Vega-Lite specification defining the visualization The system automatically injects data and schema - never include `$schema` or `data` in your spec. ## Workflow Always follow this workflow: 1. **Test query** - Verify SQL returns expected data 2. **Preview** - Use `data_preview_dashboard` to iterate 3. **Verify** - Check labels, colors, layout 4. **Create** - Save with `data_create_dashboard` 5. **Render** - Generate PNG with `data_render_dashboard` ## Chart Types ### Bar Chart ```json { "mark": "bar", "encoding": { "x": {"field": "category", "type": "nominal"}, "y": {"field": "value", "type": "quantitative"} } } ``` ### Line Chart ```json { "mark": {"type": "line", "point": true}, "encoding": { "x": {"field": "date", "type": "temporal"}, "y": {"field": "value", "type": "quantitative"} } } ``` ### Pie/Donut Chart ```json { "mark": {"type": "arc", "innerRadius": 50}, "encoding": { "theta": {"field": "value", "type": "quantitative"}, "color": {"field": "category", "type": "nominal"} } } ``` ### Scatter Plot ```json { "mark": "point", "encoding": { "x": {"field": "x_value", "type": "quantitative"}, "y": {"field": "y_value", "type": "quantitative"}, "color": {"field": "category", "type": "nominal"}, "size": {"field": "weight", "type": "quantitative"} } } ``` ### Heatmap ```json { "mark": "rect", "encoding": { "x": {"field": "x_category", "type": "nominal"}, "y": {"field": "y_category", "type": "nominal"}, "color": {"field": "value", "type": "quantitative", "scale": {"scheme": "blues"}} } } ``` ### Area Chart ```json { "mark": "area", "encoding": { "x": {"field": "date", "type": "temporal"}, "y": {"field": "value", "type": "quantitative"} } } ``` ## Field Types | Type | Description | Examples | |------|-------------|----------| | `quantitative` | Numbers (continuous) | Sales, temperature, count | | `nominal` | Categories (unordered) | Region, product type | | `ordinal` | Categories (ordered) | Rating, size (S/M/L) | | `temporal` | Dates/times | Order date, timestamp | ## Layout Options ### Vertical Stack (vconcat) ```json { "vconcat": [ {"title": "Chart 1", "mark": "bar", "encoding": {...}}, {"title": "Chart 2", "mark": "line", "encoding": {...}} ] } ``` ### Horizontal Layout (hconcat) ```json { "hconcat": [ {"title": "Left", "mark": "bar", "encoding": {...}}, {"title": "Right", "mark": "arc", "encoding": {...}} ] } ``` ### Grid Layout ```json { "concat": [ {"title": "Chart 1", "mark": "bar", "encoding": {...}}, {"title": "Chart 2", "mark": "line", "encoding": {...}}, {"title": "Chart 3", "mark": "point", "encoding": {...}}, {"title": "Chart 4", "mark": "arc", "encoding": {...}} ], "columns": 2 } ``` ## Cross-Panel Interactivity The key advantage of combined specs: selections in one panel can filter another. ### Point Selection Click to select, filter other panels: ```json { "vconcat": [ { "params": [{"name": "regionSelect", "select": {"type": "point", "fields": ["region"]}}], "mark": "bar", "encoding": { "x": {"field": "region", "type": "nominal"}, "y": {"aggregate": "sum", "field": "amount"}, "opacity": {"condition": {"param": "regionSelect", "value": 1}, "value": 0.3} } }, { "transform": [{"filter": {"param": "regionSelect"}}], "mark": "line", "encoding": { "x": {"field": "date", "type": "temporal"}, "y": {"aggregate": "sum", "field": "amount"} } } ] } ``` ### Brush Selection Drag to select a range: ```json { "hconcat": [ { "params": [{"name": "brush", "select": {"type": "interval", "encodings": ["x"]}}], "mark": "area", "encoding": { "x": {"field": "date", "type": "temporal"}, "y": {"aggregate": "sum", "field": "amount"} } }, { "transform": [{"filter": {"param": "brush"}}], "mark": "bar", "encoding": { "x": {"field": "category", "type": "nominal"}, "y": {"aggregate": "sum", "field": "amount"} } } ] } ``` ## Tooltips Always add tooltips for hover context: ```json { "encoding": { "tooltip": [ {"field": "date", "type": "temporal", "title": "Date", "format": "%B %d, %Y"}, {"field": "revenue", "type": "quantitative", "title": "Revenue", "format": "$,.0f"}, {"field": "category", "type": "nominal", "title": "Category"} ] } } ``` ### Number Formatting | Format | Input | Output | |--------|-------|--------| | `,.0f` | 1234567 | 1,234,567 | | `,.2f` | 1234.5 | 1,234.50 | | `.1%` | 0.125 | 12.5% | | `$,.0f` | 1234 | $1,234 | ### Date Formatting | Format | Output | |--------|--------| | `%Y` | 2024 | | `%B` | January | | `%b %Y` | Jan 2024 | | `%B %d, %Y` | January 15, 2024 | ## Color Schemes ### Categorical ```json { "color": { "field": "category", "type": "nominal", "scale": {"scheme": "category10"} } } ``` Available: `category10`, `category20`, `tableau10`, `tableau20`, `set1`, `set2`, `pastel1` ### Sequential ```json { "color": { "field": "value", "type": "quantitative", "scale": {"scheme": "blues"} } } ``` Available: `blues`, `greens`, `oranges`, `reds`, `purples`, `viridis`, `plasma` ### Custom Colors ```json { "color": { "field": "status", "type": "nominal", "scale": { "domain": ["low", "medium", "high"], "range": ["#22c55e", "#eab308", "#ef4444"] } } } ``` ## Query Parameters Make dashboards dynamic with parameterized SQL: ```json { "query": "SELECT * FROM sales WHERE date >= {{start_date}} AND region = {{region}}", "queryParameters": { "start_date": "CURRENT_DATE - INTERVAL 30 DAY", "region": "'US'" } } ``` Override at render time: ```json { "queryParameters": { "start_date": "CURRENT_DATE - INTERVAL 90 DAY", "region": "'EU'" } } ``` ## Transforms Process data within the visualization: ### Calculate ```json { "transform": [ {"calculate": "datum.revenue - datum.cost", "as": "profit"} ] } ``` ### Filter ```json { "transform": [ {"filter": "datum.sales > 1000"} ] } ``` ### Aggregate ```json { "transform": [ { "aggregate": [ {"op": "sum", "field": "sales", "as": "total_sales"} ], "groupby": ["category"] } ] } ``` ### Window Functions ```json { "transform": [ { "window": [ {"op": "sum", "field": "sales", "as": "cumulative_sales"} ], "sort": [{"field": "date"}] } ] } ``` ## Layering Overlay multiple marks: ### Line with Points ```json { "layer": [ {"mark": "line"}, {"mark": "point"} ], "encoding": { "x": {"field": "date", "type": "temporal"}, "y": {"field": "value", "type": "quantitative"} } } ``` ### Bar with Labels ```json { "layer": [ {"mark": "bar"}, { "mark": {"type": "text", "dy": -5}, "encoding": {"text": {"field": "value", "type": "quantitative"}} } ], "encoding": { "x": {"field": "category", "type": "nominal"}, "y": {"field": "value", "type": "quantitative"} } } ``` ### Reference Line ```json { "layer": [ {"mark": "bar", "encoding": {...}}, { "mark": "rule", "encoding": { "y": {"datum": 10000}, "color": {"value": "red"}, "strokeDash": {"value": [4, 4]} } } ] } ``` ## Axes & Legends ### Axis Customization ```json { "encoding": { "x": { "field": "date", "type": "temporal", "axis": {"title": "Date", "format": "%b %Y", "labelAngle": -45} }, "y": { "field": "revenue", "type": "quantitative", "axis": {"title": "Revenue ($)", "format": "$,.0f"} } } } ``` ### Legend Position ```json { "encoding": { "color": { "field": "category", "type": "nominal", "legend": {"title": "Category", "orient": "bottom"} } } } ``` Positions: `left`, `right`, `top`, `bottom`, `top-left`, `top-right`, `bottom-left`, `bottom-right` ## Versioning Dashboards support automatic versioning: ### Create Snapshot ```typescript await dashboardApi.createSnapshot(dashboardId, { name: 'q1-final', message: 'Finalized Q1 dashboard' }); ``` ### List Versions ```typescript const versions = await dashboardApi.listVersions(dashboardId); ``` ### Restore Version ```typescript await dashboardApi.promoteVersion(dashboardId, versionId); ``` ### Toggle Versioning ```typescript await dashboardApi.setVersioningEnabled(dashboardId, false); ``` ## Best Practices 1. **Always add tooltips** - Show values on hover for all charts 2. **Single query** - Use JOINs/CTEs to get all data in one query 3. **Preview first** - Iterate with `data_preview_dashboard` before saving 4. **Use muted colors** - Avoid harsh saturated colors 5. **Clear titles** - Each panel should explain itself 6. **Limit data** - Use SQL LIMIT or `queryLimit` parameter 7. **Test interactivity** - Verify selections filter correctly ## Color Guidelines Use soft, muted colors: | Purpose | Recommended | Avoid | |---------|-------------|-------| | Success | `#22c55e`, `#4ade80` | `#00ff00` | | Info | `#3b82f6`, `#60a5fa` | `#0000ff` | | Danger | `#ef4444`, `#f87171` | `#ff0000` | | Neutral | `#6b7280`, `#9ca3af` | `#000000` | ## Complete Example ```json { "query": "SELECT date, region, SUM(amount) as revenue FROM sales GROUP BY date, region", "spec": { "vconcat": [ { "title": "Click region to filter", "params": [{"name": "sel", "select": {"type": "point", "fields": ["region"]}}], "mark": "bar", "encoding": { "x": {"field": "region", "type": "nominal"}, "y": {"aggregate": "sum", "field": "revenue", "type": "quantitative"}, "opacity": {"condition": {"param": "sel", "value": 1}, "value": 0.3}, "tooltip": [ {"field": "region", "type": "nominal"}, {"aggregate": "sum", "field": "revenue", "type": "quantitative", "format": "$,.0f"} ] } }, { "title": "Revenue trend (filtered)", "transform": [{"filter": {"param": "sel"}}], "mark": {"type": "line", "point": true}, "encoding": { "x": {"field": "date", "type": "temporal"}, "y": {"aggregate": "sum", "field": "revenue", "type": "quantitative"}, "tooltip": [ {"field": "date", "type": "temporal", "format": "%B %Y"}, {"aggregate": "sum", "field": "revenue", "type": "quantitative", "format": "$,.0f"} ] } } ] } } ``` --- ## Getting Started Source: https://docs.vertesiahq.com/data-platform/getting-started Markdown: https://docs.vertesiahq.com/llms/data-platform/getting-started.md This guide walks you through creating your first DataStore, importing data, running queries, and creating a dashboard. ## Prerequisites - A Vertesia project with the Data Platform plugin installed - Data files (CSV, JSON, or Parquet) to import - Appropriate permissions to create DataStores and dashboards ## Step 1: Create a DataStore A DataStore is a DuckDB database that will hold your structured data. ### Using the UI 1. Navigate to the Data Platform section in your project 2. Click **New Project** to create a data project 3. Enter a name and description for your project 4. The system will create both a Collection (for files) and a DataStore (for structured data) ### Using the API ```typescript import { VertesiaClient } from '@vertesia/client'; const client = new VertesiaClient({ apiKey: 'your-api-key' }); const dataApi = client.data; // Create a new DataStore const store = await dataApi.create({ name: 'sales-analytics', description: 'Sales data for analytics dashboards', tags: ['analytics', 'sales'], }); console.log('Created DataStore:', store.id); ``` ## Step 2: Create Tables Define the schema for your data by creating tables. ### Using the API ```typescript // Create tables with relationships const tables = await dataApi.createTables(store.id, { tables: [ { name: 'customers', description: 'Customer information', columns: [ { name: 'id', type: 'INTEGER', primary_key: true }, { name: 'name', type: 'STRING', nullable: false }, { name: 'email', type: 'STRING', semantic_type: 'email' }, { name: 'created_at', type: 'TIMESTAMP' }, ], }, { name: 'orders', description: 'Customer orders', columns: [ { name: 'id', type: 'INTEGER', primary_key: true }, { name: 'customer_id', type: 'INTEGER', nullable: false }, { name: 'amount', type: 'DECIMAL', semantic_type: 'currency' }, { name: 'order_date', type: 'DATE' }, ], foreign_keys: [ { column: 'customer_id', references_table: 'customers', references_column: 'id', on_delete: 'CASCADE', }, ], }, ], }); ``` ### Using AI Schema Creation In the Data Platform UI, you can upload data files and use AI to automatically generate an optimal schema: 1. Upload your CSV/JSON/Parquet files to the project Collection 2. Click **Create Schema with AI** 3. The AI agent analyzes your files and proposes a schema 4. Review and approve the suggested tables and relationships ## Step 3: Import Data Import data from various sources into your tables. ### From Inline Data ```typescript const importJob = await dataApi.import(store.id, { tables: { customers: { source: 'inline', data: [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ], }, }, mode: 'append', message: 'Initial customer import', }); ``` ### From CSV Files ```typescript const importJob = await dataApi.import(store.id, { tables: { orders: { source: 'url', url: 'https://example.com/data/orders.csv', format: 'csv', }, }, mode: 'replace', message: 'Import orders from CSV', }); ``` ### Import Modes - **append**: Add new rows to existing data - **replace**: Replace all existing data in the table ## Step 4: Query Data Execute SQL queries against your DataStore using DuckDB syntax. ```typescript const result = await dataApi.query(store.id, { sql: ` SELECT c.name, COUNT(o.id) as order_count, SUM(o.amount) as total_spent FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.id, c.name ORDER BY total_spent DESC LIMIT 10 `, limit: 100, }); console.log('Columns:', result.columns); console.log('Rows:', result.rows); console.log('Execution time:', result.execution_time_ms, 'ms'); ``` ### Query Features DuckDB provides powerful analytics capabilities: - Window functions: `ROW_NUMBER()`, `LAG()`, `LEAD()`, `RANK()` - Common Table Expressions (CTEs) - `QUALIFY` clause for filtering window function results - Pivoting and unpivoting - JSON functions for semi-structured data ## Step 5: Create a Dashboard Create a Vega-Lite dashboard to visualize your data. ```typescript const dashboardApi = dataApi.dashboards(store.id); const dashboard = await dashboardApi.create({ name: 'Sales Overview', description: 'Key sales metrics and trends', query: ` SELECT DATE_TRUNC('month', order_date) as month, SUM(amount) as revenue FROM orders GROUP BY 1 ORDER BY 1 `, spec: { $schema: 'https://vega.github.io/schema/vega-lite/v5.json', mark: 'bar', encoding: { x: { field: 'month', type: 'temporal', title: 'Month' }, y: { field: 'revenue', type: 'quantitative', title: 'Revenue' }, }, }, }); console.log('Created dashboard:', dashboard.id); ``` ### Preview Before Saving Test your visualization without saving: ```typescript // Preview returns a PNG image const preview = await dashboardApi.preview({ query: 'SELECT category, SUM(amount) as total FROM orders GROUP BY category', spec: { mark: 'arc', encoding: { theta: { field: 'total', type: 'quantitative' }, color: { field: 'category', type: 'nominal' }, }, }, }); ``` ## Next Steps Now that you have the basics, explore more advanced features: - [Tools Reference](/data-platform/tools) - All Data Platform tools for AI agents - [Skills Reference](/data-platform/skills) - Reusable skills for data workflows - [Dashboards Guide](/data-platform/dashboards) - Advanced Vega-Lite visualizations - [API Reference](/api/openapi#tag/data) - Complete REST API documentation --- ## Overview Source: https://docs.vertesiahq.com/data-platform/overview Markdown: https://docs.vertesiahq.com/llms/data-platform/overview.md The **Data Platform** is Vertesia's solution for unified data management, combining AI-powered schema creation, SQL analytics, and interactive dashboards in one integrated experience. ## What is the Data Platform? The Data Platform enables you to: - **Manage structured data** using DuckDB-backed DataStores - **Import data** from CSV, JSON, Parquet, and Excel files - **Query data** using standard SQL with DuckDB extensions - **Create dashboards** with Vega-Lite visualizations - **Analyze data** with AI assistance through specialized agents ## Key Concepts ### DataStores A DataStore is a DuckDB database that stores your structured data. Each DataStore: - Contains one or more tables with defined schemas - Supports SQL queries with DuckDB's powerful analytics extensions - Provides automatic versioning for schema changes and data imports - Can be linked to a Collection for file-based data sources ### Tables Tables within a DataStore have: - **Columns** with types: STRING, INTEGER, BIGINT, FLOAT, DOUBLE, DECIMAL, BOOLEAN, DATE, TIMESTAMP, JSON - **Semantic types** for enhanced understanding: email, phone, url, currency, percentage, person_name, address, country, date_iso, identifier - **Foreign key relationships** with referential integrity - **Indexes** for query optimization ### Dashboards Dashboards are Vega-Lite visualizations backed by SQL queries: - **Single or multi-panel** layouts with vconcat, hconcat, or grid arrangements - **Interactive selections** for cross-filtering between panels - **Query parameters** with `{{param}}` syntax for dynamic filtering - **Automatic versioning** with named snapshots for important states ### Projects A Project links a Collection (files) with a DataStore (database): - Files in the Collection serve as data sources - The DataStore holds the structured, queryable data - Linked via `dp:` tag - AI agents can analyze files and automatically create schemas ## Architecture ``` Collection (files) DataStore (DuckDB) │ │ └──── dp: tag ───┘ │ Project linking ``` - **Tag-based linking**: Collections and DataStores are connected through tags - **GCS storage**: DataStore files are stored in Google Cloud Storage - **Versioning**: Automatic versions created on schema changes and imports - **Snapshots**: Named snapshots protected from automatic cleanup ## Use Cases ### Business Intelligence Dashboards Create interactive dashboards that visualize key metrics from your data. Combine bar charts, line graphs, and tables to tell a data story. ### Data Analysis with AI Use AI agents to explore your data, run complex queries, and generate insights. The AI can understand your schema and write appropriate SQL queries. ### ETL and Data Import Import data from various sources (CSV, JSON, Parquet) with atomic operations. Transform and clean data during import with column mapping and type conversion. ### Schema Design Let AI analyze your data files and suggest optimal database schemas with appropriate types, relationships, and indexes. ## Next Steps - [Getting Started](/data-platform/getting-started) - Create your first DataStore and dashboard - [Tools Reference](/data-platform/tools) - Complete reference for Data Platform tools - [Skills Reference](/data-platform/skills) - Learn about data-focused agent skills - [Dashboards](/data-platform/dashboards) - Deep dive into Vega-Lite visualizations --- ## Skills Reference Source: https://docs.vertesiahq.com/data-platform/skills Markdown: https://docs.vertesiahq.com/llms/data-platform/skills.md Skills are reusable capabilities that unlock specialized tools and provide domain-specific knowledge for AI agents. The Data Platform includes five skills for comprehensive data workflows. ## Overview | Skill | Purpose | Tools Unlocked | |-------|---------|----------------| | `data_analysis` | Query and analyze data with SQL/DuckDB | `data_get_schema`, `data_list_tables`, `execute_shell` | | `data_import` | Import data from files with atomic operations | `data_import`, `execute_shell` | | `data_migration` | Schema migrations with data transformations | `data_alter_table`, `data_create_tables`, `data_import`, `execute_shell` | | `data_modeling` | Design database schemas | `data_create_database`, `data_create_tables`, `data_alter_table` | | `data_visualization` | Create Vega-Lite dashboards | `data_preview_dashboard`, `data_create_dashboard`, `data_update_dashboard`, `data_render_dashboard` | ## data_analysis Query and analyze persistent data stores using SQL with DuckDB. ### When to Use - Data is in a persistent data store (created via `data_modeling` + `data_import`) - SQL-based analytics, reusable queries, or data versioning needed - Complex analytics like window functions, CTEs, or cohort analysis ### Recommended Approach Use native DuckDB in the sandbox via `execute_shell` for best performance: ```python import duckdb # Connect to synced database con = duckdb.connect('/home/daytona/databases/my_store.duckdb') # Run any SQL query result = con.execute(''' SELECT customer_id, SUM(amount) as total FROM orders GROUP BY customer_id ORDER BY total DESC LIMIT 10 ''').fetchdf() print(result) ``` ### Key Features - **Window Functions**: `ROW_NUMBER()`, `LAG()`, `LEAD()`, `RANK()` - **CTEs**: Break complex queries into readable parts - **QUALIFY Clause**: Filter window function results - **Pivoting**: Transform rows to columns - **Multi-database queries**: Join across multiple databases ### Example: Cohort Analysis ```sql WITH first_purchase AS ( SELECT customer_id, MIN(order_date) as cohort_date FROM orders GROUP BY customer_id ) SELECT DATE_TRUNC('month', fp.cohort_date) as cohort, DATE_DIFF('month', fp.cohort_date, o.order_date) as months_since, COUNT(DISTINCT o.customer_id) as customers FROM orders o JOIN first_purchase fp ON o.customer_id = fp.customer_id GROUP BY 1, 2 ``` ## data_import Import data from CSV, JSON, Parquet files or inline data with atomic multi-table support. ### When to Use - Loading data from files into data stores - Batch data updates or refreshes - ETL workflows with data transformation ### Supported Formats - **CSV**: Comma-separated values with header row - **JSON**: Array of objects or newline-delimited JSON - **Parquet**: Columnar format for large datasets ### Import Modes - **append**: Add new rows to existing data (default) - **replace**: Clear table before importing ### Data Sources | Source | URI Format | Example | |--------|------------|---------| | inline | (data in `data` field) | Direct JSON array | | gcs | `gs://bucket/path` | `gs://my-bucket/data.csv` | | url | `https://...` | `https://example.com/data.csv` | | artifact | `out/filename` | `out/cleaned_data.csv` | ### Column Normalization When importing, column names are automatically normalized: - `Item Type` → `item_type` - `Order-ID` → `order_id` - `123_count` → `_123_count` Create table schemas using normalized names. ### Example: Multi-Table Import ```json { "mode": "replace", "message": "Monthly data refresh", "tables": { "customers": { "source": "url", "uri": "https://example.com/customers.csv", "format": "csv" }, "orders": { "source": "gcs", "uri": "gs://bucket/orders.parquet", "format": "parquet" } } } ``` ## data_migration Perform schema migrations with data transformations. ### When to Use Migration vs Alter | Change | Use | |--------|-----| | Add nullable column | `data_alter_table` | | Drop column | `data_alter_table` | | Rename column | `data_alter_table` | | Change column type | **Migration** | | Split column into two | **Migration** | | Merge columns | **Migration** | | Restructure table | **Migration** | ### Migration Workflow 1. **Create snapshot** before migration 2. **Export data** from current table 3. **Transform** with Python/pandas 4. **Update schema** with new structure 5. **Import** transformed data ### Common Patterns **Change Column Type:** ```python df['amount'] = pd.to_numeric(df['amount'], errors='coerce') ``` **Split Column:** ```python df[['first_name', 'last_name']] = df['full_name'].str.split(' ', n=1, expand=True) ``` **Merge Columns:** ```python df['full_address'] = df['street'] + ', ' + df['city'] + ', ' + df['state'] ``` ### Safety Best Practices 1. Always create a named snapshot before migration 2. Test on sample data first 3. Validate row counts before and after 4. Know how to rollback from snapshot ## data_modeling Create databases and design data store schemas with tables, columns, and relationships. ### When to Use - Creating new data stores - Designing table schemas - Defining relationships between tables ### Schema Design Best Practices **Table Naming:** - Use snake_case: `customer_orders` - Use plural names: `customers`, `products` - Descriptive junction tables: `customer_product_access` **Column Types:** - `STRING` - Text data - `INTEGER` / `BIGINT` - Whole numbers - `DECIMAL` - Precise decimals (financial data) - `BOOLEAN` - True/false - `DATE` / `TIMESTAMP` - Temporal values - `JSON` - Structured data **Semantic Types:** Add hints for AI understanding: - `email`, `phone`, `url` - Contact info - `currency`, `percentage` - Financial values - `person_name`, `address` - Personal info - `identifier` - IDs and codes ### Example: E-Commerce Schema ```json { "tables": [ { "name": "customers", "columns": [ {"name": "id", "type": "INTEGER", "primary_key": true}, {"name": "email", "type": "STRING", "semantic_type": "email"}, {"name": "name", "type": "STRING", "semantic_type": "person_name"} ] }, { "name": "orders", "columns": [ {"name": "id", "type": "INTEGER", "primary_key": true}, {"name": "customer_id", "type": "INTEGER"}, {"name": "amount", "type": "DECIMAL", "semantic_type": "currency"} ], "foreign_keys": [ {"column": "customer_id", "references_table": "customers", "references_column": "id"} ] } ] } ``` ## data_visualization Create persistent Vega-Lite dashboards with SQL-backed data. ### When to Use - Data is in a persistent data store - Saved, reusable dashboards needed - Cross-panel interactivity (selections filter other panels) ### Workflow 1. **Test query** - Verify SQL returns expected data 2. **Preview** - Use `data_preview_dashboard` to iterate 3. **Verify** - Check labels, colors, layout 4. **Create** - Save with `data_create_dashboard` 5. **Render** - Generate PNG with `data_render_dashboard` ### Dashboard Structure A dashboard consists of: - **query**: Single SQL query returning all data - **spec**: Vega-Lite specification (no `$schema` or `data` - injected automatically) ### Layout Options | Layout | Description | |--------|-------------| | `vconcat` | Vertical stack | | `hconcat` | Horizontal layout | | `concat` + `columns` | Grid layout | ### Cross-Panel Interactivity Selections in one panel can filter another: ```json { "vconcat": [ { "params": [{"name": "regionSelect", "select": {"type": "point", "fields": ["region"]}}], "mark": "bar", "encoding": { "x": {"field": "region"}, "y": {"aggregate": "sum", "field": "amount"} } }, { "transform": [{"filter": {"param": "regionSelect"}}], "mark": "line", "encoding": { "x": {"field": "date", "type": "temporal"}, "y": {"aggregate": "sum", "field": "amount"} } } ] } ``` ### Query Parameters Make dashboards dynamic with parameterized SQL: ```json { "query": "SELECT * FROM sales WHERE date >= {{start_date}}", "queryParameters": { "start_date": "CURRENT_DATE - INTERVAL 30 DAY" } } ``` ### Best Practices 1. **Always add tooltips** - Show values on hover 2. **Single query** - Use JOINs/CTEs for all needed data 3. **Preview first** - Verify before saving 4. **Use muted colors** - Avoid harsh saturated colors 5. **Clear titles** - Each panel should explain itself ## Skill Combinations Skills work together for complete workflows: **Data Pipeline:** 1. `data_modeling` - Create schema 2. `data_import` - Load initial data 3. `data_analysis` - Query and explore 4. `data_visualization` - Create dashboards **Schema Evolution:** 1. `data_modeling` - Initial schema 2. `data_analysis` - Identify needed changes 3. `data_migration` - Transform schema and data **Reporting Workflow:** 1. `data_analysis` - Develop queries 2. `data_visualization` - Create dashboard 3. `data_analysis` - Refine based on feedback ## Related Documentation - [Tools Reference](/data-platform/tools) - Detailed tool parameters - [Dashboards Guide](/data-platform/dashboards) - Vega-Lite deep dive - [API Reference](/api/openapi#tag/data) - REST API documentation --- ## Tools Reference Source: https://docs.vertesiahq.com/data-platform/tools Markdown: https://docs.vertesiahq.com/llms/data-platform/tools.md The Data Platform provides a comprehensive set of tools for AI agents to manage data stores, tables, queries, and dashboards. Tools are organized into categories based on their function. ## Read Tools These tools are enabled by default and provide read-only access to data stores. ### data_get_schema Get the schema of a data store including table definitions, columns, and relationships. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `format` | string | No | `'full'` for complete details, `'data'` for AI-optimized summary (default) | **Returns:** Schema with tables, columns, relationships, and metadata. ### data_list_tables List all tables in a data store with metadata. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | **Returns:** Array of tables with column counts and row counts. ### data_list_dashboards List dashboards in a data store with optional status filtering. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `status` | string | No | Filter by status: `'active'` or `'archived'` | **Returns:** Array of dashboard summaries. ### data_list_dashboard_versions List version history for a dashboard. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `dashboard_id` | string | Yes | The ID of the dashboard | | `limit` | number | No | Maximum versions to return | **Returns:** Array of version records with timestamps and snapshot names. ## Write Tools These tools are disabled by default and must be unlocked by skills. They modify data stores. ### data_create_database Create a new DuckDB database for storing analytical data. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `name` | string | Yes | Database name (lowercase with underscores) | | `summary` | string | No | Description of the database purpose | **Returns:** Created data store with ID. ### data_create_tables Create one or more tables atomically in a single transaction. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `tables` | array | Yes | Array of table definitions | | `message` | string | Yes | Commit message for version history | **Table Definition:** ```json { "name": "customers", "description": "Customer information", "columns": [ { "name": "id", "type": "INTEGER", "primary_key": true }, { "name": "email", "type": "STRING", "semantic_type": "email", "nullable": false } ], "foreign_keys": [ { "column": "account_id", "references_table": "accounts", "references_column": "id", "on_delete": "CASCADE" } ] } ``` **Column Types:** `STRING`, `INTEGER`, `BIGINT`, `FLOAT`, `DOUBLE`, `DECIMAL`, `BOOLEAN`, `DATE`, `TIMESTAMP`, `JSON` **Semantic Types:** `email`, `phone`, `url`, `currency`, `percentage`, `person_name`, `address`, `country`, `date_iso`, `identifier` ### data_alter_table Modify an existing table schema. Creates a version snapshot before changes. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `table_name` | string | Yes | The table to alter | | `add_columns` | array | No | Columns to add | | `drop_columns` | array | No | Column names to remove | | `rename_columns` | array | No | Columns to rename (`{from, to}`) | | `modify_columns` | array | No | Column modifications | ### data_import Import data into one or more tables atomically. Creates a version snapshot before import. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `tables` | object | Yes | Map of table names to import configs | | `mode` | string | No | `'append'` (default) or `'replace'` | | `message` | string | No | Description for version history | **Import Sources:** - **inline**: Data provided directly in the `data` field - **gcs**: Google Cloud Storage path (`gs://bucket/path`) - **url**: HTTPS URL to file - **artifact**: Sandbox output file (`out/filename.csv`) **Example:** ```json { "store_id": "store123", "tables": { "customers": { "source": "inline", "data": [ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"} ] }, "orders": { "source": "url", "uri": "https://example.com/orders.csv", "format": "csv" } }, "mode": "append", "message": "Initial data import" } ``` ### data_query (Deprecated) Execute SQL queries against a data store. Prefer using native DuckDB via `execute_shell` for better performance. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `sql` | string | Yes | SQL query to execute | | `params` | object | No | Query parameters for `{{param}}` placeholders | | `limit` | number | No | Max rows to return (default: 100, max: 10000) | ## Dashboard Tools Tools for creating and managing Vega-Lite visualizations. ### data_preview_dashboard Preview a dashboard without saving. Renders to PNG for iteration. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `query` | string | Yes | SQL query for dashboard data | | `spec` | object | Yes | Vega-Lite specification | | `queryLimit` | number | No | Max rows (default: 10000) | | `queryParameters` | object | No | Default values for `{{param}}` placeholders | | `scale` | number | No | Scale factor (default: 1, use 2 for retina) | | `backgroundColor` | string | No | Background color (default: `'#ffffff'`) | **Returns:** PNG image of the rendered dashboard. ### data_create_dashboard Create a new saved dashboard. Use `data_preview_dashboard` first to iterate on design. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `name` | string | Yes | Dashboard name (unique within store) | | `summary` | string | No | Description of the dashboard | | `query` | string | Yes | SQL query for dashboard data | | `spec` | object | Yes | Vega-Lite specification | | `queryLimit` | number | No | Max rows (default: 10000) | | `queryParameters` | object | No | Default parameter values | ### data_update_dashboard Update an existing dashboard. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `dashboard_id` | string | Yes | The dashboard to update | | `name` | string | No | New name | | `summary` | string | No | New description | | `query` | string | No | New SQL query | | `spec` | object | No | New Vega-Lite specification | ### data_render_dashboard Render a saved dashboard to PNG. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `dashboard_id` | string | Yes | The dashboard to render | | `queryParameters` | object | No | Override default parameters | | `scale` | number | No | Scale factor | ### data_snapshot_dashboard Create a named snapshot of the current dashboard state. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `dashboard_id` | string | Yes | The dashboard to snapshot | | `name` | string | Yes | Snapshot name | | `message` | string | No | Description of this snapshot | ### data_promote_dashboard_version Restore a specific version as the current dashboard state. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `dashboard_id` | string | Yes | The dashboard | | `version_id` | string | Yes | The version to promote | ### data_set_dashboard_versioning Enable or disable automatic versioning for a dashboard. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `store_id` | string | Yes | The ID of the data store | | `dashboard_id` | string | Yes | The dashboard | | `enabled` | boolean | Yes | Whether versioning is enabled | ## Best Practices ### Atomic Operations All schema changes and imports are atomic: - Creating multiple tables happens in a single transaction - Multi-table imports succeed or fail together - Rollback on any failure preserves data integrity ### Versioning - Automatic versions created on schema changes and imports - Named snapshots protected from 30-day TTL cleanup - Use snapshots for important milestones - Rollback available for any version ### Query Optimization For complex analytics, use native DuckDB via `execute_shell`: ```sql -- Window functions SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY amount DESC) as rank FROM orders; -- QUALIFY clause SELECT * FROM orders QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) = 1; ``` ### Dashboard Workflow 1. Test query separately to verify data 2. Use `data_preview_dashboard` to iterate on visualization 3. Verify the preview looks correct 4. Use `data_create_dashboard` to save 5. Use `data_render_dashboard` to generate final output --- ## Configuration Source: https://docs.vertesiahq.com/environments Markdown: https://docs.vertesiahq.com/llms/environments.md Vertesia is a bring your own key service which supports many model inference service providers such as OpenAI, Google Cloud, Amazon Bedrock and many others. Users can configure and use several providers of their choice, either by using Studio, the REST API or the SDK. Please refer to each provider documentation page to learn how to use it with Vertesia: * [Amazon Bedrock](/environments/aws) * [Google Cloud](/environments/gcp) * [Microsoft Azure](/environments/azure) * [OpenAI](/environments/openai) For Amazon Bedrock and Google Cloud, helper scripts to automate and the configuration are available [here](https://github.com/vertesia/composableai/tree/main/tools/environment-configuration). For other [inference providers](/concepts#inference-providers), select the provider from the list in Studio and add your API key. ## Vertesia Execution Environments Vertesia introduces the concept of Execution Environment. An environment is configured to use an inference provider and contains the individual models that are available. Vertesia also provides managed environments, including Amazon Bedrock and Google Cloud, that you can use to get started quickly and try out the platform — just accept the default setting when creating your new project. ![Environment List](/environments/environment_list.png) ## Execution Environment An Execution Environment attributes include: * An inference service provider * An API key * An Endpoint URL * Available models from the provider that can be enabled * Enabled models * Projects within the user's Vertesia organization that can use environment ![Environment Detail](/environments/environment_detail.png) --- ## Amazon Bedrock Source: https://docs.vertesiahq.com/environments/aws Markdown: https://docs.vertesiahq.com/llms/environments/aws.md Vertesia uses Workload Identity Federation to access AWS services, and act as an OIDC Identity Provider for AWS. In practice this means that Vertesia presents a Vertesia-generated, cryptographically-signed token (JWT), that is verified by AWS Secure Token Service, and then STS issues in exchange a short lived access token that assign the role and permissions defined in the AWS configuration. Before you start: please make sure you have enabled models in Bedrock. See: [Manage access to Amazon Bedrock foundation models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html). In addition to this documentation page, a helper script which automates all the configuration steps is available [here](https://github.com/vertesia/composableai/tree/main/tools/environment-configuration). ## Configure OIDC Provider * Login to the AWS Console * Go to **IAM** and select **Identity Providers** * Select **Add provider** * Select **OpenID Connect**,set provider to **https://sts.vertesia.io** and the Audience to **bedrock**. You can add any tag, to match your company policies or preferences. You now have configured the identity provider to verify and decode tokens created by Vertesia. To configure the access to Bedrock, you simply need to create a role, and authorize your Vertesia Environment to access Bedrock, as Vertesia generates an environment-specific token each time it’s calling the Bedrock API. If you plan to have multiple Vertesia Environments for Bedrock, or run Bedrock in multiple AWS regions (which requires multiple Vertesia Environments), then this must be configured for each Vertesia Environment. ## Configure a new Vertesia Execution Environment * Open [Vertesia Studio](https://cloud.vertesia.io/studio/) * First, copy your Organization ID as it will be required in the following steps. You can display your Organization ID by clicking on the user info icon in the top right corner of the Studio UI ![User Info](/environments/aws/vertesia_organization_id.png) * Go to **Environments** * Create a new Environment, select **bedrock** as Provider, put the **Bedrock region in Endpoint URL** ![AWS Identity Provider](/environments/aws/aws_vertesia_new_env.png) * Take note of the environment ID ![Vertesia Environment ID](/environments/aws/aws_vertesia_env_id.png) ## Configure an AWS IAM Role * Open the AWS Console * Go to **IAM**, select **Roles** * Create a new Role (e.g. Vertesia Environment X) * Select **Web Identity** as Trusted Entity Type, select the newly created **sts.vertesia.io** Identity Provider, and the Audience **bedrock**. * Add Condition: * Key: **sts.vertesia.io:sub** * Condition **StringEquals** * Value: **env:ORGANIZATION_ID:ENVIRONMENT_ID** You need to replace the values ORGANIZATION_ID with your Vertesia Organization ID, and ENVIRONMENT_ID with the Environment ID in Vertesia that will access the account. * Select or Create a Policy that gives access, for example **AmazonBedrockFullAccess**, or you can create a more narrow one depending on the use case for this environment. * Enter a Role Name, then review and create the Role * Select the new role created, and take note of the Role ARN now displayed, in this case: **arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/VertesiaEnvironmentX** ## Finalize the Vertesia Execution Environment Configuration * Go to **Environments** * Select the Environment you have previously created * Set the API Key to the Role ARN of the role in AWS, in this example: **arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/VertesiaEnvironmentX** * Refresh the page. * You should now be able to see models listed from your Bedrock environment. --- ## Microsoft Azure Source: https://docs.vertesiahq.com/environments/azure Markdown: https://docs.vertesiahq.com/llms/environments/azure.md ## Overview This guide explains how to connect Microsoft Foundry to Vertesia, enabling Vertesia to access large language models (LLMs) hosted by Microsoft on Azure. The integration uses **workload identity federation** (passwordless authentication) through Microsoft Entra ID, meaning no long-lived secrets or API keys from Azure are stored in Vertesia — instead, Vertesia's Security Token Service (STS) exchanges tokens with Microsoft Entra on behalf of each environment. By the end of this guide, you will have: - A Microsoft Foundry resource with a project and deployed models - A Microsoft Entra app registration with a federated credential linked to your Vertesia environment - The Entra app granted appropriate permissions to call the Microsoft Foundry APIs - A configured Vertesia environment that can use your Azure-hosted models --- ## Architecture Overview Understanding the hierarchy of objects is essential before starting: ``` Azure Subscription └── Resource Group └── Microsoft Foundry Resource (the top-level Azure resource) └── AI Foundry Project (the workspace you use in the portal) └── Model Deployments (the specific models you enable) Microsoft Entra ID (separate from AI Foundry) └── App Registration (represents Vertesia as an external client) ├── Federated Credential (trusts Vertesia's STS to exchange tokens) └── Service Principal (the identity that is assigned roles in Azure) ``` **Key insight:** The Entra App Registration acts as the "bridge" between Vertesia and Azure. Vertesia uses federated identity (not a traditional API key) to request tokens from Microsoft Entra, which then authorizes calls to the Microsoft Foundry APIs. The "API key" field in Vertesia is actually a combination of the **Tenant ID** and **Client ID** from the app registration — not a traditional Azure key. --- ## Prerequisites Before you begin, ensure you have the following: ### Azure Requirements | Requirement | Details | | :---- | :---- | | Azure account | An active Azure subscription | | Permissions | **Contributor** role (or higher) on the target subscription or resource group to create Microsoft Foundry resources | | Role assignment rights | **Owner** role on the subscription or resource group to assign roles to the app registration's service principal | | Microsoft Entra access | **Application Administrator**, **Cloud Application Administrator**, or equivalent role to create app registrations and federated credentials | > **Note:** If you have Owner on the subscription, this covers all the above requirements. If you only have Contributor, you will need to work with an Azure administrator to complete the IAM role assignment step. ### Vertesia Requirements | Requirement | Details | | :---- | :---- | | Vertesia account | Access to the Vertesia environment where you want to configure the AI provider | | Environment ID | The ID of the Vertesia environment (found in the environment settings page URL or settings panel) | | Vertesia STS Issuer URL | The token issuer URL for Vertesia's Security Token Service (obtain this from your Vertesia administrator or the provider setup instructions in the UI) | --- ## Part 1: Set Up the Microsoft Foundry Resource ### Step 1: Navigate to Microsoft Foundry 1. Open a browser and go to the [Azure Portal](https://portal.azure.com). 2. In the search bar at the top, type **Microsoft Foundry** and select it from the results. 3. You will land on the Microsoft Foundry overview page. > > **Important distinction:** There are two types of AI resource in Azure: > > - **AI Foundry Resource** (recommended): Provides access to managed foundation models. This is the correct type for most enterprise use cases. > - **AI Hub Resource** (legacy): Required only for Hugging Face models. Unless specifically requested, use the standard AI Foundry Resource. ### Step 2: Create a Microsoft Foundry Resource 1. From the Microsoft Foundry overview page, click **Create a resource** (or **\+ Create**). 2. A resource creation form will appear. Fill in the following fields: | Field | Value | | :---- | :---- | | **Subscription** | Select your Azure subscription | | **Resource Group** | Select an existing resource group, or create a new one (e.g., `my-company-ai`) | | **Region** | Select the Azure region closest to your team or where your data should reside | | **Name** | Enter a unique name (e.g., `mycompany-ai-resource`). This name will become part of your endpoint URL and **must be globally unique** | | **Pricing tier** | Standard S0 | 3. Ensure the option to allow project management is enabled (this is typically enabled by default for AI Foundry resources created through the portal). 4. Click **Review \+ Create**, then **Create**. 5. Wait for the deployment to complete (typically 1–3 minutes). > > **Endpoint URL Note:** The resource name you choose becomes the subdomain in your endpoint URL. For example, if you name the resource `mycompany-ai`, your endpoint will be `https://mycompany-ai.services.ai.azure.com/`. Both `*.services.ai.azure.com` and `*.openai.azure.com` formats are accepted. ### Step 3: Open the Microsoft Foundry Portal 1. Once the resource is created, navigate to it in the Azure Portal (search for the resource name, or find it in the resource group). 2. On the resource's overview page, click **Go to Microsoft Foundry Portal**. 3. This opens the AI Foundry Portal at `ai.azure.com`, scoped to your resource. --- ## Part 2: Create a Project Projects are the primary workspaces within an AI Foundry Resource. Each resource can contain multiple projects. Model deployments and API access are organized at the project level. ### Step 4: Create a New Project 1. In the Microsoft Foundry Portal (`ai.azure.com`), look for the project selector in the upper-left area. 2. Click **Create new project** (or select **Create a project** from the main screen). 3. Enter a project name (e.g., `vertesia-production`). This name will also appear in your Vertesia environment configuration. 4. Click **Create project** and wait for the project to initialize. > > **Key rule:** One federated credential in Microsoft Entra corresponds to one Vertesia environment. If you plan to connect multiple Vertesia environments (e.g., staging and production) to the same Microsoft Foundry resource, you can reuse the same resource and project with multiple federated credentials — or create separate projects for better isolation. --- ## Part 3: Deploy Models Before Vertesia can call any models, you must explicitly deploy the models you want to use within your project. ### Step 5: Deploy a Model 1. In the Microsoft Foundry Portal, with your project open, navigate to **Models and Endpoints** in the left sidebar (under the **Build** or **My assets** section). 2. Click **Deploy model**. 3. Select the model catalog source. 4. Browse or search for the model you want. 5. Click **Deploy** on the model you want. 6. A deployment configuration panel will appear: - **Deployment name:** A name for this deployment (recommend using the default model name). This name is used when making API calls. - **Deployment type:** Standard (recommended for most use cases) - **Tokens per minute (TPM) rate limit:** Set based on your expected usage 7. Click **Deploy** to confirm. > > **Deployment name matters:** When Vertesia makes API calls, it uses the deployment name you assign here. If you use a custom name, you will need to ensure Vertesia is configured accordingly. Using the default model name simplifies this. Repeat this step for each model you want to make available through Vertesia. --- ## Part 4: Collect Microsoft Foundry Connection Details Before moving to the identity configuration, collect the following values — you will need them when configuring Vertesia. ### Step 6: Get Your Resource Name and Project Name From the AI Foundry Portal, note: - **Resource Name:** The name you gave the resource in Step 2 (e.g., `mycompany-ai`) - **Project Name:** The name of the project created in Step 4 (e.g., `mycompany-production`) The **Endpoint URL** for your Vertesia configuration will be: ``` https://.services.ai.azure.com/ ``` > You can also find the project endpoint directly in the AI Foundry Portal on the project overview page (the "project endpoint" field). --- ## Part 5: Create a Microsoft Entra App Registration This step creates the identity that Vertesia will use to authenticate with Azure. You are not creating a user — you are registering Vertesia as a trusted external application. ### Step 7: Open Microsoft Entra ID 1. Go to the [Azure Portal](https://portal.azure.com). 2. In the search bar, type **Microsoft Entra ID** and select it. > > **Navigation note:** Microsoft Entra ID is sometimes referred to as "Azure Active Directory" in older documentation. Accessing it through the Azure Portal (portal.azure.com) is the most straightforward path. ### Step 8: Create an App Registration 1. In the Microsoft Entra ID blade, select **App registrations** from the left navigation menu. 2. Click **\+ New registration** at the top. 3. Fill in the registration form: | Field | Value | | :---- | :---- | | **Name** | A descriptive name for this registration (e.g., `Vertesia Microsoft Foundry Inference` or `vertesia-ai`). This is for your identification only. | | **Supported account types** | **Accounts in this organizational directory only (Single tenant)** | | **Redirect URI** | Leave blank — not required for this integration | 4. Click **Register**. ### Step 9: Record the Tenant ID and Client ID After the app registration is created, you will be on the **Overview** page. Record the following values — you will need them for Vertesia: | Field | Also Called | Description | | :---- | :---- | :---- | | **Application (client) ID** | Client ID | The unique identifier for this app registration | | **Directory (tenant) ID** | Tenant ID | The unique identifier for your Azure AD / Entra tenant | These two values combined form the "API key" used in Vertesia: ``` {Tenant ID}:{Client ID} ``` > **Example:** If your Tenant ID is `11111111-aaaa-bbbb-cccc-222222222222` and your Client ID is `33333333-dddd-eeee-ffff-444444444444`, the Vertesia API key would be: `11111111-aaaa-bbbb-cccc-222222222222:33333333-dddd-eeee-ffff-444444444444` --- ## Part 6: Add a Federated Credential A federated credential tells Microsoft Entra to trust tokens issued by Vertesia's Security Token Service (STS). This is what allows Vertesia to authenticate as the app registration without a traditional password or certificate. ### Step 10: Navigate to Certificates and Secrets 1. In your app registration, select **Certificates & secrets** from the left navigation menu. 2. Click the **Federated credentials** tab. 3. Click **\+ Add credential**. ### Step 11: Configure the Federated Credential 1. In the **Federated credential scenario** dropdown, select **Other issuer**. > > **Important:** Select "Other issuer" — not "Managed identity" or any of the pre-configured options (GitHub, Kubernetes, etc.). Vertesia operates as an external OpenID Connect (OIDC) provider. > 2. Fill in the credential fields: | Field | Value | Notes | | :---- | :---- | :---- | | **Issuer** | Vertesia's STS Issuer URL | This is the URL of Vertesia's token endpoint. Obtain this from your Vertesia administrator or the setup tooltip in the Vertesia provider configuration UI. It will be in a format similar to `https://sts.vertesia.io` or equivalent regional STS endpoint like `https://sts.eu1.vertesia.io`. | | **Type** | `Explicit subject identifier` | Select the `Explicit subject identifier` option. | | **Value** | Your Vertesia information | This ties the credential to a specific Vertesia organization/environment. The format is `env:{ORGANIZATION_ID}:{ENVIRONMENT_ID}` to restrict the credential to a single environment, or `env:{ORGANIZATION_ID}` to use the credential in more than one environment within a Vertesia organization. The `ORGANIZATION_ID` is available in the user menu on the right of the top nav bar in Vertesia Studio. The `ENVIRONMENT_ID` is available in the environment settings page. | | **Name** | A descriptive label | Choose a name that identifies the Vertesia environment this credential is for (e.g., `vertesia-production`). **This name cannot be changed after creation.** | | **Audience** | `api://AzureADTokenExchange` | Leave this as the default. | 3. Optionally, add a **Description** for future reference (e.g., "Allows Vertesia production environment to access Microsoft Foundry"). 4. Click **Add** to save the credential. > > **Critical:** The Subject Identifier must exactly match the organization/environment identifier(s) that Vertesia includes in the tokens it sends to Microsoft Entra. If there is a mismatch, token exchange will silently fail. Double-check the value with your Vertesia configuration. > > **Scope:** Each federated credential is tied to the Subject Identifier. If you need to connect multiple Vertesia organizations or environments, then create additional federated credentials on the same app registration. --- ## Part 7: Grant the App Registration Access to Microsoft Foundry The app registration now exists as an identity in Entra, but it does not yet have permission to call Microsoft Foundry APIs. You must grant it the appropriate Azure role on the AI Foundry resource or project. ### Step 12: Assign a Role via IAM 1. Go to the [Azure Portal](https://portal.azure.com). 2. Navigate to your **Microsoft Foundry Resource** (search for the resource name in the search bar, or browse to it via the resource group). 3. In the resource's left navigation menu, select **Access control (IAM)**. 4. Click **\+ Add** \> **Add role assignment**. 5. In the **Role** tab, search for and select **Foundry User**. > > **Role guidance:** > > - **Foundry User** (formerly **Azure AI User**) — Minimum role required for Vertesia to call AI APIs and run inference. Recommended for least-privilege access. > - **Contributor** — Broader access; allows creating and modifying resources within the scope. Use only if administrative capabilities are also needed. > - Do **not** use roles prefixed with "Cognitive Services" for Foundry scenarios. 6. Click **Next** to go to the **Members** tab. 7. For **Assign access to**, select **User, group, or service principal**. 8. Click **\+ Select members**. 9. In the search box, type the name of the app registration you created in Step 8 (e.g., `Vertesia AI Foundry Integration`). 10. Select it from the search results and click **Select**. 11. Click **Review \+ assign**, then **Review \+ assign** again to confirm. > > **Scope note:** Assigning the role at the **resource level** grants Vertesia access to all projects within that resource. Assigning it at the **project level** (navigate to the specific project in the Azure Portal, then to its IAM settings) restricts access to that project only, which is the recommended least-privilege approach. --- ## Part 8: Configure Vertesia With all Azure components in place, you can now configure the Vertesia environment to use Microsoft Foundry as an LLM provider. ### Step 13: Add the Microsoft Foundry Provider in Vertesia 1. Log in to your Vertesia account. 2. Navigate to **Settings** \> **Environments** (or the relevant section for managing AI providers/environments). 3. Select the environment you want to configure, or create a new environment. 4. Choose **Microsoft Foundry** as the AI provider. 5. Fill in the connection details: | Vertesia Field | Value | Where to Find It | | :---- | :---- | :---- | | **Endpoint / Resource Name** | `{resource-name}.services.ai.azure.com` | The name you chose for the Microsoft Foundry Resource in Step 2 | | **Project Name** | The project name from Step 4 | Visible in the AI Foundry Portal, upper-left project selector | | **API Key** | `{Tenant ID}:{Client ID}` | From the app registration Overview page (Step 9\) | > **Endpoint format:** Enter only the resource name portion or the full URL, as instructed by the Vertesia UI. Both `https://{resource-name}.services.ai.azure.com` and `https://{resource-name}.openai.azure.com` are accepted by Azure. > 6. Save the environment configuration. ### Step 14: Verify the Connection 1. Within Vertesia, test the environment connection using the built-in connection test (if available). 2. Alternatively, create a simple AI task or prompt in the environment and confirm that the response comes back from Azure. 3. If the connection fails, refer to the Troubleshooting section below. --- ## Troubleshooting ### Authentication errors / Token exchange failure | Symptom | Likely Cause | Resolution | | :---- | :---- | :---- | | "Unauthorized" or 401 errors | The app registration's service principal does not have an IAM role on the AI Foundry resource | Re-check Step 12; ensure the role assignment was applied and saved correctly | | Token exchange fails silently | The Subject Identifier in the federated credential does not match the environment ID Vertesia sends | Verify the Subject Identifier in the federated credential matches the Vertesia environment ID exactly (case-sensitive) | | Wrong issuer error | The Issuer URL in the federated credential is incorrect | Confirm the issuer URL with your Vertesia administrator and update the credential | ### Resource / Project not found | Symptom | Likely Cause | Resolution | | :---- | :---- | :---- | | "Resource not found" | Incorrect resource name or endpoint URL in Vertesia | Verify the resource name in the Azure Portal and ensure no typos in the Vertesia endpoint field | | "Project not found" | Incorrect project name | Check the project name in the AI Foundry Portal (it is case-sensitive) | ### Model not available | Symptom | Likely Cause | Resolution | | :---- | :---- | :---- | | Model name not recognized | The model was not deployed in the AI Foundry project | Deploy the model in the AI Foundry Portal (Step 5\) | | Quota exceeded | The model deployment's TPM (tokens per minute) limit is too low | Increase the TPM limit in the AI Foundry Portal under Models and Endpoints | ### Permission errors during setup | Symptom | Likely Cause | Resolution | | :---- | :---- | :---- | | Cannot see resource group in portal | Missing permissions on the resource group | Ask your Azure administrator to grant Contributor access at the subscription or resource group level | | Cannot create app registration | Missing Entra permissions | Ask your Azure administrator to grant Application Administrator or Cloud Application Administrator role in Microsoft Entra | --- ## Important Notes and Limitations ### Federated Credentials Per Vertesia Environment Each federated credential on the Entra app registration is tied to a specific Vertesia orgzanization/environment ID in the Subject Identifier field. This means: - One Vertesia environment requires one federated credential. - Multiple credentials can be on the **same** app registration. - There is a limit of **20 federated credentials** per app registration. ### Resource Type Selection - Use **Microsoft Foundry Resource** (AIServices type) for standard foundation models. - **Hub Resources** (the older resource type) are required only for Hugging Face models. Hub resources are considered a more legacy path and new capabilities will primarily be introduced in the standard AI Foundry resource. ### Deployment Names The deployment name you assign to a model in the AI Foundry Portal is the name used in API calls. It is recommended to use the default model name to avoid confusion, but custom names are supported. ### Endpoint URL Formats Microsoft Foundry accepts endpoints in two formats — both are equivalent: - `https://{resource-name}.services.ai.azure.com/` - `https://{resource-name}.openai.azure.com/` ### Name Immutability The **Name** field on a federated credential cannot be changed after creation. If you need to rename it, delete the credential and create a new one (the Issuer and Subject can still be the same). ### Azure RBAC Propagation Delay Role assignments in Azure can take several minutes (up to 10 minutes in some cases) to propagate. If you receive authorization errors immediately after assigning a role, wait a few minutes and try again. --- ## Summary of Values Needed in Vertesia | Vertesia Setting | Where to Find It | | :---- | :---- | | **Endpoint** | `https://{resource-name}.services.ai.azure.com/` — resource name from Step 2 | | **Project Name** | Chosen in Step 4, visible in AI Foundry Portal | | **API Key** | `{Tenant ID}:{Client ID}` from app registration Overview (Step 9\) | And the values needed for the federated credential in Microsoft Entra (Step 11): | Credential Field | Value | | :---- | :---- | | **Issuer** | Vertesia's STS URL | | **Subject Identifier** | Vertesia organization/environment ID from Vertesia Studio | | **Audience** | `api://AzureADTokenExchange` | --- ## References - [Microsoft Foundry Quickstart: Set Up Resources](https://learn.microsoft.com/en-us/azure/foundry/tutorials/quickstart-create-foundry-resources) - [Create a Project in Microsoft Foundry](https://learn.microsoft.com/en-us/azure/foundry/how-to/create-projects) - [Create a Trust Relationship with an External Identity Provider (Federated Credentials)](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-create-trust) - [Role-Based Access Control for Microsoft Foundry](https://learn.microsoft.com/en-us/azure/foundry/concepts/rbac-foundry) - [Endpoints for Microsoft Foundry Models](https://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/endpoints) --- ## Google Cloud Source: https://docs.vertesiahq.com/environments/gcp Markdown: https://docs.vertesiahq.com/llms/environments/gcp.md Vertesia uses Workload Identity Federation to access GCP services, and act as an OIDC Identity Provider for GCP. In practice this means that Vertesia present a Vertesia-generated, cryptographically-signed token (JWT), that is verified by GCP Secure Token Service, and then STS issues in exchange a short lived access token that assign the role and permissions defined in IAM. Thanks to this approach, you can set fine grained permissions on any environment or account in Vertesia. In addition to this documentation page, a helper script which automates all the configuration steps is available [here](https://github.com/vertesia/composableai/tree/main/tools/environment-configuration) ## Create an Identity Pool If you want to add a provider to an existing pool, skip this step and go directly to the next section * Open the [Google Cloud Console](https://console.cloud.google.com/) * Go to Workload Identity Pools * Create a new pool (you can use any name) ![GCP Create Pool](/environments/gcp/gcp_new_pool.png) ## Configure a Provider * Provider: `OpenID Connect (OIDC)` * Name: `vertesia` * Issuer URL: https://sts.vertesia.io * Copy the `default audience url` to later use as the API Key. * Configure the provider attributes to map Vertesia’s token attributes to the Google’s principal attributes: | Google Attributes | OIDC attributes | | ---- | ---- | | google.subject | assertion.sub | | attribute.account | assertion.account.id | | attribute.project | assertion.project.id | | attribute.name| assertion.name | ![GCP Attribute Mapping](/environments/gcp/gcp_attribute_mapping.png) On the pool details page, after the creation, copy the `IAM principal value` to use in the next step: `principal://iam.googleapis.com/projects/PROJECT_ID/locations/global/workloadIdentityPools/POOL_NAME/subject/SUBJECT_ATTRIBUTE_VALUE` ## Grant Permissions You now need to give permissions to your Vertesia identity pool access to VertexAI. * Go to IAM * Click on Grant Access to set a role on a principal * Set the principal to the URL you noted above, adding your Vertesia account ID: `principalSet://iam.googleapis.com/projects/PROJECT_ID/locations/global/workloadIdentityPools/POOL_NAME/attribute.account/ORGANIZATION_ID` * To get the value for ORGANIZATION_ID, Open [Vertesia Studio](https://cloud.vertesia.io/studio/) * You can display your Organization ID by clicking on the user info icon in the top right corner of the Studio UI ![User Info](/environments/aws/vertesia_organization_id.png) * Select **Agent Platform User** and **Service Account Token Creator** as a role, or a custom role if you have created one Other principal formats can be used to make access more restrictive: * Only a specific execution environment: `principalSet://iam.googleapis.com/projects/PROJECT_ID/locations/global/workloadIdentityPools/POOL_NAME/subject/env:ORGANIZATION_ID:ENVIRONMENT_ID` * Only request coming from a specific project: `principalSet://iam.googleapis.com/projects/PROJECT_ID/locations/global/workloadIdentityPools/POOL_NAME/attribute.project/PROJECT_ID` ## Configure a new Vertesia Execution Environment In Vertesia, go to Environments, you can now add a new environment for the provider Google Agent Platform (Vertex AI), and set the following: * API Key: set `the default audience URL` that you have noted in [Configure a Provider](#configure-a-provider) * Endpoint: set it to `REGION_NAME:GOOGLE_PROJECT_NAME`, for example `us-central1:myvertesiaproject` Save, refresh the page. Models should now list, and you can enable models in your environment. --- ## Model Deprecation Source: https://docs.vertesiahq.com/environments/model-deprecation Markdown: https://docs.vertesiahq.com/llms/environments/model-deprecation.md Model deprecation means that certain models may be phased out or become unavailable as providers update their offerings. Check the official deprecation documentation on your platform (VertexAI, Bedrock, etc) for the latest information about supported models and timelines. For major models such as Anthropic it can be prudent to check their deprecation schedules, even if you are using them via third-party platforms. Exact deprecation dates may vary by platform and do not always align with the listed dates. Below are direct links to model deprecation documentation for platform and model providers: ## Model Deprecation Links **Anthropic**: [Anthropic Model Deprecations](https://docs.claude.com/en/docs/about-claude/model-deprecations) **AWS Bedrock**: [Bedrock Model Lifecycle](https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html) **Azure Foundry**: [Azure AI Foundry Model Lifecycle & Retirement](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/model-lifecycle-retirement) **Azure OpenAI**: [Azure OpenAI Model Retirements](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/model-retirements) **Google Vertex AI**: [Vertex AI Model Deprecations](https://cloud.google.com/vertex-ai/generative-ai/docs/deprecations) **Groq Cloud**: [Groq Model Deprecation](https://console.groq.com/docs/deprecations) **HuggingFace Inference Endpoint**: [HuggingFace Inference Endpoints](https://huggingface.co/) (see API docs for endpoint/model status) **IBM WatsonX**: [IBM Foundation Model Lifecycle](https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-model-lifecycle.html?context=wx) **Mistral AI**: [Mistral AI Models](https://docs.mistral.ai/getting-started/models#legacy-models) **OpenAI**: [OpenAI Model Deprecations](https://platform.openai.com/docs/deprecations) **Replicate**: [Replicate Documentation](https://replicate.com/) (model status and deprecation are noted per model) **Together AI**: [Together AI Deprecations](https://docs.together.ai/docs/deprecations) Refer to their official documentation. Staying informed will help you avoid disruptions due to model deprecation and ensure you are using the most up-to-date models for your projects. --- ## OpenAI Source: https://docs.vertesiahq.com/environments/openai Markdown: https://docs.vertesiahq.com/llms/environments/openai.md To use OpenAI with Vertesia, first generate an [API Key in you OpenAI dashboard](https://platform.openai.com/api-keys). Next, go to Environments and configure a new Execution Environment: * Give your environment a name * Set `OpenAI` in Provider * Leave the endpoint URL empty * paste your API key ![Environment Detail](/environments/openai/openai.png) --- ## Errors Source: https://docs.vertesiahq.com/errors Markdown: https://docs.vertesiahq.com/llms/errors.md In this guide, we will talk about what happens when something goes wrong while you work with the API. Let's look at some status codes and error types you might encounter. {{ className: 'lead' }} You can tell if your request was successful by checking the status code when receiving an API response. If a response comes back unsuccessful, you can use the error type and error message to figure out what has gone wrong and do some rudimentary debugging (before contacting support). --- ## Status codes Here is a list of the different categories of status codes returned by the API. Use these to understand if a request was successful. A 2xx status code indicates a successful response. A 4xx status code indicates a client error — this means it's a _you_ problem. A 5xx status code indicates a server error — you won't be seeing these. --- ## Rate limits When you exceed the API rate limit, the API responds with **`429 Too Many Requests`**. The response carries headers describing your current standing, and — on a 429 — *why* you were limited and when to retry. The exact limits depend on your account's tier. The maximum number of requests allowed in the reported window. Requests remaining in the reported window (always `0` on a 429). UTC epoch seconds when the quota window resets. The resource bucket the request counted against (e.g. `genai`, `content`). Only on a 429: `pacing` (short burst — safe to retry shortly) or `quota` (hourly budget exhausted — do **not** auto-retry). Only on a 429: seconds to wait before retrying (always ≥ 1). Only on a `pacing` 429: the exact milliseconds to wait before retrying. Never present on a `quota` 429. The official Vertesia SDKs **automatically retry `pacing` (burst) 429s** for you — sleeping for `x-ratelimit-retry-ms` (or `Retry-After`) and retrying with a bounded number of attempts. They do **not** retry `quota` 429s: an exhausted hourly budget is surfaced to your code so you can back off. Raw HTTP callers should follow the same rule by reading the headers above. --- ## Interactions Execution Errors Whenever an execution is unsuccessful, the API will return an error response with an error type and message. You can use this information to understand better what has gone wrong and how to fix it. Most of the error messages are pretty helpful and actionable. After an execution error occurs, the Run status is set to `failed` and the execution is stopped. The error message is stored in the `error` field of the Run object. ```json {{ title: "Error response" }} { "code": "error", "message": "429 Rate limit reached for gpt-4 in organization on tokens per min. Limit: 10000 / min. Please try again in 6ms. Contact us through our help center at help.openai.com if you continue to have issues." } ``` --- ## Event Subscriptions Source: https://docs.vertesiahq.com/events/subscriptions Markdown: https://docs.vertesiahq.com/llms/events/subscriptions.md Event subscriptions let a project react to platform events. A subscription matches events by category, action, resource type, and an optional JSONLogic condition, then delivers each matching event to a workflow, agent, process, or webhook. Use subscriptions for integrations such as: - notifying an external system when a document is created or updated - starting a workflow, agent, or process when a content object changes - routing content events differently for documents, images, audio, or video - reacting to events pushed in from external systems (Salesforce, GitHub, ...) through an [ingest channel](#inbound-external-event-channels-incoming-webhooks) - building lightweight project automations without polling ## Where to manage subscriptions In Studio, open **Store** and select **Event Bus**. The page has these main views: - **Subscriptions**: create, edit, duplicate, disable, or delete user-managed subscriptions. - **Webhooks**: the subset of subscriptions whose target is an outbound webhook. - **Ingest Channels**: create [inbound channels](#inbound-external-event-channels-incoming-webhooks) that let external systems push events into the bus. - **Deliveries**: inspect matched events and delivery state for workflow, agent, process, and webhook targets. System subscriptions are shown for visibility when relevant, but protected system subscriptions cannot be edited or deleted. Projects that were migrated from legacy customized system rules may show a protected **Override** subscription. That override replaces the Vertesia-provided system subscription for the project instead of running beside it. ## Event model Every event has a stable envelope: ```json { "event_id": "01HF7YAT00ABCDEFGHJKMNPQRS", "event_category": "content", "action": "create", "resource_type": "content_object", "resource_id": "object-id", "account_id": "account-id", "project_id": "project-id", "tenant_id": "tenant-id", "timestamp": "2026-05-23T00:00:00.000Z", "source": "zeno-server", "resource_data": { "metadata": { "type": "document" } } } ``` The main event categories are: | Category | Typical use | | --- | --- | | `content` | content object create, update, revision, delete, type changes | | `workflow` | workflow lifecycle events | | `security` | authentication, authorization, and permission events | | `billing` | inference, embedding, and image generation usage events | | `system` | platform maintenance and system events | | `external` | events pushed in from outside Vertesia through an [ingest channel](#inbound-external-event-channels-incoming-webhooks) | ## API reference The subscription management and delivery inspection APIs are generated from the OpenAPI spec: - `GET /api/v1/events/subscriptions` - `POST /api/v1/events/subscriptions` - `GET /api/v1/events/subscriptions/{subscriptionId}` - `PUT /api/v1/events/subscriptions/{subscriptionId}` - `DELETE /api/v1/events/subscriptions/{subscriptionId}` - `POST /api/v1/events/deliveries/search` Ingest channels (inbound webhooks) are managed under the same topic: - `GET /api/v1/events/channels` - `POST /api/v1/events/channels` - `GET /api/v1/events/channels/{channelId}` - `PUT /api/v1/events/channels/{channelId}` - `DELETE /api/v1/events/channels/{channelId}` The public ingest endpoint external systems POST to — `POST /webhooks/events/{accountId}/{projectId}/{channelId}` — is token/signature authenticated and is **not** part of the authenticated Vertesia API; its request shape is documented in the [ingest channels](#inbound-external-event-channels-incoming-webhooks) section. See [Event Subscriptions in the API reference](/api/openapi#tag/Event-Subscriptions). The outbound webhook request body is not a callable Vertesia API endpoint, so its payload shape is documented in the webhook section below. ## Create a webhook subscription in Studio 1. Open **Store > Event Bus > Subscriptions**. 2. Select **Create subscription**. 3. Enter a name and optional description. 4. Choose `project` scope. Account-scoped subscriptions are reserved for a future account-wide automation model. 5. Set the filter. 6. Set **Target type** to `webhook`. 7. Enter the webhook URL and timeout. 8. Save the subscription. The webhook URL must use `https`. Local development can allow `http` only when the server is explicitly running with local-development SSRF allowances. Private/internal hosts and dangerous ports are blocked. ## Webhook payload For the default `event_envelope` payload mode, Vertesia sends: ```json { "event": { "event_id": "01HF7YAT00ABCDEFGHJKMNPQRS", "event_category": "content", "action": "create", "resource_type": "content_object", "resource_id": "object-id", "account_id": "account-id", "project_id": "project-id", "tenant_id": "tenant-id", "timestamp": "2026-05-23T00:00:00.000Z", "source": "zeno-server", "resource_data": {} }, "delivery": { "id": "delivery-intent-id", "subscription_id": "subscription-id", "attempt": 1 } } ``` Vertesia delivers webhooks through a durable workflow. If the target is temporarily unavailable, the delivery is retried and its status is visible in the **Deliveries** tab. ## Webhook headers and signing User-managed webhook subscriptions support only safe plaintext headers: - `accept` - `content-type` - `user-agent` - `x-request-id` - `x-correlation-id` - `x-vertesia-correlation-id` Credential-bearing headers such as `Authorization`, `x-api-key`, cookies, and auth tokens are rejected. Webhook signing is optional. New webhook subscriptions are unsigned unless signing is enabled. Legacy subscriptions migrated from workflow rules also remain unsigned unless you enable signing later. When signing is enabled, Vertesia creates a per-subscription signing secret and shows it once when the subscription is created or rotated. Store that value in your webhook receiver. Vertesia does not show the existing secret again. Signed webhook requests include these headers: | Header | Value | | --- | --- | | `X-Vertesia-Event-Id` | The platform event ID | | `X-Vertesia-Delivery-Id` | The delivery intent ID, stable across retries | | `X-Vertesia-Timestamp` | Unix timestamp in seconds, generated when the webhook delivery workflow starts | | `X-Vertesia-Signature` | `v1=` | | `X-Vertesia-Event-Category` | Event category, such as `content` | | `X-Vertesia-Event-Action` | Event action, such as `create` | The signature is HMAC-SHA256 over: ```text .. ``` Use the exact raw HTTP request body bytes when verifying the signature. Reject requests with timestamps outside your accepted replay window. Vertesia's shared verifier uses a 15-minute replay window by default. Deduplicate by `X-Vertesia-Delivery-Id` or by the pair `event.event_id` + `delivery.subscription_id`. ## Define filters A subscription filter can match: - `event_category`: array of categories, or `["*"]` - `exclude_event_category`: categories to exclude - `action`: event actions - `resource_type`: resource types - `condition`: a JSONLogic object evaluated against the event envelope - `semantic_condition`: an optional LLM-evaluated natural-language predicate (see below) Example: match created or updated content objects: ```json { "event_category": ["content"], "resource_type": ["content_object"], "action": ["create", "update"] } ``` Example: match only document content objects: ```json { "event_category": ["content"], "resource_type": ["content_object"], "action": ["create", "update", "revision_created"], "condition": { "==": [ { "var": "resource_data.metadata.type" }, "document" ] } } ``` Example: match updates where the source changed: ```json { "event_category": ["content"], "resource_type": ["content_object"], "action": ["update", "revision_created"], "condition": { "==": [ { "var": "details.dirty.source" }, true ] } } ``` ### Semantic conditions (optional LLM filtering) A `semantic_condition` is a natural-language predicate evaluated by an LLM **after** all structural filters (`event_category`, `action`, `resource_type`, `condition`) have already matched. Use it when the routing decision cannot be expressed as a structural rule — for example "the document looks like a signed contract amendment". ```json { "event_category": ["content"], "resource_type": ["content_object"], "action": ["create", "update"], "semantic_condition": { "instruction": "the document appears to be a signed contract amendment", "mode": "enforce", "on_error": "fail_closed", "evaluator": { "type": "interaction", "enrich_with_content": true } } } ``` - **`evaluator.type`**: - `interaction` (default) — a single LLM classification call. Set `enrich_with_content: true` to include an excerpt of the content object's text, and optionally `interaction_ref` to use a stored classifier interaction. - `agent` — a non-interactive agent run that may use tools (fetch documents, inspect processes) to decide. Slower and more expensive; the delivery sits in the `evaluating` state until the agent finishes. - **`mode`**: `enforce` (default) skips delivery on a negative verdict; `shadow` records the verdict on the delivery without ever blocking it — useful for trialing a condition before enforcing it. - **`on_error`**: `fail_closed` (default) does **not** deliver when evaluation errors out; `fail_open` delivers anyway. Verdicts are visible per delivery in the **Deliveries** tab. ## Create a webhook subscription with the CLI Create a JSON file: ```json { "name": "Notify CRM on document changes", "description": "Sends document create/update events to the CRM ingestion endpoint.", "scope": "project", "enabled": true, "priority": "normal", "run_as_role": "automation", "filter": { "event_category": ["content"], "resource_type": ["content_object"], "action": ["create", "update"], "condition": { "==": [ { "var": "resource_data.metadata.type" }, "document" ] } }, "target": { "type": "webhook", "url": "https://example.com/vertesia/events", "signing_mode": "signed", "payload_mode": "event_envelope", "timeout_ms": 30000, "headers": { "content-type": "application/json" } } } ``` Apply it: ```sh vertesia events subscriptions apply --file crm-document-webhook.json ``` Omit `signing_mode` or set it to `legacy_unsigned` for an unsigned webhook. Useful commands: ```sh vertesia events subscriptions list vertesia events subscriptions get vertesia events subscriptions update --file subscription.json vertesia events subscriptions delete ``` ## Create a workflow subscription A workflow target starts a named workflow for each matching event. The workflow receives the event reference and any configured variables. ```json { "name": "Run contract analysis", "scope": "project", "run_as_role": "automation", "filter": { "event_category": ["content"], "resource_type": ["content_object"], "action": ["create"], "condition": { "==": [ { "var": "resource_data.metadata.type" }, "document" ] } }, "target": { "type": "workflow", "endpoint": "AnalyzeContract", "workflow_class": "contract-analysis", "vars": { "analysis_mode": "standard" } } } ``` `run_as_role` is **required** on every subscription. It sets the project service token the delivery runs as, so a subscription always has an explicit run-as identity and never falls back to running as the event's originating user. User-managed subscriptions currently support: - `automation` - `executor` - `reader` ## Create an agent subscription An agent target starts an autonomous agent run for each matching event. The target can reference an interaction by ID, app ref, or system ref. If `interaction_ref` is omitted, Vertesia uses the general-purpose system agent. The target `data` object is passed to the interaction and can contain event templates: ```json { "name": "Review changed policy documents", "scope": "project", "run_as_role": "automation", "filter": { "event_category": ["content"], "resource_type": ["content_object"], "action": ["update"], "condition": { "==": [ { "var": "resource_data.metadata.type" }, "document" ] } }, "target": { "type": "agent", "interaction_ref": "sys:GeneralAgent", "data": { "message": "Review {{ event.resource_id }} and summarize the policy impact.", "event": "$event", "event_ref": "$event_ref", "object_id": "$event.resource_id", "touched_fields": "$event.details.dirty" }, "max_iterations": 12 } } ``` Event subscription agent runs are stored with `source_type: "event_subscription"` and include the event subscription ID and event reference. ## Create a process subscription A process target starts a process run for each matching event. The target can reference a stored process or provide an inline process definition. ```json { "name": "Start renewal process", "scope": "project", "run_as_role": "automation", "filter": { "event_category": ["content"], "resource_type": ["content_object"], "action": ["create"] }, "target": { "type": "process", "process_ref": "contract-renewal", "run_type": "programmatic", "data": { "message": "Start renewal review for {{ event.resource_id }}.", "event_ref": "$event_ref", "object_id": "$event.resource_id" } } } ``` Process target `data` is merged into the process initial context, so the target process definition must accept the fields you pass. ## IDP document events When IDP completes, Vertesia publishes a `document_processed` content event in addition to the generic workflow lifecycle event. It is scoped to the processed content object, so webhook, workflow, agent, and process subscriptions can filter it directly without inspecting a workflow result. The event contains operational aggregates and references only. Extracted data, citations, bounding boxes, prompts, and page images remain in artifact storage. ```json { "event_category": "content", "action": "document_processed", "resource_type": "content_object", "resource_id": "document-id", "meters": [ { "category": "processing", "type": "documents", "quantity": 1 }, { "category": "processing", "type": "pages", "quantity": 75 }, { "category": "processing", "type": "ocr_pages", "quantity": 4 }, { "category": "processing", "type": "vision_pages", "quantity": 6 }, { "category": "extraction", "type": "properties", "quantity": 14 }, { "category": "extraction", "type": "citations", "quantity": 218 }, { "category": "verification", "type": "digitally_verified", "quantity": 211 }, { "category": "verification", "type": "ai_verified", "quantity": 5 }, { "category": "verification", "type": "unverified", "quantity": 2 }, { "category": "review", "type": "issues", "quantity": 1 } ], "details": { "schema_version": 1, "pipeline": "grounded_extraction", "page_count": 75, "ocr_page_count": 4, "vision_page_count": 6, "property_count": 14, "citation_count": 218, "verification": { "total": 218, "digitally_verified": 211, "ai_verified": 5, "unverified": 2 }, "confidence": 0.94, "coverage_min": 0.72, "hardness": 0.32, "escalated": false, "reviewed": true, "review_issue_count": 1, "verdict": "needs_review", "verdict_reason": "Two values could not be verified against the source.", "models_used": [ { "role": "extraction", "run_id": "interaction-run-id", "model": "model-id", "environment_id": "environment-id", "provider": "provider-id" } ], "result_path": "magic-pdf/document-id/grounded-extraction.json", "workflow_id": "grounded-workflow-id", "workflow_run_id": "temporal-run-id", "workflow_type": "PdfGroundedExtractionWorkflow" } } ``` Inference audit events produced by direct extraction and review calls carry the same `details.workflow_run_id`. Join on that field to analyze document outcomes by the actual inference model and token usage without duplicating accounting data in the document event. Large-document review agents are identified by `details.review_agent_run_id` when present. For operational dashboards, `POST /api/v1/audit-trail/aggregate` accepts bounded, typed aggregations over event counts and meters. The server always applies the authenticated account scope and forces the authenticated project for project-scoped principals; client-provided scope never replaces those predicates. For example, the following query returns daily document and page throughput: ```json { "projectId": "project-id", "from": "2026-07-01T00:00:00.000Z", "to": "2026-08-01T00:00:00.000Z", "filter": { "actions": ["document_processed"], "details": [ { "field": "pipeline", "values": ["grounded_extraction"] } ] }, "groupBy": [ { "dimension": "time", "resolution": "day" } ], "metrics": [ { "id": "documents", "operation": "count" }, { "id": "pages", "operation": "sum_meter", "meterCategory": "processing", "meterType": "pages" } ] } ``` The following subscription sends IDP outcomes that need review to a signed webhook: ```json { "name": "Notify on IDP review", "scope": "project", "run_as_role": "automation", "filter": { "event_category": ["content"], "resource_type": ["content_object"], "action": ["document_processed"], "condition": { "==": [ { "var": "details.verdict" }, "needs_review" ] } }, "target": { "type": "webhook", "url": "https://example.com/vertesia/idp-events", "signing_mode": "signed", "payload_mode": "event_envelope", "timeout_ms": 30000 } } ``` The generic `workflow_completed` and `workflow_failed` events are still published for workflow lifecycle automation. IDP workflow lifecycle events also target the content object and identify the workflow in `details.workflow_type`. ## Inbound external event channels (incoming webhooks) Subscriptions deliver events **out** of Vertesia. An **ingest channel** is the reverse: a per-channel public URL that external systems (Salesforce, GitHub, Slack, ...) POST to in order to push events **into** the bus. Each ingested event is published with `event_category: "external"` and `source: "external:"`, then matched by subscriptions exactly like any internal event — so you react to it with a workflow, agent, process, or webhook the same way. > Vertesia has no other generic "incoming webhook". To push data in without a channel, use the REST API directly. ### Create a channel in Studio 1. Open **Store > Event Bus > Ingest Channels**. 2. Select **Create channel**. 3. Enter a name and a `source` label (stamped on events as `external:`, e.g. `salesforce`). 4. Optionally set a default action / resource type, a [transform](#transforms), and [signature verification](#signature-verification). 5. Save. The **ingest token** (and the **signing secret**, if signature verification is enabled) are shown **once** — copy them now; Vertesia never shows them again. The channel's ingest URL is: ```text POST https:///webhooks/events/// ``` ### Authenticating inbound requests A channel authenticates senders one of two ways: - **Ingest token** (default). Pass the token as `Authorization: Bearer `, an `x-vertesia-ingest-token` header, or a `?token=` query parameter (for senders that cannot set headers). - **HMAC signature** (see [below](#signature-verification)). When signature verification is configured the sender signs the request body instead, and the ingest token becomes optional. ### Request body The body is JSON. The full raw body is always preserved under the event's `details.payload`. Top-level fields, when present, set the corresponding event fields directly: ```json { "action": "received", "resource_type": "lead", "resource_id": "00Q5g00000ABCDEments", "idempotency_key": "evt-12345", "timestamp": "2026-05-23T00:00:00.000Z", "payload": { "any": "domain data" }, "details": { "extra": "merged into event.details" } } ``` All fields are optional. When omitted, the channel's `transform` and defaults apply. Sending the same `idempotency_key` twice produces the same `event_id`, so retries are de-duplicated. A successful POST returns `202` with `{ "event_id": "..." }`. ### Transforms Most providers send their own payload shape and cannot wrap it in the envelope above. A **transform** declaratively maps dot-paths in the raw body to event fields (array indices supported, e.g. `commits.0.id`): ```json { "transform": { "action_path": "action", "resource_type_path": "resource", "resource_id_path": "pull_request.id", "idempotency_key_path": "delivery_id", "timestamp_path": "created_at", "static_details": { "provider": "github" } } } ``` Extracted values override the channel defaults; the raw body is still kept under `details.payload`. ### Signature verification For senders that sign their requests (GitHub, Stripe, a Salesforce Apex callout), enable optional HMAC verification on the channel instead of relying on the ingest token. The server recomputes `HMAC(algorithm, signing_secret, rawBody)` and compares it (timing-safe) to the header value after stripping the configured prefix. ```json { "signature": { "header": "x-hub-signature-256", "algorithm": "sha256", "encoding": "hex", "prefix": "sha256=" } } ``` - `header` — request header carrying the signature. - `algorithm` — `sha256` (default) or `sha1`. - `encoding` — `hex` (default) or `base64`. - `prefix` — optional literal stripped from the header value before comparison. The **signing secret** is generated by Vertesia and shown once on create or rotation; configure the sender to HMAC the **raw request body** with it. When signature verification is configured, the ingest token is not required (providers authenticate by signature alone). Rotate the secret by updating the channel with `rotate_signing_secret: true`. ### Create a channel with the API Ingest channels are managed through the REST API under `/api/v1/events/channels` (`POST` to create, `GET` to list, `GET`/`PUT`/`DELETE` by id). Send the channel definition as the request body: ```json { "name": "Salesforce leads", "source": "salesforce", "default_action": "received", "default_resource_type": "lead", "transform": { "resource_id_path": "lead.id" }, "signature": { "header": "x-vertesia-signature", "algorithm": "sha256", "encoding": "hex" } } ``` ```sh curl -X POST "$VERTESIA_API/api/v1/events/channels" \ -H "authorization: Bearer $VERTESIA_TOKEN" \ -H "content-type: application/json" \ --data @salesforce-leads.json ``` Then create a subscription that matches the external events and runs your automation: ```json { "name": "Qualify inbound leads", "scope": "project", "run_as_role": "automation", "filter": { "event_category": ["external"], "resource_type": ["lead"], "action": ["received"] }, "target": { "type": "agent", "interaction_ref": "sys:GeneralAgent" } } ``` ## Event templates Agent and process target data supports event templates: | Template | Result | | --- | --- | | `"$event.resource_id"` | Inserts the referenced value while preserving its original JSON type | | `"{{ event.resource_id }}"` | Inserts the referenced value while preserving its original JSON type | | `"Document {{ event.resource_id }} changed"` | Replaces the template inline as text | | `"$event"` | Passes the complete event object | | `"$event_ref"` | Passes the event reference | | `"$delivery.id"` | Passes the delivery intent ID | If a template references a missing field, the delivery fails because retrying the same event cannot resolve the missing field. ## Delivery behavior Event subscriptions are at-least-once. Delivery can be retried after transient failures, so webhook handlers should be idempotent. Use `event.event_id` and `delivery.subscription_id` as stable identifiers when deduplicating work. For all delivery target types, Vertesia starts the underlying Temporal workflow with a stable workflow identifier based on the event and subscription, so repeated delivery attempts do not intentionally start duplicate work for the same event/subscription pair. ## Troubleshooting Use **Store > Event Bus > Deliveries** to inspect: - whether an event matched the subscription - delivery status: pending, running, succeeded, retrying, failed, or cancelled - workflow IDs and run IDs for started deliveries - recent errors and retry state If no delivery appears: - check that the subscription is enabled - confirm the subscription is in the same project as the event - check `event_category`, `action`, and `resource_type` - test the JSONLogic condition against the event shape - confirm the event source includes the fields referenced by the condition If webhook delivery fails: - confirm the URL is reachable from Vertesia - keep the timeout below 50 seconds - return a successful HTTP status quickly - deduplicate using `event_id` - inspect the delivery error in the **Deliveries** tab --- ## Vertesia MCP Connector Source: https://docs.vertesiahq.com/integrations/mcp Markdown: https://docs.vertesiahq.com/llms/integrations/mcp.md The Vertesia MCP connector gives supported AI clients access to the tools and resources in your Vertesia project. Use it to inspect and manage agents, interactions, prompts, processes, documents, views, project access, event automation, and agent runs. ## Choose your Vertesia region Use the endpoint for the region that hosts your Vertesia account and project: | Region | MCP server URL | | --- | --- | | US1 | `https://mcp.us1.vertesia.io/mcp` | | EU1 | `https://mcp.eu1.vertesia.io/mcp` | You can find your assigned region in Vertesia Studio under **User menu > Environment**. A client configured with one region cannot access an account or project hosted in another region. The marketplace connector currently uses US1. Direct connections from Claude Code, Codex, the ChatGPT desktop app, and other MCP clients can use either regional endpoint. ## Requirements Before connecting, you need: - a Vertesia user identity with access to an account and project in the selected region; - permission to use the resources and operations you intend to invoke; and - an MCP client that supports remote Streamable HTTP servers and browser-based OAuth. The connector can only access resources available to the signed-in Vertesia user. Project permissions and access controls continue to apply to every tool call. ## OAuth client setup Vertesia supports [Client ID Metadata Documents (CIMD)](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/), which lets compatible clients identify themselves without a separately issued client ID or secret. Vertesia does not support OAuth Dynamic Client Registration (DCR). Use CIMD when the client supports it. Current Claude Code discovers CIMD automatically. If a client requires a pre-registered OAuth client instead, create one in Vertesia Studio under **Settings > Applications > OAuth Clients**: 1. Enter a recognizable client name. 2. Add the exact callback or redirect URI supplied by the MCP client. 3. Optionally restrict the OAuth client to specific scopes or a specific Vertesia project. 4. Copy the client ID into the MCP client's advanced OAuth settings. Only create a client secret when the MCP client explicitly requires one. Public clients using PKCE normally do not need a secret. Never paste an OAuth client secret, Vertesia API key, or access token into a chat. Common callback URIs are: | Client | Callback or redirect URI | | --- | --- | | Claude.ai | `https://claude.ai/api/mcp/auth_callback` | | Claude Desktop | `http://127.0.0.1:3334/callback` | | ChatGPT | Use the exact callback URL displayed while creating the plugin | Callback URIs must match exactly, including the scheme, host, port, path, and trailing slash if one is shown. ## Connect from Claude.ai If Vertesia is available in the Connectors Directory, select it and choose **Connect**. For a custom connection: 1. Open **Customize > Connectors**. 2. Select **+ > Add custom connector** and enter the MCP server URL for your Vertesia region. 3. If requested, open **Advanced settings** and enter the client ID for an OAuth client registered with the Claude.ai callback URI shown above. 4. Add the connector, choose **Connect**, and complete Vertesia sign-in in the browser. 5. Select the Vertesia account and project, then review and approve the requested access. Workspace owners may need to configure the connector before other members can connect it. The exact controls depend on the Claude plan and workspace policy. See Anthropic's [custom connector instructions](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp). ## Connect from Claude Code Add Vertesia as a user-scoped remote HTTP server so it is available in every project. Substitute the EU1 endpoint if your Vertesia project is hosted in EU1. ```bash claude mcp add --transport http --scope user vertesia https://mcp.us1.vertesia.io/mcp ``` Then start Claude Code and run `/mcp`, or authenticate directly from the shell: ```bash claude mcp login vertesia ``` Complete the browser sign-in, select an account and project, and approve the requested scopes. Use `claude mcp list` to verify that Vertesia is connected. Claude Code discovers Vertesia's CIMD support automatically, so a registered OAuth client and fixed callback port are not normally required. ## Connect from ChatGPT and Codex ### ChatGPT web Install the published Vertesia plugin when it is available in the Plugins directory. During development or private testing, create a plugin using the MCP server URL for your region and select CIMD in the advanced OAuth settings. If CIMD is unavailable in your ChatGPT workspace, create a Vertesia OAuth client using the exact callback URL shown by ChatGPT, then enter its client ID in the plugin's OAuth settings. After connecting, complete Vertesia sign-in and select the account and project to authorize. Workspace administrators can control whether custom or published plugins are available. ### ChatGPT desktop app, Codex CLI, and Codex IDE extension These clients share the same local MCP configuration. In the ChatGPT desktop app or Codex IDE extension, open **Settings > MCP servers**, add a **Streamable HTTP** server using the regional Vertesia URL, save it, and restart the client. Select **Authenticate** when prompted. To configure the same server from the command line: ```bash codex mcp add vertesia --url https://mcp.us1.vertesia.io/mcp codex mcp login vertesia ``` Use `codex mcp list` or `/mcp` in the Codex terminal interface to verify the connection. For more detail, see the [Codex MCP documentation](https://developers.openai.com/codex/mcp). ## Authentication and authorization Vertesia delegates user authentication to Firebase and the OpenID identity provider configured for your account. The MCP authorization flow does not ask you to create or copy a Vertesia password. The selected account and project determine the connector context. Within that context: - read tools can inspect only data visible to your identity; - write tools require the permissions associated with the requested operation; - destructive operations are identified in tool metadata so the host can request confirmation; and - disconnecting the connector prevents the client from making additional calls with that authorization. Authorization does not grant access to other Vertesia projects or bypass project-level access controls. ### Enterprise SSO configuration If your organization uses an external identity provider, its allowed redirect URIs must include the Firebase auth handler for every Vertesia region your users access: | Region | Identity-provider redirect URI | | --- | --- | | US1 | `https://cloud.us1.vertesia.io/__/auth/handler` | | EU1 | `https://cloud.eu1.vertesia.io/__/auth/handler` | Registering only `https://cloud.vertesia.io` is not sufficient. This configuration belongs in the enterprise identity provider and is separate from the MCP client's OAuth callback URI. ## Capabilities The connector exposes the Vertesia Studio MCP tool catalog except for project-wide configuration updates. Capabilities include: - listing, inspecting, creating, updating, publishing, and deleting prompts and interactions; - running interactions and diagnosing agent execution; - designing, validating, starting, and monitoring processes; - searching, reading, creating, and updating project documents; - managing views, environments, members, roles, and access controls; - configuring event subscriptions and inbound event channels; and - inspecting workflow runs, child runs, traces, and artifacts. The connector also exposes focused guides for multi-step Vertesia workflows. MCP clients may present these guides as skills or as `learn_*` tools. The tools available to the client may evolve as Vertesia adds capabilities, while the permissions of the signed-in user remain authoritative. ## Safety and data handling - Start with read-only requests when exploring a project. - Review the exact target before approving an update, publication, access-control change, or deletion. - Use a non-production project when testing write or destructive workflows. - Do not ask the connector to return secrets, authentication tokens, or data you are not authorized to access. - Treat signed download or upload URLs as temporary credentials: do not paste them into chat or share them with another user. Connector requests are processed in the region of the selected MCP endpoint. For details about data handling, see the [Vertesia Privacy Policy](https://vertesiahq.com/privacy) and [Terms of Service](https://vertesiahq.com/terms). ## Troubleshooting ### Sign-in does not complete Allow pop-ups and redirects for the AI client and Vertesia authentication pages, then retry the connection. Make sure you use an identity that has already been invited to the intended Vertesia account. If a registered OAuth client is in use, verify that its callback URI exactly matches the URI sent by the MCP client. For enterprise SSO, confirm that the identity provider allows the regional `__/auth/handler` URI listed above. ### The expected account, project, or resource is missing Confirm that the MCP server URL uses the region that hosts the account. Then confirm that you selected the correct account and project during authorization. The connector cannot see resources outside that project or resources hidden by its access-control rules. ### The client reports unsupported OAuth registration Use CIMD when the client supports it. If the client only supports pre-registered OAuth clients, create one in Vertesia Studio and provide the exact callback URI and client ID as described above. DCR is not supported. ### A write operation is denied Your Vertesia role may not include the required permission, or the AI client may still require confirmation. Ask a Vertesia project administrator to verify your role; do not share credentials or tokens to work around the denial. ### The connector must be removed Disconnect Vertesia from the AI client's connector or app settings. If access must be revoked centrally, contact your Vertesia administrator. For additional help, contact [support@vertesiahq.com](mailto:support@vertesiahq.com). --- ## Amazon S3 Integration Source: https://docs.vertesiahq.com/integrations/s3 Markdown: https://docs.vertesiahq.com/llms/integrations/s3.md ## Introduction The Amazon S3 integration provides in-place access to binaries stored in S3 as Vertesia content objects. Vertesia uses Workload Identity Federation to access AWS services and act as an OIDC Identity Provider for AWS. In practice this means that Vertesia presents a Vertesia-generated, cryptographically-signed token (JWT), that is verified by AWS Secure Token Service, and then STS issues in exchange a short lived access token that assign the role and permissions defined in the AWS configuration. **This guide describes** the process of configuring AWS to let Vertesia access external S3 buckets from other organizations. ## Configure OIDC Provider Folllow the [Amazon Bedrock documentation](/environments/aws#configure-oidc-provider) to configure Vertesia as an OIDC Provider and then come back to continue with this guide. On the provider you already configured for Bedrock, add a new audience: **blobs**. If you don’t want to configure access to Bedrock then you just need to configure the OIDC provider with the **blobs** audience. ## Create IAM Role for S3 * Go to IAM, select Roles * Create a new Role (e.g. *Vertesia Blobs*) * Select Web Identity as Trusted Entity Type, select the newly created sts.vertesia.io Identity Provider, and **blobs** as audience. * Add the following Condition: * Key: sts.vertesia.io:sub * Condition StringEqual * Value: project:ORG\_ID:PROJECT\_ID You need to replace the values ORG\_ID with your Vertesia Organization ID, and PROJECT\_ID with the Project ID in Vertesia that will access S3. If you want to let all projects from your Vertesia organization access S3, then you can use *\** for the project ID. **Note:** if you use “*\*”* then you should use *StringLike* as operator. **Examples:** * A single project given its ID is allowed: project:\:\ * All projects from the organization are allowed: project:\:\* * Enter a Role Name, and you can review and **Create Role** * Select the new role created, and take note of the Role ARN now displayed. **Example:** arn:aws:iam::\:role/\ ### Role Policy You need to attach a policy to your role. The minimal configuration for a policy to access s3 a bucket is: ``` { "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "sts:AssumeRoleWithWebIdentity", "s3:Get*", "s3:ListBucket" ], "Resource": "arn:aws:s3:::/*" } ] } ``` **Note** that `sts:AssumeRoleWithWebIdentity` is required. Action `s3:ListBucket` is optional, but required for AWS SDK checksums for presigned URLs . ## Configure the Vertesia Project You need now to configure the project you want to use to access blobs in S3 by specifying the role ARN you created at the previous step. Go to **Settings \> Integrations** Enable the AWS integration and paste the role ARN you previously created then click on **Save**. ![][image1] ## Create Content Objects from S3 blobs To create a content object from a S3 blob you can use the operations **POST “https://\/api/v1/objects”** With a payload like the following one: ```javascript { name: "my-photo", content: { source: "s3://vertesia-test/my-photo.jpg#eu-north-1", name: "my-photo.jpg", type: "image/jpeg", }, } ``` You can also add a **type** property as a sibling of the name property if you already know the type of the object to create. If you don’t add a type, then Vertesia will figure out the type to use. **Important Note** In the example above the blob URI is ending with a **\#eu-north-1** hash. This is used to specify the region of the bucket, otherwise if no region is specified S3 will fail. [image1]: --- ## Agent nodes Source: https://docs.vertesiahq.com/processes/agent-nodes Markdown: https://docs.vertesiahq.com/llms/processes/agent-nodes.md An **agent node** delegates an open-ended sub-task to a child conversation workflow that can call tools. The parent process still owns control flow: the agent is a worker, not an orchestrator. This page covers the exact contract between the engine and the agent so you can author agent nodes that actually behave. ## The contract, in one sentence > Given a prompt, a result schema, and a tool set, produce a single JSON object matching the schema. The engine does the rest. The agent does **not** pick the next node via tool calls, does **not** write context via tool calls, and does **not** decide its own termination. It produces a value. ## What the engine provides to the agent When the engine dispatches an agent node, it injects the following into the child conversation: 1. **A system/user prompt** (`sys:ProcessAgentNode` interaction by default) containing: - The process name and description. - The node id, the node's `prompt`, and the node's declared writes. - The current process context (serialized). - The available agent-triggered transitions (if any). - A human-readable description of the result schema. 2. **A derived `result_schema`** — a JSON Schema built from `node.writes` filtered through `process.context.schema.properties`. If the node has more than one agent-triggered transition, the schema also requires a `_next_node` field whose enum is the set of declared targets. 3. **The declared tools** from `node.tools`, plus any `learn_*` skills. Tools declared at node level are added to whatever the default tool set contains. Agents do **not** receive process-control tools such as `set_context`, `transition_to`, `skip_node`, `continue_process`, `retry_node`, or `fail_process` — those belong to the higher-level supervised orchestrator and would be rejected by the result schema anyway. ## What the agent must return A single JSON object that matches the result schema. Llumiverse drivers honour `result_schema` via native structured output when available, and the conversation workflow returns that structured block in its final output. For a node with `writes: ["parties", "total_value"]` and a single agent transition `flag_clauses`, the schema is roughly: ```json { "type": "object", "properties": { "parties": { "type": "string" }, "total_value": { "type": "number" } }, "required": ["parties", "total_value"], "additionalProperties": false } ``` And a valid final response: ```json { "parties": "ACCOR SA and Vertesia SAS", "total_value": 97500 } ``` The engine auto-transitions to `flag_clauses` because the node has exactly one agent-triggered transition. For a node with two agent-triggered transitions, the schema additionally requires `_next_node`: ```json { "type": "object", "properties": { "classification": { "type": "string" }, "_next_node": { "type": "string", "enum": ["human_review", "auto_publish"] } }, "required": ["classification", "_next_node"], "additionalProperties": false } ``` The `_next_node` value picks the transition. ## What happens on the parent side 1. The parent reads the child conversation's final output and looks for a structured block (`type: "json"` from the driver, or a JSON-parseable text block as a fallback). 2. It splits the result into `{ _next_node, ...writes }`. 3. It applies `writes` through the validator — rejecting anything outside `node.writes` and anything that violates the context schema. 4. It picks the transition: - If `_next_node` is present, that target must be a declared agent transition (or the engine errors). - Otherwise, if there's exactly one agent-triggered transition, it's auto-picked. - Otherwise, there's no target and the node fails. 5. It checkpoints context to Mongo and moves on. ## Failure behavior The engine fails the node rather than auto-advancing when: - A schema was declared but the child returned no parseable structured output matching it. - The parsed `_next_node` is not in the declared enum. - The returned writes violate `process.context.schema` or reference fields outside `node.writes`. A failing node leaves the run in `failed` status with the node history entry marked `completed` up to the point of exit (so you can inspect what the child conversation actually did in the **Conversation** tab). ## Tools and skills Declare tools the agent needs in `node.tools`: ```json "extract_terms": { "type": "agent", "tools": ["fetch_document"], "writes": ["parties", "term_length"], ... } ``` Skills (`learn_*`) are available by default so the agent can self-unlock capabilities. If you want a leaner tool set, explicit names without a `+`/`-` prefix replace the default; names prefixed with `+` are added to it, `-` removes. ## Artifact isolation Each agent-node child conversation is launched with `launch_id = "-"`. That maps to a per-node artifact namespace under `agents//workstreams//...` so sibling nodes never overwrite each other's `tools.json`, `conversation.json`, or generated artifacts. ## Authoring tips - Always fill in `human_description` so the observability view can tell a human reader what the node does. - Keep `node.writes` tight — the agent only sees and emits these fields, which both reduces hallucination surface and makes validation strict. - Prefer single-transition nodes; push branching into dedicated `condition` nodes. Multi-transition agent nodes are harder to reason about and require the agent to decide routing. - Reference context fields in `node.prompt` using `{{field}}` — the engine expands inputs against the context before dispatch. Missing fields throw at template time, so only reference fields guaranteed to exist. --- ## Authoring processes Source: https://docs.vertesiahq.com/processes/authoring Markdown: https://docs.vertesiahq.com/llms/processes/authoring.md A process is a JSON document. You can write it by hand, generate it with the **Studio Assistant** (which loads the `learn_process_design` skill), or iterate with an LLM in any editor and paste the result. This page covers both paths and the rules a valid definition must satisfy. ## The editor in Vertesia Studio Studio gives you both a visual authoring surface and a code editor for the same definition: - **Statechart / process board.** The visual view renders the process as cards and transitions. For larger definitions, use lanes and phases so the graph reads like a business process board instead of a raw dependency graph. - **Inspector.** Clicking a node or transition opens the selected definition as YAML, plus readable fields such as type, tool, interaction, writes, phase, lane, and order. - **Explain.** The inspector can call `sys:ExplainProcess` for the selected node, guard, or whole process. Use this before changing an unfamiliar definition. - **Code tab.** The full definition is still editable as JSON/YAML when you need bulk changes, search/replace, or a reviewable diff. - **Live parse.** Save is blocked while the document is unparseable. - **Validation.** The save path runs the same server-side process-definition validation used by the `validate_process` tool. The Studio Assistant calls that tool directly while iterating. The stored definition contract is explicit about its schema generation: native process definitions carry `format_version: 1`. Studio can prefill that field for a new draft, but the saved JSON keeps it present so future migrations have a stable boundary. ## Authoring with the Studio Assistant The fastest way to go from "I want a process that does X" to a committed draft is the **Studio Assistant**. When you ask it to author or change a process, it loads the `learn_process_design` skill — which unlocks the process tools and grammar before it drafts anything: - `learn_process_design` — loads the full process grammar and authoring rules before drafting. - `list_tools`, `list_interactions`, `get_interaction` — confirm every name it references actually exists. - `validate_process` — validates a draft JSON against the schema. The assistant iterates until clean. - `create_process` / `update_process` — write or revise a draft in the catalog. - `publish_process` — publishes a draft only after explicit confirmation. - `start_process_run`, `get_process_run` — run and inspect a draft to test it end to end. - `LayoutProcessDefinition` — improves visual metadata and transition labels without changing runtime behavior. - `ask_user`, `plan`, `update_plan`, `think` — standard authoring scaffolding. Its iteration loop is: 1. Discover — one `ask_user` batch with all open questions. 2. Load the grammar (`learn_process_design`). 3. Confirm tool / interaction names with the user before referencing them. 4. Draft the JSON. 5. Validate; fix every error; repeat until clean. 6. Create or update a draft. Do not set `status` or `version`; the server owns them. 7. Publish only after explicit confirmation. Include an optional publish comment when one is provided. 8. Summarize: node graph, inputs expected per run, outputs written to context, where humans intervene. ### Rules a valid definition must satisfy These are the traps that catch first-time authors. They're codified in the `learn_process_design` skill and in the server-side validator: 1. **`node.input` is a runtime mapping, not a schema.** Use a bare template: `"invoice": "{{invoice_doc_id}}"`. Never put a JSON-schema fragment like `{ "type": "string", "editor": "document" }` inside `node.input` — that belongs on the target interaction's input schema. 2. **Document fields in `context.schema` must set both `format` and `editor`.** Use `{ "type": "string", "format": "document", "editor": "document", "description": "…" }`. Without `editor`, the Start Run modal falls back to a raw `store:` text field. 3. **Agent nodes return structured output, not control-flow tool calls.** Don't prompt worker agents to call process-control tools such as `set_context`, `transition_to`, or `skip_node` — those belong to the top-level supervisor, not node workers. The engine builds a `result_schema` from `node.writes` (plus a `_next_node` enum on multi-transition nodes) and constrains the child's output to match. See [Agent nodes](/processes/agent-nodes). 4. **Agent nodes get only the tools you declare in `node.tools`.** Skills (`learn_*`) are available so the agent can self-unlock capabilities. 5. **Agent nodes use `sys:ProcessAgentNode` by default.** It injects process orientation automatically. Override with `node.interaction` only when you need a custom interaction with its own input schema. 6. **Every node should have a `human_description`.** One or two sentences in plain language, distinct from the developer-facing `description`. Surfaces in observability. Undescribed nodes read as noise in the run report. 7. **Template references throw on missing fields.** Only reference context fields guaranteed to exist at the node. 8. **Every `condition` node must match a branch or have `default: true`.** No match and no default = runtime error. 9. **Context is capped at 64 KB serialized.** Store large payloads as artifact URIs and reference them. 10. **Do not overload collection fanout and fixed split/join in your own mental model.** Use `condition` to choose one path, `branch` to run a fixed set of named branches and join, and `foreach` to repeat a child body over a collection. 11. **Fanout child bodies do not own routing.** In `foreach` and `branch`, the child body should be `tool`, `interaction`, `agent`, or `process` only, with no nested `transitions` or `branches`. 12. **Any node that writes context must declare `writes`.** A non-empty `context_update`, human task answer, interaction result, agent result, collected `foreach` output, or collected `branch` output is rejected unless the parent node declares the target fields in `writes`. 13. **Supervisor overrides must be explicit.** `transition_to` follows declared exits by default, and `skip_node` requires `skippable: true`. Use `metadata.supervisor.allow_transition_override` or `metadata.supervisor.allow_skip` only for deliberate break-glass cases. 14. **Large processes need visual metadata.** Set `metadata.phase`, `metadata.lane`, and `metadata.order` on nodes when the graph has more than a straight-line handful of steps. The run UI uses those values for the Process Navigator and stable layout. 15. **`set_context` tool nodes use an `updates` envelope.** A process tool node that calls `set_context` must pass `{ "updates": { ... } }`, not raw context fields at the top level. The node still declares `writes` for every updated field. 16. **Use real interaction names.** For open-ended agent behavior, prefer `sys:GeneralAgent` or a custom agent interaction. Use `sys:ProcessAgentNode` for normal worker-agent nodes unless you have a specific interaction contract to override. ### Assignees Human-task `assignee` is either a group reference (`group:`) or a concrete user id. `role:` is not supported — use `group:` instead. Leave unset if the task should be available to anyone who can see the inbox. ## Process board metadata The process graph is readable only when authors describe the business structure, not just the transitions. For larger processes, model the visual board deliberately: - **`metadata.phase`** is the stage of work: intake, AI review, approval, fulfillment, closeout. - **`metadata.lane`** is the responsible actor or system: requester, AI assistant, policy, legal, procurement, outcome. - **`metadata.order`** is the stable sort key inside that lane. - **Transition and branch `label`** values are the business names of exits: approved, rejected, needs legal, amount >= 10000. In the swimlane view, lanes render as vertical responsibility bands and nodes stack top-to-bottom within each lane. The main flow usually moves left-to-right across lanes; alternate paths can loop down or back across lanes. Keep lane names short and consistent. Add layout metadata to every node in multi-stage processes: ```json { "agent2_asset_generation": { "type": "agent", "title": "Agent 2 — Campaign Asset Generation", "metadata": { "phase": "Gate 2", "lane": "agent", "order": 10 }, "writes": ["asset_package"], "transitions": [ { "to": "gate2_asset_review", "label": "Send package to review" } ] }, "gate2_asset_review": { "type": "human_task", "title": "Gate 2 — Asset Package Review", "metadata": { "phase": "Gate 2", "lane": "review", "order": 20 } } } ``` Use a small, stable vocabulary for `lane`: business roles (`requester`, `legal`, `finance`, `claims_adjuster`) are better than implementation terms when humans need to read the board. Use implementation lanes (`ai_assistant`, `policy`, `tooling`) when ownership is genuinely a system. Add `label` to transitions and branches when the guard is not obvious. The graph renders those labels on the edge, and the inspector shows the selected transition / branch as YAML. ### Improving layout with the assistant When a graph becomes hard to read, use the Studio Assistant or the **Improve layout** action. It calls `LayoutProcessDefinition` with the current definition and optional notes about the current rendering. The interaction is constrained to visual fields: - node `title`, `description`, and `human_description` - node `metadata.phase`, `metadata.lane`, `metadata.order`, and optional `metadata.position` - transition / branch `label` It should not change runtime behavior: no node ids, node types, context schema, inputs, writes, tools, interactions, guards, targets, defaults, or tasks. ## Hand-authoring workflow If you're editing directly in Studio or in a Git checkout: 1. Start from the schema: define `context.schema` first. Every field a node writes must exist here. 2. Lay out nodes and transitions. Name them for what they do, not how they work (e.g. `legal_review`, not `human_task_1`). 3. Fill each node's `human_description`. Ask yourself: what would a reviewer read here during observability? 4. For `agent` nodes, set the minimum `writes` scope — the result_schema is derived from it, so tighter writes means stricter agent output. 5. Add visual metadata and transition labels before the graph gets large. It is much easier to keep the run view readable while authoring than to repair it later. 6. Validate. Validation catches unreferenced nodes, branches with no default, and schema mismatches. 7. Save the draft. Run it against a known-good input. Iterate. Publish only when it is stable and the publishing user confirms. ## Versioning Process definitions use draft/published versioning: - New definitions are drafts at `version: 1`. - Draft edits mutate the latest draft. - Published revisions are immutable. - Editing a published head creates the next draft head instead of changing the published version. - Listings show only the latest/head revision by default; use the Versions tab or `all_versions` only when you need history. - Publishing requires explicit confirmation and can include a comment. - A historical version can be reverted into the current draft from the Versions tab. Already-running processes continue to execute the definition that was passed into their Temporal workflow at start. Editing or reverting the catalog entry does not change an in-flight run. ## Next - [Node types](/processes/node-types) — the reference for each `type`. - [Observability](/processes/observability) — how to watch a run and debug what an agent node actually produced. --- ## The Process Model Source: https://docs.vertesiahq.com/processes/model Markdown: https://docs.vertesiahq.com/llms/processes/model.md This page is about **how to think** about a process, not how to use the API. If you read only one page in this section before writing your first process, read this one. ## The thesis A business process is a sequence of decisions and actions. Some of those decisions are deterministic ("if value > 50K, route to legal"). Some are probabilistic ("extract the parties from this contract"). Traditional tools force you to pick a side: - A **workflow engine** forces deterministic code. LLM output is a string you parse at your own risk. - A **chat agent** forces probabilistic reasoning. The same input gives different routes depending on mood. The Vertesia process engine is designed to sit at the seam. The engine owns the deterministic parts — transitions, guards, routing, validation. The agents own the probabilistic parts — reading a document, judging risk, drafting output — *bounded by a schema the engine enforces*. This is the whole point. Everything else — `result_schema`, `writes`, `_next_node`, guards, condition nodes — follows from this separation. As the native format evolves, the control-flow vocabulary is settling into three distinct ideas: - `condition` for choosing one path - `branch` for fixed split/join fanout - `foreach` for collection iteration That split matters because BPMN structured parallelism maps to `branch`, while multi-instance fanout maps to `foreach`. Persisted native definitions are also explicitly versioned with `format_version: 1`. That field is part of the process-definition contract, not incidental editor metadata. ## Three axes: control, state, behavior A process definition cleanly separates three concerns: | Concern | Where it lives | Who owns it | | --- | --- | --- | | **Control flow** | `transitions`, `branches`, `guards` | The engine (deterministic) | | **State** | `context` (typed by `context.schema`) | The engine, mutated through `node.writes` | | **Behavior** | Node bodies — prompts, tools, interactions, tasks | Agents, interactions, humans, or deterministic code | | **Presentation** | `metadata.phase`, `metadata.lane`, `metadata.order`, transition labels | Authors, for observability | A node never decides unilaterally. A tool node fills a fixed value. An interaction returns a schema-shaped object. An agent returns a schema-shaped object and optionally picks from declared transitions. A human task writes declared fields and enables guarded transitions. In every case, the engine validates, applies writes, evaluates guards, and picks the next node. If you catch yourself writing a prompt that says "then decide whether to do X or Y" — stop. Lift that decision into a transition guard, and make the node return the signal the guard needs. Presentation metadata is not part of runtime correctness. The workflow can execute without it. It matters because real business processes get wide quickly: approval gates, revision loops, escalation paths, and finalization branches become hard to read as a raw graph. Set `metadata.phase`, `metadata.lane`, and `metadata.order` so the run UI can group the process into navigable stages. Add transition or branch `label` values when a guard deserves a business name. ## Writes scope is a contract `node.writes` isn't a nit; it's *the* contract between a node and the rest of the process. A node that emits a non-empty context update must declare the fields it intends to write. Missing `writes` is treated as "this node writes nothing," not "this node may write anything." For an agent node, the engine: 1. Builds a `result_schema` from `node.writes` intersected with `context.schema.properties`. 2. Hands that schema to the child conversation so the LLM is physically constrained to emit those fields (and only those fields). 3. Validates the returned object against both the schema and the writes list before applying anything. Consequences: - **Agent impact is bounded.** A misbehaving agent can't write `total_value` on a node that only declares `legal_decision`. The engine refuses. - **Routing is trustworthy.** Guards can key off `total_value` without worrying whether some earlier node silently clobbered it. - **Debugging is crisp.** The per-node context diff in the run inspector shows exactly what each node changed. Nothing else. Keep writes as tight as possible. Three named fields is better than a single blob. ## Why the engine owns routing Agents are non-deterministic by design. For a chat, that's a feature. For a business process that decides whether a contract is reviewed by a human, it's a liability. So routing lives in the definition: - **`auto`** transitions with JSON Logic guards — the engine picks based on context. - **`agent`** transitions with a `_next_node` enum in the result schema — the agent picks, but only from the declared set, and only with a value the schema accepts. - **`user`** transitions — a human signal drives the move, through the Task Inbox or the Advance button. - **`condition`** nodes — pure routing, no behavior, required `default: true` fallback. What you *don't* do is let an agent call a `transition_to` tool mid-thought. That tool exists (see below) but only in supervised mode, and only for the top-level orchestrator. ## Durability and human time Because a process runs as a Temporal workflow, every checkpoint is a resumable point. Concretely: - An agent crash retries the node. The run doesn't restart. - A human task can wait days. The workflow parks, the cluster can redeploy, and the signal still fires when the answer arrives. - A worker redeploy mid-run resumes from the last checkpoint. Context is preserved. This is load-bearing for real processes. Contract review, compliance checks, fund operations, content pipelines — all have steps that genuinely take time or block on people. The engine absorbs that naturally. ## Versioning Process definitions are versioned. Each `create_process_definition` or edit bumps `version`. When a run starts, the engine snapshots the current definition into the run (`process_definition_snapshot`). The run walks that snapshot; editing the published definition afterward does **not** affect in-flight runs. This means publishing a revision is safe — no lurking state about which runs will see the new version. New starts see new, old continues old. ## Two execution modes Every run has a `run_type`: **programmatic** or **supervised**. These are the two ways a process can be driven. ### Programmatic (default) The engine walks the definition node by node, applying writes and picking transitions exactly as above. No outer LLM is in the loop. This is what every run starts as unless you explicitly pick supervised. Use programmatic for the 95% of cases where the process flow *is* the logic. Predictable, auditable, cheap. ### Supervised Supervised runs add a top-level `ProcessSupervisor`: a long-lived child conversation workflow that starts with the process and receives structured process events as the run advances. It sees the current node, recent history, available transitions / branches, current context, and any failure metadata. The supervisor can respond with commands: - **`continue_process`** — let the deterministic process keep going. - **`set_context`** — propose a context repair. The process validates the merged context against the process schema and blocks all `_` fields. - **`transition_to`** — move the process through a declared exit from the current node. - **`skip_node`** — treat the current node as skipped and move forward when the node is explicitly skippable. - **`retry_node`** — re-enter the current or requested node. - **`fail_process`** — fail the process with a supervisor-provided reason. The supervisor has process-control tools the worker agents never get: `set_context`, `transition_to`, `skip_node`, `continue_process`, `retry_node`, and `fail_process`. The workflow converts those tool calls into commands and applies them only after validation. The orchestrator is given the same observability a human would have and can steer when things go sideways. This is the path for: - Running a process with a human-in-the-loop chat — "review this step before continuing." - Handling ambiguous inputs where the deterministic flow isn't enough. - Meta-reasoning over many process instances (e.g. batch runs where the orchestrator decides priority). The supervised orchestrator is effectively an overlay. The engine still owns state mutation and routing — `set_context` must satisfy the process context schema, internal `_` fields cannot be written, and `transition_to` must follow a declared transition or branch from the current node. `skip_node` is accepted only when the current node has `skippable: true`. If you need break-glass behavior, opt in explicitly with supervisor policy metadata: ```json { "metadata": { "supervisor": { "allow_transition_override": true, "allow_skip": true } } } ``` The same `metadata.supervisor` policy can also be placed on an individual node. Node-level policy is preferable when only one step needs a controlled override. A run's `config.user_message` carries an optional message from the user to the orchestrator — "prioritize speed over thoroughness on this batch." The worker agents inside nodes are **always** in programmatic posture: constrained by result_schema, bounded by writes. They don't know whether they're running under a supervised or programmatic outer loop. That invariant is intentional — agents at the edge stay simple and auditable regardless of what's driving the overall run. ## When not to reach for a process A process is the wrong tool when: - The whole task is a single LLM call. Use an interaction. - The flow is straight-line and needs no state. Use a workflow. - The user wants an open-ended chat and there's no fixed sequence. Use an agent. - The sequence is long but purely deterministic (ETL, file processing). Use a workflow. Reach for a process when you have **branching**, **state that accumulates**, **human gates**, and at least one step where agentic reasoning pays off. That's the sweet spot. ## Summary — how to think about it 1. **Separate**: control flow, state, behavior. The definition encodes all three explicitly. 2. **Bound agents with writes + result_schema.** The engine only accepts what the schema allows. 3. **Route deterministically.** Guards on context, never on prose. 4. **Treat durability as free.** Let processes wait for humans, retry on failures, span days. 5. **Version confidently.** Snapshots mean publishing is safe. 6. **Pick the mode.** Programmatic by default, supervised when you want an LLM driving the outer loop. The rest of this section — [node types](/processes/node-types), [agent nodes](/processes/agent-nodes), [authoring](/processes/authoring), [observability](/processes/observability), [task inbox](/processes/task-inbox), [tutorial](/processes/tutorial-contract-review) — is mechanics. Come back to this page when something feels off; usually what's off is one of the principles above. --- ## Node types Source: https://docs.vertesiahq.com/processes/node-types Markdown: https://docs.vertesiahq.com/llms/processes/node-types.md Each node in a process definition has a `type`. The engine's behavior for executing the node, applying writes, and picking the next transition depends on the type. Every node also accepts two documentation fields: - **`description`** — developer-facing notes for authors of the definition. - **`human_description`** — a one- or two-sentence plain-language explanation shown to users watching the run. Fill this in — undescribed nodes read badly in observability. ## `tool` A deterministic step. Typically used to apply a fixed `context_update` (e.g. setting `legal_decision: "auto_approved"` when bypassing human review) or to invoke a pre-registered tool. ```json "auto_approve": { "type": "tool", "config": { "context_update": { "legal_decision": "auto_approved" } }, "writes": ["legal_decision"], "transitions": [ { "to": "store_output" } ] } ``` - **Writes**: either the literal `config.context_update`, or derived from `node.writes` if set. Any non-empty context update requires `node.writes`. - **Transitions**: `auto`-triggered by default; guards supported. ## `interaction` Invokes a Vertesia interaction by name. Use for deterministic single-shot LLM calls (summarization, extraction, classification) where the interaction already has a prompt and is registered in the project. ```json "classify_invoice": { "type": "interaction", "interaction": "ClassifyInvoice", "input": { "doc_id": "{{invoice_doc_id}}" }, "writes": ["gl_code", "amount"], "transitions": [ { "to": "approve" } ] } ``` The engine forwards a derived **`result_schema`** built from `node.writes` ∩ `process.context.schema.properties` to the interaction execution, so the returned JSON lines up with the node's writes contract. The interaction's own schema is overridden for the duration of the call. - **Writes**: declared in `node.writes`; populated from the interaction's structured JSON output. - **Transitions**: `auto`-triggered by default. ## `agent` A worker agent that may use tools. The engine builds a `result_schema` from `node.writes` (plus a `_next_node` enum if the node declares more than one agent-triggered transition) and asks the child conversation to return exactly that JSON. The agent can use any tool declared in `node.tools`; `learn_*` skills remain available. ```json "extract_terms": { "type": "agent", "prompt": "Extract key terms from the contract at {{contract_doc_id}}.", "tools": ["fetch_document"], "writes": ["parties", "term_length", "governing_law", "total_value", "auto_renewal"], "transitions": [ { "to": "flag_clauses", "trigger": "agent" } ], "human_description": "Reads the contract and extracts key terms into process context." } ``` See [Agent nodes](/processes/agent-nodes) for the full contract. Key rules: - Agent nodes do **not** get process-control tools such as `set_context`, `transition_to`, `skip_node`, `continue_process`, `retry_node`, or `fail_process`. Those are top-level orchestrator tools; worker agents produce structured output instead. - If a node declares writes but the child conversation returns no parseable schema-matching output, the engine fails the node rather than silently advancing. ## `process` Starts another process workflow and waits for it to complete. Use this for reusable subprocesses such as invoice review, document intake, or vendor onboarding. The child run gets its own process history, artifacts, gates, and human tasks. ```json "review_invoice": { "type": "process", "process": "65f000000000000000000000", "input": { "invoice": "{{invoice}}" }, "returns": { "from": "context.decision" }, "writes": ["invoice_decision"], "transitions": [ { "to": "route_invoice" } ] } ``` Inline definitions are also supported: ```json "review_invoice": { "type": "process", "process_definition": { "format_version": 1, "process": "invoice_review", "initial": "extract", "context": { "schema": { "type": "object", "additionalProperties": true }, "initial": {} }, "nodes": { "extract": { "type": "agent", "writes": ["decision"], "transitions": [ { "to": "done" } ] }, "done": { "type": "final" } } }, "input": { "invoice": "{{invoice}}" }, "writes": ["invoice_review"] } ``` - **`process`** — stored process definition id, built-in `sys:*` process, or installed app process such as `app:contracts:contract_review`. - **`process_version`** — optional published version to pin when `process` references a stored process history. Omit to use the latest head. - **`process_definition`** — inline `ProcessDefinitionBody`; useful for local reusable building blocks. Pair with `process: "tmp:"` only when you need a temporary id for the inline definition. - **`run_type`** — `programmatic` by default; `supervised` is allowed for child processes that need an orchestrator mode. - **`input`** — merged over the child process definition's initial context. - **`returns.from`** — path to read from the completed child state, commonly `context.some_field`. - **`returns.context`** — list of child context fields to return when a single `from` path is not enough. - **Writes**: declared in `node.writes`; populated from the selected child output. ## `script` Runs deterministic Python, JavaScript, or TypeScript in a Daytona sandbox. Script resources are defined once under top-level `resources.scripts` and referenced by name from one or more nodes. The source is discriminated so additional source types, such as artifacts or Git repositories, can be supported without changing the resource or node shape. ```json { "resources": { "scripts": { "score_contract": { "language": "python", "entrypoint": "score.py", "source": { "type": "inline", "files": { "score.py": "import json, os\npayload = json.load(open(os.environ['VERTESIA_PROCESS_INPUT']))\nresult = {'risk_score': len(payload['input']['clauses'])}\njson.dump(result, open(os.environ['VERTESIA_PROCESS_RESULT'], 'w'))" } }, "packages": [] } } }, "nodes": { "score": { "type": "script", "script": "score_contract", "input": { "clauses": "{{extraction.clauses}}" }, "timeout": 300, "writes": ["risk_score"], "transitions": [{ "to": "review" }] } } } ``` The currently supported source is `source: { type: "inline", files: { ... } }`. The script reads `VERTESIA_PROCESS_INPUT`, a JSON file containing `context`, resolved `input`, process/node identifiers, and the attempt number. It must write valid JSON to `VERTESIA_PROCESS_RESULT`. Files written below `VERTESIA_PROCESS_OUT_DIR` are uploaded as run artifacts; `result.json` itself is not uploaded. - Make the top-level keys in `result.json` match `writes`. Other keys are dropped. With exactly one declared write and no matching key, the entire result value is assigned to that write. - JavaScript uses `JSON.parse(fs.readFileSync(process.env.VERTESIA_PROCESS_INPUT, "utf8"))` and writes with `fs.writeFileSync(process.env.VERTESIA_PROCESS_RESULT, JSON.stringify(result))`. TypeScript uses the same contract and runs through the pinned `tsx` runtime. - Additional output files are stored below `out/process///`; the redacted console log is stored alongside them. - The sandbox is reused for script nodes in the same workflow and language. Package installation is cached and serialized. - JavaScript and TypeScript sandboxes include `@vertesia/client`; import it directly without declaring it in `packages`. - User code is never automatically retried. A manual process-node retry gets a fresh invocation directory and attempt number. - `timeout` defaults to 300 seconds and must be between 1 and 600. - The script receives the run's API token and therefore has the run's full API authority. Treat embedded code and dependency changes as privileged code review. - Do not write credentials to output files. Console output is scrubbed and stored as a log artifact, but arbitrary output artifacts are uploaded verbatim. ## `human_task` Pauses the process until a person submits an answer via the Task Inbox. The `task` definition describes what the reviewer sees. ```json "legal_review": { "type": "human_task", "task": { "title": "Legal Review Required: {{parties}}", "description": "Please review and submit your decision.", "assignee": "group:legal", "fields": [ { "name": "legal_decision", "type": "select", "required": true, "options": ["approve", "reject", "request_edits"] }, { "name": "legal_notes", "type": "text", "required": false } ] }, "writes": ["legal_decision", "legal_notes"], "transitions": [ { "to": "store_output", "guard": { "==": [{ "var": "legal_decision" }, "approve"] } }, { "to": "rejected", "guard": { "==": [{ "var": "legal_decision" }, "reject"] } }, { "to": "flag_clauses", "guard": { "==": [{ "var": "legal_decision" }, "request_edits"] } } ] } ``` Details: - `task.title` and `task.description` support `{{var}}` placeholders that are expanded against the process context at task creation time and stored expanded on the task. A new task (new context) will render fresh values. - `assignee` must be either `group:` or a concrete user id. `role:` is not supported. - When the task is submitted, each answer field is copied into context following `node.writes`; then the guarded transitions pick the next node. ## `condition` Pure routing. Evaluates `branches` in order, picks the first whose `when` rule passes, or falls through to the branch marked `default: true`. Every condition node **must** declare either a matching `when` for all possible states or a `default` branch, otherwise runtime fails. ```json "risk_route": { "type": "condition", "branches": [ { "to": "legal_review", "when": { "or": [ { ">": [{ "var": "total_value" }, 50000] }, { "==": [{ "var": "has_critical_flag" }, true] } ] } }, { "to": "auto_approve", "default": true } ] } ``` Guards and branch rules use [JSON Logic](https://jsonlogic.com/) with a Vertesia-specific extension: `artifact_exists: { var: "some_path" }` resolves to `true` if an artifact with that path exists on the run. ## `foreach` Runs a child node body once per item in a context array. The child can be a `tool`, `interaction`, `agent`, `script`, or `process` node. ```json "process_each_line": { "type": "foreach", "foreach": "invoice_lines", "as": "line", "node": { "type": "interaction", "interaction": "ClassifyLine", "input": { "line": "{{line}}" }, "writes": ["gl_code", "amount"] }, "collect": "classified_lines", "failure_policy": "collect_errors", "writes": ["classified_lines"] } ``` - **`foreach`** — context path to an array (max 1000 items). - **`as`** — the variable name each item is exposed as inside the child node's context. - **`item_id`** — optional template for a stable per-item id, e.g. `{{invoice.id}}`. - **`max_concurrency`** — optional positive integer cap. If unset, all items are launched together. - **`collect`** — context key to accumulate per-item results into. - **`failure_policy`** — `fail_fast` (default) or `collect_errors` to keep the survivors and return error info for the failures. For subprocess fanout, use `node.type: "process"`: ```json "review_invoices": { "type": "foreach", "foreach": "invoices", "as": "invoice", "item_id": "{{invoice.id}}", "max_concurrency": 25, "node": { "type": "process", "process": "65f000000000000000000000", "input": { "invoice": "{{invoice}}" }, "returns": { "from": "context.decision" } }, "collect": { "into": "invoice_results", "include": ["status", "index", "item_id", "output", "error", "child_run_id"] }, "failure_policy": "collect_errors", "writes": ["invoice_results"] } ``` String `collect` keeps the compact form. Object `collect` returns an envelope for each item. The common fields are `status`, `index`, `item_id`, `item`, `output`, `context_update`, `error`, `child_run_id`, `child_workflow_id`, and `child_workflow_run_id`. The nested `node` is task-like leaf work only: use `tool`, `interaction`, `agent`, `script`, or `process`, and keep routing on the parent `foreach` node. ## `branch` Use `branch` for structured split/join semantics: - a fixed named set of branches - each branch usually modeled as a child subprocess or inline child process definition - the engine launches all branches - the parent waits for all of them, then continues That is the clean mapping for BPMN structured parallel split/join. It is intentionally different from collection fanout: - `foreach` means "run the same child body once per item" - `branch` means "run these named branches and join" Each branch child is also task-like leaf work only: use `tool`, `interaction`, `agent`, `script`, or `process`, and keep routing on the parent `branch` node. ```json "review_tracks": { "type": "branch", "join": "all", "branches": [ { "id": "legal", "title": "Legal Review", "node": { "type": "process", "process": "legal_review_subprocess" } }, { "id": "finance", "title": "Finance Review", "node": { "type": "process", "process": "finance_review_subprocess" } } ], "collect": { "into": "review_results", "include": ["status", "branch_id", "branch_title", "output", "error"] }, "writes": ["review_results"], "transitions": [{ "to": "consolidate" }] } ``` ## `final` Terminal state. Entering a `final` node completes the process run. ```json "done": { "type": "final" }, "rejected": { "type": "final" } ``` Use multiple final nodes when you want the run's final status to carry meaning (e.g. `done` vs `rejected`), which is visible in the run graph. --- ## Observability Source: https://docs.vertesiahq.com/processes/observability Markdown: https://docs.vertesiahq.com/llms/processes/observability.md A process run is durable. Everything that happened — which node executed, what the context was before and after, what the child agent said, which tools it called — is captured. The run UI surfaces this through three tabs. ## Process tab The default view. Two panes and a strip: ### State chart An interactive graph of the process definition. The renderer uses the visual metadata in the definition when present: - `node.metadata.phase` groups a large process into major stages. - `node.metadata.lane` separates responsibility, such as `agent`, `review`, `routing`, or `finalization`. - `node.metadata.order` gives a stable order inside a phase / lane. If that metadata is absent, the UI infers a readable grouping from node titles and ids. That works for quick drafts, but published definitions should set metadata explicitly so the graph stays stable as the process evolves. The graph has two complementary renderers: - **Diagram** — a compact node-link view for quick topology checks. - **Swimlanes** — a process board that groups nodes by responsibility lane and phase. Use this for business review, demos, and long-running processes with human handoffs. The left **Process Navigator** groups nodes by phase and lets you jump without panning across the full canvas. The graph can render in three visibility modes: - **Main** — the primary path through the process, hiding most loop-limit and escalation detours. - **Nearby** — the selected node, its direct incoming / outgoing neighbors, and their guards. - **All** — the complete process definition. Each node card shows: - Node id (or title, if set). - Node type (`agent`, `human_task`, …) as a pill. - `human_description` if set, otherwise `node.title`. - `tool:` or `interaction:` ref if declared. - The writes scope as small chips. Color rings indicate state: the current node has an info-colored ring, completed nodes a success ring, failed nodes a destructive border, skipped nodes are dimmed. The initial node is always tagged. On a run, the executed route is highlighted separately from the untaken graph. Completed nodes show success state, the current node is emphasized, and the transitions that were actually taken use a stronger primary stroke. This matters on branching processes: the definition may have many exits, but the run should make the chosen path obvious. Transitions and condition branches are selectable too. Guarded exits render as small edge labels. Click a guard label or edge to inspect the YAML definition on the right. ### Node inspector Click a node to inspect it on the right pane: - **Header** — the node's `human_description` in plain text, plus badges and a Retry action (for admins, when the run is active and this is the current node). - **Transitions** — the declared targets with labels / triggers, and for the currently-active node an **Advance** button that emits a manual transition signal (admin-gated). - **Tasks** — any human tasks created for this node, embedded with the response form so admins can answer inline. - **Node History** — every time the node has been entered, with: - Status (`completed`, `running`, `skipped`, `failed`) - Entered / exited timestamps - The **context diff** that was applied on exit, rendered as a scrollable JSON block - **Definition** — `type`, `tool`, `interaction`, `writes`, `skippable`. - **Context** — the full current process context, scrollable. - **YAML** — the selected node, transition, branch, task, input, metadata, or runtime state as copyable YAML. - **Explain** — calls the built-in `sys:ExplainProcess` interaction with the process definition, current context, selected node, and selected guard. Use it to get a concise explanation of what the step does, why it routes the way it does, and what can go wrong. The context diff is what most debugging starts with: for each run of the node, it shows exactly what was written. An empty diff after an agent node running a schema-constrained write usually means the child conversation produced unparseable output (the run will have failed). When a transition is selected, the inspector switches from node details to edge details. This is the fastest way to debug "why did it go there?" questions: inspect the guard YAML, current context, and explanation together. ### Timeline strip Along the bottom: one pill per `node_history` entry in chronological order. Clicking a pill selects that node in the inspector. ## Conversation tab Mounts the same `ModernAgentConversation` you get for standalone agent runs, scoped to the process run id. Every agent node's child conversation lives under that run as its own **workstream** (named after the node id). The conversation view's workstream tabs let you flip between: - the **main** workstream (usually read-only for programmatic runs — the process workflow doesn't do LLM calls itself), - and one workstream per agent node that ran. Each workstream streams live while the node is active, and stays inspectable after completion — you can read the full message history, see every tool call (with inputs and outputs), and copy structured outputs. This is the equivalent of the agent observability view you already use for conversational agents, but rooted at the process run rather than a bare agent run. For **supervised** process runs, the Conversation tab also exposes chat input to the top-level `ProcessSupervisor`. The process workflow posts structured events into that supervisor conversation: run start, node entered, node completed, guard failed, no transition matched, node failed, and human task waiting. The supervisor can answer by calling process-control tools: `set_context`, `transition_to`, `skip_node`, `continue_process`, `retry_node`, or `fail_process`. The process workflow remains the source of truth: it validates context writes, declared transition targets, skip policy, internal `_` fields, and checkpoint sequence before applying anything. The supervisor is useful when the deterministic engine reaches an ambiguous point: a guard fails, no transition matches, a node result needs interpretation, or a human wants a controlled override. In normal programmatic runs there is no top-level supervisor conversation; only child workstreams for agent nodes appear. ## Observability tab The generic **Agent Observability** view, scoped to the process run id. Useful for: - Temporal history (every signal, every activity, every retry). - Conversation snapshots per activity (before / after, downloadable). - Tool-call metrics and context-window charts. Because the process run id is also the agent run id for every child conversation, this tab surfaces the full agent hierarchy: the outer process as the parent, each agent node as a sub-agent. ## Task inbox Not a tab of the run view, but related: the **Task Inbox** in the global nav shows process `human_task` tasks and agent `ask_user` tasks that the current user can read. See [Task Inbox](/processes/task-inbox). ## Typical debugging flow 1. Open the failed run; the State tab usually points at the node that failed. 2. In the Inspector, read the last node-history entry's `context_diff`. Empty diff on an agent node → schema didn't match, jump to the Conversation tab for that workstream. 3. In the Conversation tab, switch to the node's workstream, read the final assistant message. If it's prose, the agent didn't emit JSON. If it's a partial JSON, you can see exactly which required field was missing. 4. If the problem is routing, click the edge or guard label and inspect the guard YAML against the current context. 5. Use **Explain** on the selected node or guard when you need a plain-language summary before changing the definition. 6. Fix `node.writes`, the guard, or the prompt, save a new version, and start a fresh run. --- ## Processes Source: https://docs.vertesiahq.com/processes/overview Markdown: https://docs.vertesiahq.com/llms/processes/overview.md The Vertesia **Process Engine** runs business processes that are a hybrid of deterministic state machines and agentic reasoning. A process defines a graph of **nodes** connected by **transitions**, walked by a Temporal workflow. Some nodes are pure code (conditions, tool calls), some call an interaction, some delegate to an autonomous agent, and some pause for a human to review. The engine is the answer when a plain conversational agent is too loose and a plain DSL workflow is too rigid. You get deterministic routing, durable state, and human-in-the-loop gates — plus agents that can do open-ended sub-tasks bounded by a schema. ## When to use a process vs a workflow or an agent | You want… | Use | | --- | --- | | A single agent chatting and calling tools | [Agent](/agent-runner/overview) | | Deterministic pipelines authored in JSON DSL or TypeScript | [Workflows](/workflows/overview) | | A multi-step business process with branching, gates, retries, and mixed automation + humans | **Processes** (this section) | A process is well-suited when you can describe the flow as "first do X, then either Y or Z depending on the result, then a human reviews, then finalize." One authoring distinction matters in the BPMN-aligned native model: - **`condition`** means choose one path - **`branch`** means run a fixed set of named branches and join - **`foreach`** means repeat one child body over a collection For `foreach` and `branch`, the nested child body is task-like leaf work only: use `tool`, `interaction`, `agent`, or `process`, and keep routing on the parent node. Persisted native definitions also carry `format_version: 1` explicitly so future migrations have a stable schema boundary. ## Core concepts ### Process definition A JSON document that the author commits. Key fields: - **`process`** — stable identifier (e.g. `contract_review`). - **`initial`** — the first node id to enter. - **`context.schema`** — a JSON Schema describing every field the process may read or write. Acts as the typed state the process accumulates. - **`context.initial`** — starting values for that context. - **`nodes`** — a map of `nodeId → NodeDefinition`, each with a `type` and optional `transitions`. ### Context Every running process carries a **context** object, typed by `context.schema`. Any node that writes process state must declare **`writes`** — the exact fields it is allowed to update. The engine rejects non-empty context updates when `writes` is missing and rejects writes outside that list. Context is capped at 64 KB serialized — for large payloads, store artifact URIs. ### Transitions Each node can declare `transitions`, each with: - **`to`** — target node id - **`trigger`** — `"auto"` (engine picks after guards), `"agent"` (chosen by an agent node's structured output), or `"user"` (driven by a human signal) - **`guard`** — optional JSON Logic rule evaluated against context Condition nodes use `branches` instead of transitions and route by `when` rules with a required `default: true` branch. ### Node definition (high level) ```json { "type": "agent", "prompt": "Extract the key terms…", "tools": ["fetch_document"], "writes": ["parties", "term_length", "total_value"], "transitions": [ { "to": "flag_clauses", "trigger": "agent" } ], "human_description": "Reads the contract and writes the extracted key terms into process context." } ``` See [Node types](/processes/node-types) for the full reference. ## Runtime model Each process run is a Temporal workflow (`ExecuteProcessWorkflow`) that: 1. Loads the resolved definition into the workflow input (processes are versioned; runs execute against the definition they started with). 2. Walks node by node, checkpointing to MongoDB every step. 3. For **agent** and **interaction** nodes, spawns a child conversation workflow scoped to a per-node workstream — so every node gets its own conversation view, its own tool use, and its own artifact namespace. 4. Applies writes to context through the validator (schema + writes scope), then picks the next transition. 5. On a **human_task** node, creates a task and waits for a signal from the Task Inbox. Because it's a Temporal workflow, any node failure is durable and retryable; the run can be resumed after restarts. Structured split/join follows the same Temporal-native pattern: start child branches, wait for all of them, then continue. Collection fanout is the separate "repeat over items" case. ## Observability Each run has a dedicated UI with three tabs: - **Process** — the state chart, per-node inspector (writes, transitions, context diff per entry), timeline, tasks. - **Conversation** — the live streaming conversation for whichever node's child workflow is active, with workstream tabs for each node's agent run. - **Observability** — the generic agent observability view (Temporal history, conversation snapshots, tool metrics) scoped to the process run. For end users who need to act on a process, the **Task Inbox** surfaces readable `human_task` nodes assigned to them, their groups, or available to claim. ## Two execution modes Every run has a `run_type`: - **`programmatic`** (default) — the engine walks the definition deterministically. No outer LLM. This is the right choice for the vast majority of production processes. - **`supervised`** — a top-level `ProcessSupervisor` conversation watches the run and can request `set_context`, `transition_to`, `skip_node`, `retry_node`, `continue_process`, or `fail_process` commands. The process workflow validates and applies those commands. `transition_to` must target a declared exit unless an explicit supervisor override policy is set; `skip_node` requires `skippable: true` or an explicit skip policy. Worker agents inside nodes remain schema-constrained regardless. The conceptual picture of why both modes exist and how they relate is in [The Process Model](/processes/model). ## Next - [The Process Model](/processes/model) — how to think about processes (read this before authoring). - [Node types](/processes/node-types) — the reference for each `type`. - [Agent nodes](/processes/agent-nodes) — how a worker agent is constrained by a result schema and the node's declared tools. --- ## Task Inbox Source: https://docs.vertesiahq.com/processes/task-inbox Markdown: https://docs.vertesiahq.com/llms/processes/task-inbox.md Whenever a process hits a `human_task` node, the engine creates a task and waits for an answer. Agents can also create tasks through `ask_user`, which lets a user answer later from the same inbox. Users see those tasks in the **Task Inbox** (`/store/tasks`), a split-pane view with a filterable list on the left and the selected task's detail + response form on the right. ## What shows up in my inbox The inbox filters to tasks that are actually mine to act on. Concretely, a task appears in the UI if: - **it's unassigned** (available to anyone who can see the inbox), or - **`task.assignee` is my user id**, or - **`task.assignee` starts with `group:`** and I'm a member of that group. Tasks assigned to other users or other groups are hidden. The filter runs client-side against the returned task page because the backend `list` endpoint only supports exact-assignee matching. Users with `task:read` or `task:manage` can read broader task lists; users without those permissions only get tasks the API allows them to read. ## Assignees Every `human_task` node's `task.assignee` is one of: - `group:` — owned by a group (most common for shared queues like `group:legal`). - `` — owned by one specific user. - unset — up for grabs. `role:` is **not** supported. Use `group:` for role-like semantics. ## Taking a task When a task is addressed to a group you're in — or unassigned — the detail pane shows a **Take Task** button. Clicking it sets `assignee` to your user id, pinning the task to you so others know it's being worked. After claiming, the button falls away and the task shows your name as the assignee. Note: in the current implementation, claiming a task calls the generic `PUT /api/v1/tasks/:taskId` endpoint which still requires `task:manage` permission. A dedicated self-claim endpoint that allows any eligible viewer to claim is planned; until then, claiming falls through the admin permission check. ## Filters Along the top of the list: - **Open** (default) — pending + in progress, the ones that need attention. - **Pending** — not yet started. - **In progress** — someone's working on it. - **Completed** — already answered. - **Cancelled** — tasks that were cancelled before completion. - **All** — everything returned by the API for the current user. ## Submitting an answer The detail pane renders `task.fields` as a form. Each field's type maps to an input: - `select` → a dropdown with `options`. - `text` → a textarea. - `boolean` → a checkbox. - `number`, `string` → the corresponding input. Submitting a process task sends the answer back to the process workflow as a signal. The process resumes, applies writes per `node.writes`, and evaluates guards on the declared transitions (e.g. route to `store_output` when `legal_decision === "approve"`). Submitting an agent `ask_user` task signals the agent conversation with the answer and resolves the pending ask so the agent can continue. The important operational rule is that process-sourced tasks must resume the workflow, not only mark the task document as completed. Use the Task Inbox or the process run's **answer task** API path so the workflow receives the signal and the state chart advances. ## Opening the parent run **Open Run** in the detail pane jumps to the full process-run observability view so you can see the state chart, read the context, and inspect earlier agent conversations before answering. This is often worth doing for anything more nuanced than a single approve/reject. --- ## Tutorial: Contract Review Source: https://docs.vertesiahq.com/processes/tutorial-contract-review Markdown: https://docs.vertesiahq.com/llms/processes/tutorial-contract-review.md This tutorial builds a realistic process: **contract review**. It accepts a contract document, has an agent extract key terms, has another agent flag risky clauses, routes high-value or high-risk contracts to a legal reviewer via a human task, auto-approves the rest, and finally writes the result back to the store. By the end, you'll have seen every node type working together and picked up the patterns that carry to other processes. ## What we're building ``` start └── extract_terms (agent) └── flag_clauses (agent) └── risk_route (condition) ├─ legal_review (human_task) │ ├─ store_output (agent) ← approve │ ├─ rejected (final) ← reject │ └─ flag_clauses (loop back) ← request_edits └─ auto_approve (tool) └── store_output (agent) └── done (final) ``` Context fields (the state the process accumulates): - `contract_doc_id` — input document reference. - `parties`, `term_length`, `governing_law`, `total_value`, `auto_renewal` — written by `extract_terms`. - `flagged_clauses`, `has_critical_flag` — written by `flag_clauses`. - `legal_decision`, `legal_notes` — written by `legal_review`. - `output_doc_id` — written by `store_output`. ## 1. Start from the schema Every writable field must appear in `context.schema`. Start here. ```json "context": { "schema": { "type": "object", "properties": { "contract_doc_id": { "type": "string", "format": "document", "editor": "document", "description": "Contract document to review." }, "parties": { "type": "string" }, "term_length": { "type": "string" }, "governing_law": { "type": "string" }, "total_value": { "type": "number", "description": "Total contract value in USD." }, "auto_renewal": { "type": "boolean" }, "flagged_clauses": { "type": "array", "items": { "type": "object", "properties": { "clause_type": { "type": "string" }, "excerpt": { "type": "string" }, "risk_level": { "type": "string" }, "reason": { "type": "string" } } } }, "has_critical_flag": { "type": "boolean" }, "legal_decision": { "type": "string" }, "legal_notes": { "type": "string" }, "output_doc_id": { "type": "string" } }, "required": ["contract_doc_id"] }, "initial": { "has_critical_flag": false, "auto_renewal": false, "flagged_clauses": [] } } ``` Two things to call out: - **`contract_doc_id`** uses `format: "document"` **and** `editor: "document"`. Both are required: the first enables validation, the second makes the Start Run modal render a document picker instead of a raw text field. - **`initial`** seeds a few fields so downstream guards don't have to reason about "unset vs false". ## 2. `extract_terms` — the first agent node ```json "extract_terms": { "type": "agent", "human_description": "Reads the contract document and extracts the key terms (parties, term, law, value, auto-renewal) into process context.", "prompt": "You are a contract analyst. Extract the key terms from the contract at {{contract_doc_id}}. Only extract what is explicitly stated; if unknown use 0 for numeric fields and false for booleans.", "tools": ["fetch_document"], "writes": ["parties", "term_length", "governing_law", "total_value", "auto_renewal"], "transitions": [ { "to": "flag_clauses", "trigger": "agent" } ] } ``` What's happening: - The engine builds a `result_schema` from `node.writes` ∩ `context.schema.properties`. Since there's exactly one agent-triggered transition, no `_next_node` is needed — the engine auto-advances to `flag_clauses`. - The agent gets the `fetch_document` tool so it can actually read the contract. Skills (`learn_*`) remain available. - The `{{contract_doc_id}}` placeholder is expanded against context before dispatch. The agent's final response must be JSON matching the schema — something like: ```json { "parties": "ACCOR SA and Vertesia SAS", "term_length": "12 months", "governing_law": "French law", "total_value": 97500, "auto_renewal": false } ``` ## 3. `flag_clauses` — another agent, same pattern ```json "flag_clauses": { "type": "agent", "human_description": "Identifies risky clauses in the contract across four categories and flags critical risks.", "prompt": "You are a legal risk analyst. Fetch the contract at {{contract_doc_id}} and identify risky clauses in these categories: indemnification, unlimited liability, non-standard termination, IP assignment. For each flagged clause return an object with clause_type, excerpt, risk_level (low|medium|critical), and reason. Set has_critical_flag true if any clause is critical.", "tools": ["fetch_document", "search_documents"], "writes": ["flagged_clauses", "has_critical_flag"], "transitions": [ { "to": "risk_route", "trigger": "agent" } ] } ``` ## 4. `risk_route` — a condition node Route high-value or high-risk contracts to a human; everything else auto-approves. ```json "risk_route": { "type": "condition", "human_description": "Routes contracts above $50K or with critical risk flags to legal review; everything else auto-approves.", "branches": [ { "to": "legal_review", "when": { "or": [ { ">": [{ "var": "total_value" }, 50000] }, { "==": [{ "var": "has_critical_flag" }, true] } ] } }, { "to": "auto_approve", "default": true } ] } ``` Condition nodes evaluate `branches` top-to-bottom and require a `default: true` branch as a safety net. ## 5. `legal_review` — a human task ```json "legal_review": { "type": "human_task", "human_description": "Pauses the process for a legal reviewer to approve, reject, or request edits.", "writes": ["legal_decision", "legal_notes"], "task": { "title": "Legal Review Required: {{parties}}", "description": "A contract requires legal review. Extracted terms and flagged clauses are available in the process context. Please review and submit your decision.", "assignee": "group:legal", "fields": [ { "name": "legal_decision", "type": "select", "required": true, "label": "Decision", "options": ["approve", "reject", "request_edits"] }, { "name": "legal_notes", "type": "text", "required": false, "label": "Notes or Edit Instructions" } ] }, "transitions": [ { "to": "store_output", "guard": { "==": [{ "var": "legal_decision" }, "approve"] } }, { "to": "rejected", "guard": { "==": [{ "var": "legal_decision" }, "reject"] } }, { "to": "flag_clauses", "guard": { "==": [{ "var": "legal_decision" }, "request_edits"] } } ] } ``` Highlights: - The `title` uses `{{parties}}` — expanded at task creation time and stored expanded on the task (so reviewers see "Legal Review Required: ACCOR SA and Vertesia SAS"). Re-runs that revisit this node produce a new task with fresh values. - The `assignee` is `group:legal`. Anyone in the `legal` group will see this in their Task Inbox. Users can **Take Task** to claim it to themselves. - The three guarded transitions model a standard review outcome. `request_edits` loops back to `flag_clauses` so the agent can re-analyze with the legal reviewer's notes in context. ## 6. `auto_approve` — a tool node A trivial deterministic step: just writes a fixed value into context and moves on. ```json "auto_approve": { "type": "tool", "human_description": "Marks the contract as auto-approved when it clears the risk route without needing human review.", "config": { "context_update": { "legal_decision": "auto_approved" } }, "writes": ["legal_decision"], "transitions": [ { "to": "store_output" } ] } ``` ## 7. `store_output` — a final agent node Writes a markdown summary back to the content store. ```json "store_output": { "type": "agent", "human_description": "Produces a markdown summary of the review and stores it as a new document.", "prompt": "Create a markdown artifact named contract-review.md containing the contract review results, then persist it with create_document using source: \"artifact:files/contract-review.md\". Use this markdown: \n\n# Contract Review\n\n## Decision\n{{legal_decision}}\n\n## Extracted Terms\n- Parties: {{parties}}\n- Term: {{term_length}}\n- Governing Law: {{governing_law}}\n- Total Value: ${{total_value}}\n- Auto-Renewal: {{auto_renewal}}\n\n## Flagged Clauses\n{{flagged_clauses}}\n\n## Legal Notes\n{{legal_notes}}\n\nReturn the new document id as output_doc_id.", "tools": ["write_artifact", "create_document"], "writes": ["output_doc_id"], "transitions": [ { "to": "done", "trigger": "agent" } ] } ``` ## 8. Terminals ```json "rejected": { "type": "final", "human_description": "The contract was rejected during legal review." }, "done": { "type": "final", "human_description": "The contract has been reviewed and the output document stored." } ``` Multiple `final` nodes are fine — and useful — because the run's ending node tells a human reader how it ended. ## Putting it together The top-level shape of the definition: ```json { "format_version": 1, "process": "contract_review", "description": "Reviews a contract document: extracts key terms, flags risky clauses, routes to human legal review or auto-approves, then stores the final output.", "initial": "extract_terms", "context": { /* schema + initial from step 1 */ }, "nodes": { "extract_terms": { /* step 2 */ }, "flag_clauses": { /* step 3 */ }, "risk_route": { /* step 4 */ }, "legal_review": { /* step 5 */ }, "auto_approve": { /* step 6 */ }, "store_output": { /* step 7 */ }, "rejected": { /* step 8 */ }, "done": { /* step 8 */ } } } ``` ## Running it In Vertesia Studio: 1. **Store** → **Processes** → **New Process** (or ask the Studio Assistant to create it from this spec). 2. Save as `status: "draft"`, then open it and click **Start Run**. 3. The Start Run modal renders `context.schema` as a form, with the document picker for `contract_doc_id`. Pick a contract and choose `programmatic` unless you want a supervisor conversation involved. 4. Watch the run from **Store** → **Process Runs**: the state chart highlights the active node, the Node Inspector shows each `context_diff`, and the Conversation tab streams the two agent conversations live. 5. When `legal_review` fires, the task lands in your inbox (filtered to you and `group:legal`). Submit a decision. The process resumes. 6. `store_output` creates the summary document. `done` terminates the run. ## What to customize - **Swap the extraction/flagging logic** by editing the agent prompts. The result schema is always derived from `writes`, so tightening or loosening the schema is as simple as editing the writes list. - **Change the routing threshold** by editing the `when` in `risk_route`. JSON Logic supports arithmetic, comparisons, array operations. - **Add clause-by-clause analysis** by wrapping a child agent node in a `foreach` step over `flagged_clauses`. - **Add more human gates** — e.g. a finance-approval step for very high-value contracts — as additional `human_task` nodes with `group:finance`. ## See also - [Node types](/processes/node-types) - [Agent nodes](/processes/agent-nodes) - [Observability](/processes/observability) - [Task Inbox](/processes/task-inbox) --- ## Getting Started Source: https://docs.vertesiahq.com/quickstart Markdown: https://docs.vertesiahq.com/llms/quickstart.md This guide will get you set up and ready to use Vertesia. ## Vertesia AI Studio Login to Vertesia AI Studio (https://cloud.vertesia.io). Create an [environment](/environments), connect a provider, and enable AI models. Run [interactions](/studio/interactions) as AI agents and in processes. Use the [Studio Assistant](/studio/assistant) to create, test and run anything within Vertesia AI Studio. Before using the SDK or the CLI to make requests to the platform API, you will need to create a Project and generate an API Key from your Settings (https://cloud.vertesia.io/settings#keys). In order to **access and use your interactions** from outside the Studio application, you can use the Vertesia CLI (https://www.npmjs.com/package/@vertesia/cli). If you want to **integrate your interactions in your own application** you can use the Vertesia SDK (https://www.npmjs.com/package/@vertesia/client). ## Vertesia CLI This is a command line application that can be used to access your Vertesia projects. It was designed to fulfill the following main use cases: * List and switch between your Vertesia projects * List the existing interactions and execution environments * Run interactions once or multiple times over a set of different data inputs * Generate data inputs to run the interactions against * Search through the history of runs to inspect detailed results ### Requirements A TTY terminal and, as for the SDK, Node version 18 or higher is required. ### Installation ```bash npm -g install @vertesia/cli ``` ### Basic Usage First, a profile must be created. A profile correspond to one organization/project on a vertesia environment (preview or prod). Run the following command: ```bash vertesia profiles create ``` and follow the interactive prompts. Once you have entered the profile information, you will be redirected to the authentication page. Upon successful authentication, the following command: ```bash vertesia interactions ``` will now list the interactions using the current profile. Let's run an interaction. We will use the `run` command. ```bash vertesia run {INTERACTION_ID} ``` This command has plenty of options. It is not the scope of this guide to explain them all. To summarize we can run an interaction once or multiple times on a set of data inputs (specified from a file using `--input` or inline using `--data`) we can run a interaction by giving some. When running a single interaction the response will be, by default, streamed on the console. You can also tag runs to be able to easily search for them later using `vertesia runs`. **Example:** ```bash vertesia run --tags testing {INTERACTION_1_ID} vertesia run --tags testing {INTERACTION_2_ID} # then, later retrieve the run results having the testing_session tag vertesia runs --tags testing ``` For more information about the commands use the `help` command or the `-h` flag on a command or check the [CLI documentation](/cli). ## Vertesia SDK This is a JavaScript SDK that can be used in both Node.js and in the browser. ### Requirements Node version 18 or higher is required (the fetch API is required). It will also work with node version 17.5 by using the --experimental-fetch flag ### Installation ```bash npm install @vertesia/client ``` ### Basic Usage Listing the projects in an organization: ```js import {VertesiaClient} from "@vertesia/client" const client = new VertesiaClient({ site: 'api.vertesia.io', apikey: '', }) const projects = await client.projects.list(); for (const project of projects) { console.log(project.name+': '+project.id); } ``` You can see in the previous example how we initialize the `VertesiaClient` with the API key. The API key will authenticate us on the server and will select the organization to which the key belongs. Let's list now the interactions in a project ```js import { VertesiaClient } from "@vertesia/client" const client = new VertesiaClient({ site: 'api.vertesia.io', apikey: '', }) const interactions = await client.interactions.list(); for (const interaction of interactions) { console.log(interaction.name + ': ' + interaction.id); } ``` In the example above, you see we added a `projectId` option when instantiating the client. This is the ID of the project we want to access. Both the API key and the project ID are set when initializing the client and will be used for all the requests made with that client. You can change the project ID at any time by setting it directly on the client: ```js client.project = "New Project ID" ``` Both API key and project ID parameters are required when accessing objects in a project. These parameters are sent, as custom headers, along each request to the server and are used to authenticate the user and to select the project. Let's suppose we created an interaction named "Which Color" which is sending to the LLM an object name to get in response one of its possible colors. Suppose `interactionId` is the ID of the interaction. We can run it as follows: ```js import { VertesiaClient } from "@vertesia/client" const client = new VertesiaClient({ site: 'api.vertesia.io', apikey: '', }) // Note that the interactionId argument must be a valid interaction ID which belongs to the project you are connected to. const run = await client.interactions.execute(interactionId, { data: { object: "sky" } }); console.log(run.result); ``` The response of the LLM will be: ```json { "color": "blue" } ``` ## What's next? Great, you're now set up with an API client and have made your first request to the API. Here are a few links that might be handy as you venture further: * [Starting building with the Studio Assistant](/studio/assistant) * [Go deeper into each endpoint of the API](/api/introduction) * [Go deeper into using the CLI](/cli) --- ## Attribute-Based Access Control Source: https://docs.vertesiahq.com/security/abac Markdown: https://docs.vertesiahq.com/llms/security/abac.md The [role model](/security/overview) grants access to **named people** on **named resources**. Attribute-Based Access Control (ABAC) adds a second way to write rules: by **attributes**. Instead of "Alice can read this collection," you write "anyone in the Finance department can read documents labelled Finance," or "users with clearance 3 or higher can read documents classified Confidential or below." Rules adapt as people and content change: a new hire who joins the Finance group is covered when their attributes match. Principal attributes and matching rules are resolved when an access token is issued, so refresh the user's session or issue a new API-key token before testing a policy change. ## The two kinds of attribute rules ABAC rules are still Access Control Entries, but instead of pointing at a fixed principal or resource they match by conditions: - **Principal Set** — matches **who** by their attributes (a user's department, tags, clearance; whether the caller is a user or an API key). Evaluated when a token is issued: the match decides whether the rule even enters that person's session. - **Resource Set** — matches **which content** by its attributes (a document's sensitivity, its properties, its type). Evaluated when content is queried: the match decides which documents the rule lets through. A rule can combine both: a Principal Set on the "who" side and a Resource Set on the "what" side. That pairing — *these principals* get *this role* on *content matching these conditions* — is the heart of attribute-based and clearance-based access. An **empty** condition list is a wildcard. A Principal Set with no principal conditions matches **everyone** (including API keys); a Resource Set with no resource conditions matches **every** document. Wildcards are powerful — reach for them deliberately. ## Writing conditions A condition is a field, an operator, and a value. Multiple conditions in one rule are combined with AND. | Operator | Meaning | Supported in | |----------|---------|--------------| | `$eq` / `$ne` | equals / does not equal | Principal Sets and Resource Sets | | `$gt` / `$gte` / `$lt` / `$lte` | greater / less than | Principal Sets and root-level numeric Resource Set fields | | `$in` / `$nin` | is / is not one of a list | Principal Sets and Resource Sets | | `$exists` | the field is present (or absent) | Principal Sets and Resource Sets | | `$empty` | the field is empty (or non-empty) | Principal Sets and Resource Sets | | `$like` | case-insensitive wildcard text match, where `*` matches anything | Principal Sets only | Where the field name goes depends on what it is: - **Principal fields** sit at the root of Principal Set conditions: `clearance`, `compartments`, `tags`, `email`, and `kind` (`user` or `apikey`). - **Content classification fields** sit at the root of Resource Set conditions: `sensitivity`, `compartments`, and `tags`. - **Custom attributes** sit under `properties.` — for example `properties.department`, `properties.region`. ### Referring to the person in a content rule Inside a Resource Set, a value may reference the requesting person with the `$principal.` prefix. It is resolved to that person's own value when their token is issued, then matched against each document. ```json { "sensitivity": { "$lte": "$principal.clearance" } } ``` Read this as "only show documents whose sensitivity is at most the viewer's own clearance" — the classic "no read up" rule. `$principal.` works only inside Resource Set (content) conditions; it has no effect on the principal side. ## Two kinds of roles Roles are split into two partitions, and the platform always checks the built-in set first so custom roles can never shadow it: - **System roles** — the built-in roles from the [role model](/security/overview): `owner`, `admin`, `developer`, `reader`, and so on. These govern the platform as a whole. - **ABAC content roles** — a separate, content-only set defined for attribute rules: | Role | Grants | |------|--------| | `content:reader` | read | | `content:writer` | read, write | | `content:manager` | read, write, delete | When you build an attribute rule over content, you use a **content role**, not a system role. This keeps the two worlds cleanly separated: system roles decide what someone can do across the platform; content roles decide, attribute by attribute, which documents they reach. ### Document scope and collection scope A content role applies to a **scope** — either `document` (the default) or `collection`. The same `content:reader` / `content:writer` / `content:manager` roles serve both; the scope you choose on the rule decides whether it governs documents or collections. This distinction matters for restriction, below. ## Classification: clearance, compartments, and sensitivity ABAC has first-class support for a Bell-LaPadula (BLP) style classification model, so you can express clearance-based access without hand-writing conditions. **On people** (users, groups, and API keys): - **Clearance** — a level from 0 to 4. The higher the clearance, the more sensitive the content the person may reach. - **Compartments** — named need-to-know areas (for example `engineering`, `hr`, `legal`). A person only reaches content in compartments they hold. **On content** (documents and collections): - **Sensitivity** — the same 0–4 scale, labelled **Public, Internal, Confidential, Restricted, Secret**. - **Compartments** — the areas a document belongs to. When a person belongs to several groups, their clearance is the **highest** of any group or their own (most permissive wins), and their compartments are the **union** of all of them. A document that belongs to several collections takes the **highest** sensitivity and the **union** of their compartments (most restrictive wins). The two canonical rules, written as Resource Set conditions: ```json { "sensitivity": { "$lte": "$principal.clearance" } } ``` ```json { "compartments": { "$in": "$principal.compartments" } } ``` Together: a person sees a document only when its sensitivity is within their clearance **and** they share at least one of its compartments. For write access you invert the sensitivity comparison (`$gte`) — the "no write down" rule — so classified information cannot be copied into a less-classified document. ### Advanced: unclassified content Documents created before classification was enabled may have no `sensitivity` or `compartments` at all. So a clearance rule does not silently hide them, the platform treats unclassified content as **public**: any rule that references a classification field automatically also allows documents where that field is absent or empty. When you roll classification out, plan to set a baseline sensitivity on existing content, or lean on this fallback during the transition. ## Turning restriction on: the Restrict Mode preset By default a project is **open**: its content is visible to members through the role model, and attribute rules only *add* access. To make attributes the gate — nothing visible unless a rule grants it — you switch the project into restrict mode. The IAM permissions screen has a one-click **Restrict Mode** preset. It creates a single rule that pairs a wildcard Principal Set (named *BLP Restrict Mode*, matching everyone) with a Resource Set (named *Deny All (BLP)*) whose condition matches no real document. The effect: every person now carries a content-security context, which **drops the `project:*` default**. A resource remains available when it directly names that user or one of their groups, or when another ABAC rule grants access. Create and test the real granting rules first, apply Restrict Mode, refresh test tokens, and then expand the rollout. The companion **Open Mode** preset does the reverse — a wildcard rule granting everyone read access to all content — to return a project to open. **Restrict Mode restricts documents, not collections.** The preset is document-scoped, and content lookups never fall back across scopes: a document-level restriction leaves the **collection** listing open. If you also need to lock down which collections people can see, add a **separate** rule with the scope set to **collection** (using the same `content:reader` / `content:writer` / `content:manager` roles). Restricting documents and restricting collections are two independent switches. ### Advanced: how restriction reaches the query Attribute rules are resolved when a token is issued, not when content is queried. Each matched rule contributes its role's verbs to a content-security block in the caller's session, keyed by scope — a document-scope rule contributes bare `read` / `write` / `delete` keys, while a collection-scope rule contributes `collection:read` / `collection:write` / `collection:delete`. At query time the content service reads only the keys for the scope it is filtering and turns each rule's conditions into an OR filter; there is no cross-scope fallback by design, which is exactly why a document restriction does not touch collections. If no key exists for a scope, that scope stays in open mode. Because all of the "who can see what" logic is settled at token time, the query path stays a simple, fast filter. ### Token lifecycle Role assignments, group membership, principal attributes, and matching Principal Sets are captured when the access token is issued. After changing any of them, refresh or restart the user's authenticated session. For an API key, issue a new access token. Long-running agents inherit the effective user's conditions when their agent token is created, so start a fresh run when validating an updated policy. ## Where to go next - [Configure roles and memberships](/security/roles-memberships). - [Set principal attributes](/security/principal-attributes) and [classify content](/security/content-security). - [Create and test dynamic access rules](/security/dynamic-rules), including a rule-local dry run with Studio Assistant before saving the policy. - For application and token-handling guidance, see the [developer guide](/security/developer-guide). --- ## Configure content security Source: https://docs.vertesiahq.com/security/content-security Markdown: https://docs.vertesiahq.com/llms/security/content-security.md Content security supplies the resource-side data used by both direct grants and dynamic access rules. Administrators can classify documents and collections, restrict a collection to named principals, and propagate collection policy to member documents. ## Classification fields Vertesia provides two first-class fields for Bell-LaPadula-style policies: | Field | Principal side | Resource side | Combination behavior | | --- | --- | --- | --- | | Security level | `clearance` | `sensitivity` | A typical rule permits `sensitivity <= clearance`. | | Security domains | `compartments` | `compartments` | A typical rule requires an overlapping compartment. | The default level labels are: | Value | Label | | --- | --- | | 0 | Public | | 1 | Internal | | 2 | Confidential | | 3 | Restricted | | 4 | Secret | These labels make policies easier to discuss, while the stored values remain numeric. ## Classify a document 1. Open the content object. 2. Select **Metadata**. 3. Find **Security Classification**. 4. Select **Edit**. 5. Set the sensitivity and compartments. 6. Save the object. Documents can also carry custom properties used by Resource Set rules, such as `properties.department` or `properties.customer_id`. ## Classify a collection 1. Open the collection. 2. Select **Metadata**. 3. Set the sensitivity and compartments. 4. Save the collection. Collection classification is propagated to member documents: - document sensitivity becomes the highest sensitivity contributed by its collections; - document compartments become the union of compartments contributed by its collections. When a document belongs to multiple collections, the most restrictive contributed sensitivity and the full set of contributed compartments apply. ## Manage direct collection grants Use direct grants when a collection should be accessible to named users or groups independently of its attributes. 1. Open a non-dynamic collection. 2. Select **Security**. 3. Select **New**. 4. Choose a user or group. 5. Choose **Read**, **Write**, or **Delete**. Permission levels are cumulative. 6. Save the grant. You can also add `project:*` from the collection actions. `project:*` means any project member whose system role grants the corresponding content permission. Direct grants are stored on the collection and propagated to member documents and their revisions. For an object in multiple secured collections, grants are additive: access granted by any parent collection remains effective. Removing one collection's grant does not remove access supplied by another collection or by a matching ABAC rule. Review all applicable sources when investigating unexpected visibility. ## Unclassified resources Resources without sensitivity or compartments are treated as unclassified. The current dynamic content policy keeps unclassified resources visible when a rule uses the corresponding classification field. Classify sensitive content before relying on a clearance rule to hide it. For a default-deny rollout, inventory and classify existing content first, then test the restrict-mode policy with representative users and API keys. ## Administrative bypass capabilities - The `content:read_all` permission bypasses content security for read operations only. - The `content_superadmin` role carries `content:superadmin`, which bypasses content security for read, write, and delete operations. These capabilities are intentionally broad. Use them for trusted audit, migration, or content-processing identities, not as a substitute for a normal access policy. ## Read and mutation behavior When read access is denied, Vertesia generally returns **404 Not Found** so the response does not reveal whether the resource exists. A denied write or delete generally returns **403 Forbidden** after the resource has been identified. List, search, facet, and count operations apply the same visibility policy and omit inaccessible resources. ## Operational checklist - Use stable classification and compartment vocabularies. - Prefer group grants over many individual grants. - Verify propagation after changing a large collection. - Test every applicable scope with a newly issued token. - Include direct grants, dynamic rules, and bypass roles in access reviews. - Keep at least one controlled administrative recovery path before enabling broad restrict mode. Continue with [Dynamic access rules](/security/dynamic-rules) or the [Developer guide](/security/developer-guide). --- ## Access control for developers Source: https://docs.vertesiahq.com/security/developer-guide Markdown: https://docs.vertesiahq.com/llms/security/developer-guide.md Vertesia APIs enforce roles, permissions, direct resource grants, and dynamic content rules. Applications should use those APIs as the authorization boundary and use client-side checks only to improve the user experience. ## Do not duplicate authorization in the browser Client-side permission checks are useful for hiding unavailable actions or routes, but they are not a security boundary. A user can modify browser code or call an API directly. Use this pattern: 1. Let Vertesia APIs enforce every protected read and mutation. 2. Use the current principal's system permissions to adapt the interface. 3. Treat a 403 or 404 from the API as authoritative. 4. Never fetch a broad dataset and filter sensitive records only in the browser. ABAC conditions are already resolved and enforced by content APIs. Applications normally should not decode and reimplement the condition language. ## Retrieve the role catalog The client exposes both the complete role catalog and the system-role subset: ```ts const roles = await client.iam.roles.list(); const systemRoles = await client.iam.roles.listSystem(); ``` Use `listSystem()` when building permission-aware controls because its permissions use the platform permission vocabulary. The complete catalog also includes content roles used by dynamic Resource Set rules. Role definitions are built in and can change as Vertesia adds capabilities. Retrieve the catalog instead of copying a role-to-permission table into application code. ## Inspect project access-control entries Administrative applications can list entries for the active project: ```ts const entries = await client.iam.aces.listProjectAces(); ``` An entry identifies its principal, principal type, role, target resource, resource type, and optional dynamic conditions. Treat these entries as administrative policy data. Do not expose them to users who lack permission to inspect account access. Creating, updating, and deleting entries requires elevated IAM permissions. Prefer the Studio configuration pages for human administration unless the application is explicitly an access-management application. ## Work with content APIs Content reads, searches, counts, and facets automatically apply the current token's visibility policy. A list result contains only resources the principal may read. For mutations: - a system role must grant the operation, such as `content:write`; - the resource must also pass its direct or dynamic content policy; - a collection may propagate its security and classification to member objects. Do not assume that being able to read a document implies permission to update or delete it. Dynamic roles may grant different operation sets. ## Token lifecycle Roles, group memberships, principal attributes, and dynamic rules are resolved when a token is issued. After an administrator changes policy, obtain a new token before testing. For user sessions, refresh or restart the authenticated session. For API keys, issue a new access token using the updated key context. Long-running agents inherit the effective user's content conditions when their agent token is created. Start a fresh run when validating changed access policy. ## Error handling Handle authorization outcomes without leaking resource details: - **401 Unauthorized** — the token is missing, invalid, or expired. Reauthenticate. - **403 Forbidden** — the principal is authenticated but cannot perform the mutation or administrative action. - **404 Not Found** — the resource may not exist or may be hidden by its read policy. Do not reveal which case applies. Search and list APIs usually omit inaccessible records rather than returning one error per hidden record. ## Test matrix Before releasing a permission-aware application, test: | Case | Expected result | | --- | --- | | Matching role and matching resource rule | Operation succeeds. | | Missing system permission | Operation is rejected even if resource attributes match. | | Matching system permission but non-matching ABAC rule | Resource is omitted or access is rejected. | | Direct user or group grant | Resource remains accessible in restrict mode. | | Stale token after an administrator changes policy | Old policy may remain until a new token is issued. | | Document rule without collection rule | Documents may be accessible while their collection is hidden. | | API key with no user/group attributes | Only the key's own context participates. | ## Related guides - [Access control overview](/security/overview) - [Configure roles and memberships](/security/roles-memberships) - [Configure dynamic access rules](/security/dynamic-rules) - [Configure content security](/security/content-security) --- ## Configure dynamic access rules Source: https://docs.vertesiahq.com/security/dynamic-rules Markdown: https://docs.vertesiahq.com/llms/security/dynamic-rules.md Dynamic access rules grant content access based on principal and resource attributes. They are useful when identity lists would be too large or when the same policy should follow organizational properties such as department, clearance, region, or customer assignment. ## Before you start - Configure the required [principal attributes](/security/principal-attributes). - Configure resource fields as described in [Content security](/security/content-security). - Decide whether the rule protects **documents**, **collections**, or both. - Use an organization administrator account to create or modify access-control entries. Dynamic rules grant access; they do not express explicit denies. Separate rules are additive, so a broad wildcard rule can make a narrower rule ineffective as a restriction. ## Create a rule in Studio 1. Open **Project Settings → Permissions**. 2. Select **New**. 3. Under **Who**, choose one of: - **User / Group / API Key** for a fixed principal. - **Principal Set** to match principals by attributes. 4. If using a Principal Set, enter a descriptive name and its conditions. 5. Choose the content role: - `content:reader` — read matching resources. - `content:writer` — read and write matching resources. - `content:manager` — read, write, and delete matching resources. 6. Under **Target Resource**, choose **Resource Set**. 7. Select **Document** or **Collection** scope. 8. Enter a descriptive Resource Set name and its conditions. 9. Create the permission, refresh the test principal's token, and verify the result. ## Example: clearance-based document access This rule allows every principal to read documents at or below the principal's clearance: | Setting | Value | | --- | --- | | Principal | Principal Set named `All principals` | | Principal conditions | `{}` | | Role | `content:reader` | | Scope | `document` | | Resource Set | `Documents within clearance` | | Resource conditions | `{ "sensitivity": { "$lte": "$principal.clearance" } }` | Add a compartment requirement to require both conditions: ```json { "sensitivity": { "$lte": "$principal.clearance" }, "compartments": { "$in": "$principal.compartments" } } ``` Conditions within this object use AND semantics: both sensitivity and compartment checks must match. ## Example: department isolation Match Legal users to Legal documents: **Principal conditions** ```json { "properties.department": { "$eq": "legal" } } ``` **Resource conditions** ```json { "properties.department": { "$eq": "legal" } } ``` For policies that compare the resource to each principal dynamically, use a principal reference: ```json { "properties.department": { "$eq": "$principal.properties.department" } } ``` ## Restrict and open presets The **Restrict Mode** preset creates a wildcard Principal Set and a deliberately non-matching Resource Set. Every matching principal receives an ABAC condition, which removes the blanket `project:*` grant for document reads; real granting rules then add back the intended access. Safe rollout order: 1. Create and test the real granting rules. 2. Apply **Restrict Mode** to the intended principal population. 3. Refresh test tokens and verify allowed and denied cases. 4. Expand the rollout only after the test policy behaves correctly. The **Open Mode** preset creates a wildcard principal and wildcard resource rule. It grants read access to all documents through the dynamic policy system. Because grants are additive, leave this rule in place only when broad read access is intentional. An empty Principal Set matches users and API keys. To target only one kind, add `kind = user` or `kind = apikey`. ## Document and collection scope Scopes are isolated: - A document rule controls content objects and document searches. - A collection rule controls collection lists and individual collections. If users must see a collection and its documents under the same policy, create one rule in each scope. The roles may be the same, but each rule has its own scope. ## Condition operators | Operator | Meaning | Example | | --- | --- | --- | | `$eq`, `$ne` | Equality or inequality | `{ "properties.region": { "$eq": "eu" } }` | | `$gt`, `$gte`, `$lt`, `$lte` | Numeric comparison | `{ "sensitivity": { "$lte": 2 } }` | | `$in`, `$nin` | Membership comparison | `{ "compartments": { "$in": ["legal"] } }` | | `$exists` | Field presence | `{ "properties.customer": { "$exists": true } }` | | `$empty` | Missing, null, empty string, or empty array | `{ "compartments": { "$empty": true } }` | | `$like` | Case-insensitive `*` wildcard matching | `{ "email": { "$like": "*@example.com" } }` | `$like` is for Principal Set evaluation. Avoid numeric range conditions on custom `properties.*` fields used in search; use root-level numeric fields such as `sensitivity` instead. ## Test a proposed rule with Studio Assistant Studio Assistant can dry-run one Resource Set rule before you save it. Provide: - the document or collection scope and requested operation; - the proposed content role and principal/resource conditions; - a representative principal context; - sample objects with the fields used by the rule. For example, ask: “Test whether a user with clearance 3 and the `finance` compartment can read these three sample documents through this proposed rule.” The assistant reports every check for every sample and shows the resolved `$principal.*` values. The test is deliberately rule-local. It does not include other access-control entries, direct document or collection grants, project defaults, or administrator bypasses. Use it to validate rule logic, then test the saved policy with a fresh real token before rollout. ## Verify a rule Test at least these cases after issuing fresh tokens: - a principal that should match the Principal Set; - a principal that should not match; - a classified resource that should match; - a classified resource that should not match; - an unclassified resource; - a direct user or group grant, if direct grants coexist with ABAC; - both document and collection navigation when both scopes are used. If a principal unexpectedly sees everything, look for a wildcard Resource Set or another broad rule. If a principal sees nothing, verify the Principal Set, scope, content role, resource paths, and refreshed token. --- ## Security & Access Control Source: https://docs.vertesiahq.com/security/overview Markdown: https://docs.vertesiahq.com/llms/security/overview.md Access in Vertesia is decided by **roles**. You grant a person (or a group, or an API key) a role on a project or on your whole organization, and that role decides what they can do — read content, run interactions, build workflows, manage settings, and so on. This page covers the standard model an administrator uses day to day: roles, how you grant them, and how access to content flows from collections down to the documents inside them. For rules based on *attributes* — clearance levels, departments, document sensitivity — see [Attribute-Based Access Control](/security/abac). ## Roles A role is a named bundle of permissions. You never assign individual permissions by hand; you pick a role, and the role carries the right set. The most common roles an administrator assigns: | Role | What it grants | |------|----------------| | **Owner** | Full control, including account settings and billing. | | **Admin** | Full control of the project or organization. | | **Manager** | Manage everything except account settings and billing. | | **Developer** | Build and manage interactions, content, and workflows. No account, billing, or role administration. | | **Executor** | Read and run interactions only. | | **Reader** | Read-only access — browse content and configuration, change nothing. | | **Consumer** | For end users of a micro-application built on Vertesia. | | **Member** | Baseline membership. Marks that someone belongs to the project; grants no specific actions on its own. | | **Billing** | Manage billing only. | | **Auditor** | Read everything except administration, with no ability to make changes. | A few additional roles exist for machine identities and system tasks — `application` and `automation` (for apps and event-triggered runs), `content_processor` (trusted content processing), and `content_superadmin` (see all content in a project). You rarely assign these by hand. Roles apply at the level where you grant them. A role granted on the **organization** (account) applies across every project; a role granted on a single **project** applies only there. ## Granting access Access is stored as **Access Control Entries** (ACEs). Each entry is simply: *this principal* has *this role* on *this resource*. - **Principal** — a user, a group, or an API key. - **Resource** — a project, the organization, an environment, or an interaction. - **Role** — one of the roles above. In Studio, go to the **Settings**. From there you can add a user or group to a project and choose their role, manage groups, and review or revoke existing grants. **Groups** make this easier at scale: grant a role to a group once, and every member of that group receives it. A person's effective permissions are the combination of every role they hold — directly and through their groups. ### Advanced: how a permission check resolves When someone makes a request, the platform collects all the roles they hold (from direct grants and group memberships, at both the account and project level), unions the permission sets of those roles, and checks the required permission against that union. Permissions are always computed fresh from the roles — there is no separate stored permission list to keep in sync. Endpoints declare the permission they require (for example `content:read`, `interaction:execute`, `project:admin`), and the request is allowed only if one of the caller's roles includes it. ## Content visibility: projects, collections, and documents Content permissions (`content:read`, `content:write`, `content:delete`) come from the roles above, but there is a second question: *which* documents does a grant apply to? **By default, a project's content is open to its members.** Every document and every collection is created "open to the project" — anyone whose role includes content read can see all of the project's content, and anyone whose role includes content write or delete can change it. Collections at this stage are for **organizing** content and for propagating shared metadata; they do not, by themselves, narrow who can see a document. You restrict content by naming **specific** users, groups, or roles on a collection or document instead of leaving it open to the whole project. The moment you do, that item becomes restricted — only the named principals keep access. And because documents belong to collections, a restriction placed on a collection **flows down to the documents inside it**. ### How access flows from collections to documents When a document belongs to one or more collections, its effective access is worked out from those collections, one permission at a time (read, write, and delete are resolved independently): - **Open means open to everyone in the project.** As long as an item is left open, project members with the matching content permission can access it. - **A specific grant restricts.** Naming specific users, groups, or roles turns "open to the project" into "only these principals." Restriction anywhere wins over "open" — you cannot accidentally re-open a restricted document by also leaving it open somewhere else. - **The closest collection wins.** A document takes its access from the *nearest* collection that says something specific about that permission. A rule on the collection the document sits in directly overrides a rule on a parent collection above it. - **Siblings combine.** If a document belongs to several collections at the same level that each name specific principals, those principals are added together. If nothing anywhere restricts a given permission, the document stays open to the project. Restriction is always something you add deliberately; the safe default is never to silently lock content away. ### Advanced: the cascade in detail The engine groups a document's collections by **distance** — distance 0 is a collection the document is a direct member of, distance 1 is a parent, distance 2 a grandparent, and so on. For each permission it starts at distance 0, drops any "open to the project" entries, and unions the remaining specific principals. If that distance produced any specific principals, they win and the search stops for that permission; if it produced none, the engine moves out to the next distance. If no distance anywhere names a specific principal, the permission falls back to "open to the project." This is why the closest collection wins, siblings at the same distance combine, and a single restriction anywhere is enough to lock a document down. ## How the access layers combine Vertesia evaluates content access in three layers: 1. **RBAC permission** — the principal needs a system role that permits the requested operation. 2. **Direct resource grants** — a document or collection can name users or groups for read, write, and delete. 3. **ABAC rules** — a Principal Set and Resource Set can grant a content role when their attributes match. If a principal has an ABAC condition for an operation and scope, that operation enters **restrict mode** for that principal and scope. The usual `project:*` grant no longer provides blanket access. A document or collection is available when it directly names the principal or one of their groups, **or** when at least one matching ABAC rule grants the operation. Direct grants and ABAC rules are additive; there is no deny rule that overrides another grant. Document and collection scopes are independent. A document rule does not restrict collection listings, and a collection rule does not restrict the documents inside it. Configure both scopes when the policy must protect both. For attribute-based policies such as department isolation or clearance versus sensitivity, continue to [Attribute-Based Access Control](/security/abac). ## Configure access control - [Roles and memberships](/security/roles-memberships) — assign built-in roles to users, groups, and API keys. - [Principal attributes](/security/principal-attributes) — configure the values used by Principal Sets. - [Dynamic access rules](/security/dynamic-rules) — create and test document and collection ABAC rules. - [Content security](/security/content-security) — classify resources and manage direct grants. - [Developer guide](/security/developer-guide) — build applications that respect Vertesia authorization. --- ## Configure principal attributes Source: https://docs.vertesiahq.com/security/principal-attributes Markdown: https://docs.vertesiahq.com/llms/security/principal-attributes.md Principal attributes are the input to Principal Set conditions and to `$principal.*` references in Resource Set rules. Configure them before enabling a restrictive ABAC policy. ## Principal context Vertesia evaluates dynamic rules against this logical context: | Field | Meaning | User source | API-key source | | --- | --- | --- | --- | | `kind` | Authenticated principal type: `user` or `apikey` | Token subject | Token subject | | `clearance` | Numeric security clearance | User and groups | API-key setting | | `compartments` | Security domains such as `legal` or `finance` | User and groups | API-key setting | | `email` | User email address | User profile | Empty string | | `tags` | Group classification or organizational tags | Applicable groups | Empty list | | `properties.*` | Custom organization-specific attributes | User and groups | API-key settings | ## How user and group attributes combine For a user, Vertesia merges the user's attributes with attributes from groups that apply to the selected project: - **Clearance** uses the highest configured value. - **Compartments** are combined as a union. - **Tags** are combined as a union. - **Custom properties** are merged into the `properties` object. Project-restricted groups contribute attributes only in projects they are allowed to cover. This prevents a group's clearance or department properties from leaking into unrelated projects. ## Configure group attributes Group attributes are the preferred way to manage shared organizational policy. 1. Open **Organization Settings → Groups**. 2. Create or edit a group. 3. Configure: - **Clearance** — a level from 0 (Public) through 4 (Secret). - **Compartments** — stable identifiers such as `legal`, `finance`, or `customer-a`. - **Tags** — labels that can select groups dynamically, such as `employee` or `finance`. - **Properties** — a JSON object containing fields such as `department`, `region`, or `employment_type`. - **Allowed Projects** — projects in which the group and its attributes apply. 4. Add the relevant users as members. 5. Save the group and issue a new user token before testing access. Example custom properties: ```json { "department": "legal", "region": "eu", "employment_type": "employee" } ``` Conditions reference these values using paths such as `properties.department`. ## Configure user attributes Organization administrators can edit a user's clearance, compartments, and custom properties from the organization user-management view. Use direct user attributes for exceptions that should not be modeled as group membership. Prefer group attributes for normal team policy. A large number of direct user exceptions is difficult to audit and can produce surprising merged values. ## Configure API-key attributes 1. Open **Project Settings → API Keys**. 2. Locate the key and select **Edit properties**. 3. Set its clearance, compartments, and custom properties. 4. Save and issue a new token for the key before testing. API keys do not inherit user or group attributes. Their principal context comes only from the key itself. Use a Principal Set condition such as `kind = apikey` when a policy should apply only to service access. ### Delegate API-key attributes from automation A project `admin` API key can manage security attributes on other API keys in the same project. The admin key can create or update keys subject to these bounds: - **Clearance** — when the admin key has a clearance, a target key's resulting clearance cannot exceed it. An admin key without a clearance may set any clearance. - **Compartments** — when the admin key has compartments, the target key's resulting compartments must be the same unordered set. An admin key without compartments may set any compartments. - **Properties** — custom properties are unrestricted. Assign the `admin` role only to automation that must provision other project credentials. ## Naming guidance - Keep compartment identifiers stable, lowercase, and machine-friendly. - Use the same vocabulary on principals and resources. - Store numerical policy fields such as security level in the dedicated clearance/sensitivity fields. - Put organization-specific strings and booleans under `properties`. - Avoid placing confidential values in authorization attributes; tokens may carry resolved policy information. ## Safe rollout checklist Before enabling restrict mode: 1. Confirm every affected principal has the expected merged context. 2. Classify a small set of test documents or collections. 3. Create a narrow rule for a test group. 4. Refresh that user's token and verify both allowed and denied resources. 5. Add the global restrict-mode switch only after the granting rules are ready. Continue with [Dynamic access rules](/security/dynamic-rules). --- ## Configure roles and memberships Source: https://docs.vertesiahq.com/security/roles-memberships Markdown: https://docs.vertesiahq.com/llms/security/roles-memberships.md Use role assignments for broad platform capabilities: who can administer a project, execute interactions, manage content, run workflows, or use an application. Vertesia roles are built in; administrators assign them rather than creating custom role definitions. ## Before you start Organization owners and administrators can manage organization-wide users, groups, and access-control entries. Project administrators can manage many project settings, but some organization-level identity and permission changes still require an organization administrator. Use least privilege: choose the smallest role that covers the required work, and review elevated `owner`, `admin`, and `content_superadmin` assignments regularly. ## Assign a user to a project 1. Open **Project Settings**. 2. Select **Invitations**. 3. Invite the user and choose the project role appropriate for their work. 4. After the user accepts, verify the assignment under **Project Settings → Permissions**. To change or remove an existing assignment, open **Project Settings → Permissions**, locate the user's entry, and use its actions menu. ## Assign a role to a group Group assignments keep access consistent as people join or leave a team. 1. Open **Organization Settings → Groups** and create or select a group. 2. Add the appropriate users to the group. 3. Open the target project's **Settings → Permissions** tab. 4. Select **New**. 5. Under **Who**, choose **User / Group / API Key**, then select the group. 6. Choose a system role. 7. Keep **Target Resource** set to **Project** and create the permission. Users receive the union of roles granted directly and through their applicable groups. ## Assign an API-key role Every API key has a base project role. Keep service keys narrowly scoped because the key acts independently of a human user's current session. 1. Open **Project Settings → API Keys**. 2. Create a key and choose the smallest suitable base role. 3. Store the secret immediately in a secure secret manager; it is not intended for browser code. 4. If the key needs an additional project grant, add it from **Project Settings → Permissions**. API keys can also carry ABAC attributes and participate in dynamic rules. See [Principal attributes](/security/principal-attributes). ## Common system roles | Role | Typical use | | --- | --- | | `owner` | Full organization and project control. Reserve for accountable organization owners. | | `admin` | Full administrative access without changing the ownership model. | | `manager` | Broad project operation with selected organization, billing, audit, and super-admin capabilities removed. | | `developer` | Build and operate project resources without organization or project administration. | | `reader` | Read interactions, runs, and content. | | `executor` | Execute interactions and workflows with limited read access. | | `automation` | Run automated content, interaction, and workflow operations. | | `auditor` | Read-only access to operational, content, workflow, agent-run, billing, and audit information. | | `member` | Organization membership without additional project capabilities. | | `content_superadmin` | Full content visibility and mutation bypass. Assign only for trusted administration or processing. | The complete role-to-permission mapping is available from the role catalog API. Developers can retrieve it with `client.iam.roles.listSystem()`. ## Application access Installed applications can have their own access policy. When an application requires explicit access, use its **Manage Permissions** action to grant the appropriate users or groups. Application visibility does not automatically grant the system permissions required by the APIs the application calls. ## Review assignments Use both permission views during an audit: - **Project Settings → Permissions** shows assignments and dynamic rules for the current project. - **Organization Settings → Permissions** shows project-level entries across the organization. Changes that affect token contents take effect when a new access token is issued. Refresh the session or recreate an API-key token before testing a changed role. ## Next - [Learn about Attribute-Based Access Control (ABAC)](/security/abac) - [Configure principal attributes](/security/principal-attributes) - [Build permission-aware applications](/security/developer-guide) --- ## SCIM with Microsoft Entra Source: https://docs.vertesiahq.com/security/scim Markdown: https://docs.vertesiahq.com/llms/security/scim.md ## Create API Key in Vertesia Sign in to Vertesia as an account administrator and select the project that will own the key. * Go to **Settings → API Keys → New**. * Name the key `Microsoft Entra`. * Enable **Organization SCIM provisioning**. This sets the key's role to `admin`. * Create the key and copy its one-time secret into Microsoft Entra's provisioning token field. An ordinary project admin key cannot manage SCIM groups or provision users. To reuse an existing admin secret key, an account administrator can select **Enable SCIM** in the API key list. This keeps its role, secret, and owning project unchanged. Once enabled, the designation cannot be removed. Only account administrators can manage designated keys. Disable or delete the key in Settings to revoke its SCIM management authority. SCIM group operations and user writes check the current key state on every request. User list and lookup endpoints retain the existing `account_read` permission policy: disabling or deleting a key does not immediately invalidate that read permission in an already-issued JWT. Those reads remain subject to normal token validation and expiry. Requests already authorized may finish. ## Create App in Microsoft Entra Create an “Enterprise Application” in Microsoft Entra: * Select "New application" * Select "Create your own application" from Microsoft Entra Gallery * Name the app e.g. "Vertesia" * Select "Integrate any other application you don't find in the gallery (Non-gallery)" Add user/groups:→ * Go to "Users and groups" → "Add user/group" ## Provision User and Groups ### Provisioning * Go to "Manage" → "Provisioning" → "New configuration" * Tenant URL: https://api.vertesia.io/api/v1/iam/scim/v2/ * Token: `VERTESIA_API_KEY` ### Provision on demand or as a job * "Get Started" → "Map attributes (optional) **or** "Manage" → "Attribute mapping" * Remove all except below for users * Consider disabling delete ![][image1] * Remove all except below for groups ![][image2] * "Provision on demand" or "Start provisioning" After users are provisioned as Vertesia org and group members, the group can be: * assigned a role on a project * assigned to an application ### Additional Information * Use the **consumer** role for custom apps with end users. * Existing non-SCIM users provisioned through SCIM will be updated to SCIM users. * User org role will be set to member. **Avoid lockout**: org owner/admin users converted to SCIM users become org memebers. Ensure there is more than one org admin before running a SCIM sync**. ## Troubleshooting in Micrsoft Entra Access the SCIM log in Microsoft Entra: * Go to “Monitor” → “Provisioning logs” * Go to “Monitor” → “Audit logs” ![][image5] [image1]: [image2]: [image3]: [image4]: