# Getting started with the API URL: https://metorial.com/docs/build/api Learn how to use the Metorial API, get your API keys, and make your first request --- Welcome to the Metorial API! This guide will help you quickly understand how to start using our API and make your first requests. **What you'll learn:** * How to use the Metorial API * How to get your API key * How to make your first API request **Recommended reading:** * [Integrations overview](/docs/platform/integrations) ## Before you begin: Getting your API key [#before-you-begin-getting-your-api-key] To use the Metorial API, you'll need an API key. You can generate these in two types: **Publishable API Keys** (`metorial_pk_`): * Used for public data access * Safe to use in client-side code * Starts with `metorial_pk_` **Secret API Keys** (`metorial_sk_`): * Provides full project access * Must be kept private * Starts with `metorial_sk_` You can generate both publishable and secret API keys in your Metorial dashboard by navigating to **Developer** → **API Keys** and clicking **Create API Key**. API Keys page in Metorial dashboard Create API Key dialog in Metorial dashboard ## Making your first API request [#making-your-first-api-request] When making a request, you'll always use this base URL: ```text https://api.metorial.com ``` To authenticate, include your API key in the request header: ```http Authorization: Bearer metorial_sk_abc123 ``` Here's a simple request to list your provider deployments: {`curl -X GET https://api.metorial.com/provider-deployments \\ -H "Authorization: Bearer metorial_sk_abc123"`} {`{ "object": "list", "data": [ { "object": "provider.deployment", "id": "pdp_abc123", "status": "active", "name": "Exa Search" } ], "has_more_before": false, "has_more_after": false }`} ## Full API reference [#full-api-reference] You can view the detailed API reference [here](https://metorial.com/api). ## Client libraries [#client-libraries] We make integration super easy with official SDKs for: Use Metorial with JavaScript or TypeScript. Use Metorial with Python. ## Important things to know [#important-things-to-know] ### Rate limits [#rate-limits] We have some basic usage limits to keep things fair: * 1000 requests per IP every 10 seconds * Development environments: 100 requests per 10 minutes * Production environments: 5000 requests per 10 minutes ### Handling responses [#handling-responses] Our API uses standard HTTP response codes: * `200`: Success! * `400`: Something's wrong with your request * `401`: Invalid API key * `403`: Not enough permissions * `404`: Can't find what you're looking for * `429`: You've hit the rate limit ### Pagination [#pagination] When fetching lists of items, we support cursor-based pagination. Each response includes `has_more_before`, `has_more_after`, and pagination cursors to help you fetch more items easily. ## Need more help? [#need-more-help] Check out our full documentation or reach out to our support team. Happy coding! Explore SDKs for easier integration. --- # Glossary URL: https://metorial.com/docs/build/glossary Definitions for Metorial organizations, providers, sessions, identities, consumers, and related concepts. --- This glossary summarizes core Metorial concepts. For deeper guides, see [Organizations, projects, and environments](/docs/build/operate/organizations-and-projects), [Providers](/docs/build/operate/providers), [Sessions](/docs/build/operate/sessions), and [Workforce](/docs/platform/workforce). A ## Account [#account] An account is the dashboard term for a [consumer](#consumer) managed through Workforce. Accounts represent people or external users who can receive access to approved [providers](#provider), [magic MCP servers](#magic-mcp-server), and [portals](#portal). ## Actor [#actor] An actor is any entity that can perform actions in Metorial: a person or an automated agent. Actors own one or more [identities](#identity). Examples include a developer using the [Metorial CLI](#metorial-cli), a support agent using a [portal](#portal), or a background workflow. C ## Callback [#callback] A callback is a webhook that receives event notifications when a [trigger](#trigger) fires, so your system can react to changes in an external system. ## Consumer [#consumer] A consumer is a user who interacts with a Metorial [instance](#instance) through an interface such as a [portal](#portal), the [Metorial CLI](#metorial-cli), or a custom surface. Consumers only see resources they are allowed to access, including [providers](#provider) and [magic MCP servers](#magic-mcp-server) assigned to them. ## Consumer profile [#consumer-profile] A consumer profile is how the same person is represented in a specific interface (for example a portal versus the CLI). One person can have multiple profiles; access rules remain consistent across them. ## Credential [#credential] A credential is concrete access to a specific external provider for an [identity](#identity). It combines provider authentication configuration, [provider config](#provider-config), and [provider deployments](#provider-deployment) into a fully configured connection ready to use. ## Custom provider [#custom-provider] A custom provider is a user-defined integration that connects Metorial to your own services using MCP-compatible interfaces, for example a Docker-based MCP server, a remote MCP server, or a Metorial-hosted server. Once deployed, it behaves like any other [provider](#provider) in [sessions](#session) and configurations. ## Custom provider deployment [#custom-provider-deployment] A custom provider deployment is created when you introduce a new [custom provider version](#custom-provider-version). It runs build and discovery, moves through states until successful, and enables a standard [provider version](#provider-version) you can use across Metorial. ## Custom provider version [#custom-provider-version] A custom provider version is a versioned snapshot of your custom integration. New versions do not affect existing ones. Remote MCP servers are typically not version controlled by Metorial. D ## Deployment [#deployment] In Metorial, a **deployment** is a running integration that can execute tool calls and scale to handle load. You usually work with one of two kinds: a [provider deployment](#provider-deployment), which runs a catalog or custom [provider](#provider) with its [provider config](#provider-config) and [provider auth config](#provider-auth-config), or a [custom provider deployment](#custom-provider-deployment), which is the build-and-discovery pipeline that turns a new [custom provider version](#custom-provider-version) into a usable [provider version](#provider-version). [Sessions](#session) reference deployments (for example by provider deployment ID) so your AI clients know which running integration to use. I ## Identity [#identity] An identity is a collection of external access tied to an [actor](#actor): which accounts or systems that actor can use. An identity is owned by exactly one actor and contains one or more [credentials](#credential). Identities can be shared through [identity delegation](#identity-delegation). ## Identity delegation [#identity-delegation] Identity delegation lets one [actor](#actor) temporarily use another actor’s [credentials](#credential) to act on their behalf. Delegation is controlled, scoped, and can be time-bound. ## Instance [#instance] An [organization](#organization) contains [projects](#project), and a project contains isolated environments called instances. Each project has one production instance by default and can have multiple sandbox instances. Each instance has its own resources, such as custom [providers](#provider), [sessions](#session), and logs. M ## Magic MCP server [#magic-mcp-server] A **magic MCP server** is a Metorial resource that exposes a Model Context Protocol (MCP) endpoint for your [consumers](#consumer), alongside catalog [providers](#provider). You assign it per [instance](#instance) and access rules; users connect through the same surfaces as other integrations (for example a [portal](#portal) or the [Metorial CLI](#metorial-cli)). ## Magic Skill [#magic-skill] A Magic Skill is a reusable Workforce asset that packages agent workflows, documents, custom logic, and resources. Magic Skills can be organized into [skill groups](#skill-group), published through [skill marketplaces](#skill-marketplace), and governed by default execution settings. ## Member [#member] A member is a person with access to your [organization](#organization). You assign [roles](#role), [policies](#policy), or [teams](#team) to control what they can see and do. ## Metorial CLI [#metorial-cli] The Metorial CLI is the command-line interface for [consumers](#consumer) to interact with Metorial: connect to [providers](#provider) and magic MCP servers, fetch logs, and automate workflows. O ## Organization [#organization] An organization is the top-level workspace in Metorial. It typically represents your company or a major team. All [projects](#project) and their [instances](#instance) live inside an organization, and it is the central place to manage [members](#member) and global access boundaries. P ## Participant [#participant] A participant is any entity involved in a [session](#session), for example a connected client or a [session provider](#session-provider). A session can have multiple participants at the same time. ## Policy [#policy] A policy defines who can access what by assigning [roles](#role) to [members](#member) or [teams](#team) at the [organization](#organization), [project](#project), or [instance](#instance) level. ## Portal [#portal] A portal is a custom user interface on top of a Metorial [instance](#instance). It exposes selected [providers](#provider) and resources without granting the full dashboard, and supports authentication such as SAML SSO or email login. ## Project [#project] A project groups related work inside an [organization](#organization), for example a product, feature, or team. You can set access separately per project so teams only see what they are assigned. ## Provider [#provider] A provider is an integration with an external system. It defines what is possible, typically [tools](#tool) callable via MCP or the Metorial API, and optionally [triggers](#trigger) for external events. Providers are [versioned](#provider-version), can be built-in or [custom](#custom-provider), and appear in the catalog. ## Provider auth config [#provider-auth-config] A provider auth config stores authentication material such as API keys or OAuth tokens for a [provider](#provider). It is required for authenticated providers and optional when authentication is not needed. You can maintain multiple auth configs. ## Provider config [#provider-config] A provider config stores non-sensitive settings for a [provider](#provider), for example regions, endpoints, or feature flags. Metorial can create defaults when none are required. Multiple configs per provider are supported. ## Provider deployment [#provider-deployment] A provider deployment is the runtime instance that combines a [provider](#provider) with a [provider config](#provider-config) and [provider auth config](#provider-auth-config) and executes requests. Multiple deployments can share the same configs and scale independently. For how deployments fit into the overall model, see [Deployment](#deployment). ## Provider version [#provider-version] A provider version is a specific release of a [provider](#provider). New versions can add or change [tools](#tool) and [triggers](#trigger) while older versions keep existing setups stable. ## Provider skill [#provider-skill] A provider skill is a human-readable capability summary shown on provider pages. Skills help you evaluate what a provider is useful for, but they are not executable [tools](#tool). R ## Role [#role] A role is a named set of permissions (scopes) that describes what actions someone can perform, for example read-only versus full management. [Policies](#policy) attach roles to [members](#member) or [teams](#team). S ## Session [#session] A session is a connectable environment that exposes [tools](#tool) from one or more [providers](#provider) through a single interface. [Session providers](#session-provider) attach each provider; clients connect via [session connections](#session-connection). Activity is recorded in logs. ## Session connection [#session-connection] A session connection represents one client connected to a [session](#session). Multiple connections can exist at once and do not interfere with each other. ## Session provider [#session-provider] A session provider attaches a [provider](#provider) to a [session](#session) using a [provider config](#provider-config), [provider auth config](#provider-auth-config), and [provider deployment](#provider-deployment). Each session provider contributes [tools](#tool) to the session. ## Skill group [#skill-group] A skill group organizes related [Magic Skills](#magic-skill) so they can be managed as a set. ## Skill marketplace [#skill-marketplace] A skill marketplace publishes selected plugins and [Magic Skills](#magic-skill) to users through a curated catalog. T ## Team [#team] A team is a group of [members](#member) who share the same access rules. You assign [policies](#policy) to the team once instead of managing each person individually. ## Tool [#tool] Tools are the callable actions exposed by a [provider](#provider). They are invoked through MCP or the Metorial API and may return data or perform operations in external systems. ## Trigger [#trigger] A trigger lets a [provider](#provider) react to events in an external system. When a trigger fires, a [callback](#callback) can notify your application so you can run event-driven workflows. W ## Workforce [#workforce] Workforce is the product area for managing [accounts](#account), agents, [identities](#identity), and [identity delegation](#identity-delegation). It connects human and non-human actors to the provider access, portals, and Magic MCP servers they are allowed to use. --- # Developer quickstart URL: https://metorial.com/docs/build/quickstart Get up and running with Metorial in under 5 minutes --- * How to install the Metorial SDK * Available AI providers and integrations * How to create sessions and use tools * OAuth flow for authenticated services **Before you begin** * [Create API keys](/docs/build/api) **References** * [GitHub: metorial-node](https://github.com/metorial/metorial-node) * [GitHub: metorial-python](https://github.com/metorial/metorial-python) ## Prerequisites [#prerequisites] 1. **Metorial API key**: Get one from [platform.metorial.com](https://platform.metorial.com) 2. **Provider deployment ID**: Deploy a provider (e.g., Exa for search) from the dashboard 3. **AI provider API key**: OpenAI, Anthropic, Google, etc. ## Installation [#installation] The Metorial SDK consists of two parts: the core SDK (which handles sessions and tool management) and a provider package (which integrates with your chosen AI model). Install both to get started. TypeScript Python {`# Install the core SDK npm install metorial # Install a provider package (choose one or more) npm install @metorial/ai-sdk # Vercel AI SDK (recommended) npm install @metorial/anthropic # Anthropic Claude npm install @metorial/openai # OpenAI npm install @metorial/google # Google Gemini npm install @metorial/mistral # Mistral npm install @metorial/deepseek # DeepSeek`} {`# Install the SDK pip install metorial # Install your AI provider pip install anthropic # For Claude pip install openai # For OpenAI/DeepSeek`} We support OpenAI, Anthropic, Google, Mistral, DeepSeek, and any OpenAI-compatible API. See the [TypeScript SDK](https://github.com/metorial/metorial-node) and [Python SDK](https://github.com/metorial/metorial-python) repos for all available providers. ## Your first AI agent [#your-first-ai-agent] This example shows how to create an AI agent that can use tools from your deployed providers. The agent will have access to any tools provided by your provider deployment (like search, file operations, or API calls). **What this code does:** 1. Initializes the Metorial SDK with your API key 2. Creates a session connected to your provider deployment 3. Passes the available tools to your AI model 4. Handles tool calls in an agentic loop TypeScript Python ```typescript import { Metorial } from 'metorial'; import { metorialAiSdk } from '@metorial/ai-sdk'; import { anthropic } from '@ai-sdk/anthropic'; import { stepCountIs, streamText } from 'ai'; let metorial = new Metorial({ apiKey: process.env.METORIAL_API_KEY }); let session = await metorial.connect({ adapter: metorialAiSdk(), providers: [ { providerDeploymentId: 'your-provider-deployment-id' } ] }); let result = streamText({ model: anthropic('claude-sonnet-4-20250514'), prompt: 'Search for the latest AI research', stopWhen: stepCountIs(10), tools: session.tools() }); for await (let textPart of result.textStream) { process.stdout.write(textPart); } ``` ```python from metorial import Metorial, metorial_anthropic from anthropic import AsyncAnthropic import asyncio import os metorial = Metorial(api_key=os.getenv("METORIAL_API_KEY")) anthropic = AsyncAnthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) async def main(): session = await metorial.connect( adapter=metorial_anthropic(), providers=[{"provider_deployment_id": "your-provider-deployment-id"}], ) response = await anthropic.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, tools=session.tools(), messages=[{"role": "user", "content": "What's trending on Hacker News?"}], ) if response.stop_reason == "tool_use": tool_calls = [b for b in response.content if b.type == "tool_use"] results = await session.call_tools(tool_calls) # Add results to messages and continue conversation... asyncio.run(main()) ``` ## OAuth flow [#oauth-flow] For services requiring user authentication (like Slack, GitHub, or Google Calendar), use a **Provider Setup Session** to authorize your users. The resulting **Provider Auth Config** stores their credentials for reuse. **The flow:** 1. Create a Provider Setup Session for each service that needs authentication 2. Redirect your user to the session URL to complete OAuth 3. Wait for the setup session to complete — you get back an auth config 4. Pass the auth config ID when creating sessions for that user TypeScript Python ```typescript import { Metorial } from 'metorial'; import { metorialAnthropic } from '@metorial/anthropic'; let metorial = new Metorial({ apiKey: process.env.METORIAL_API_KEY }); // 1. Create a setup session for the provider let setupSession = await metorial.providerDeployments.setupSessions.create({ providerId: 'your-slack-provider-id', providerAuthMethodId: 'oauth' // callbackUri: 'https://yourapp.com/oauth/callback' }); // 2. Send the URL to your user console.log('Authorize Slack:', setupSession.url); // 3. Wait for completion — returns the completed setup session let completed = await metorial.providerDeployments.setupSessions.waitForCompletion([setupSession]); // Store completed[0].authConfig.id for this user in your database // 4. Use the auth config in a session let session = await metorial.connect({ adapter: metorialAnthropic(), providers: [ { providerDeploymentId: 'your-provider-deployment-id', providerAuthConfigId: completed[0]!.authConfig!.id } ] }); let tools = session.tools(); ``` ```python import asyncio from metorial import Metorial, metorial_anthropic async def main(): metorial = Metorial(api_key="your-metorial-api-key") # 1. Create a setup session for the provider setup_session = await metorial.providers.setup_sessions.create( provider_id="your-slack-provider-id", provider_auth_method_id="oauth" # redirect_url="https://yourapp.com/oauth/callback" ) # 2. Send the URL to your user print(f"Authorize Slack: {setup_session.url}") # 3. Wait for completion — returns the completed setup session completed = await metorial.wait_for_setup_session([setup_session]) # Store completed.auth_config.id for this user in your database # 4. Use the auth config in a session session = await metorial.connect( adapter=metorial_anthropic(), providers=[ { "provider_deployment_id": "your-slack-provider-deployment-id", "provider_auth_config_id": completed[0].auth_config.id }, { "provider_deployment_id": "your-exa-deployment-id" # No OAuth needed } ], ) tools = session.tools() # Use tools... asyncio.run(main()) ``` ## Error handling [#error-handling] The Metorial SDK provides specific error types to help you handle different failure scenarios. Catch `MetorialAPIError` for API-related issues like authentication failures, rate limits, or invalid deployment IDs. TypeScript Python ```typescript import { MetorialAPIError } from 'metorial'; try { /* ... */ } catch (error) { if (error instanceof MetorialAPIError) { console.error(`API Error: ${error.message} (Status: ${error.status})`); } else { console.error('Unexpected error:', error); } } ``` ```python from metorial import AuthenticationError, NotFoundError, RateLimitError try: session = await metorial.connect( adapter=metorial_openai(), providers=[{"provider_deployment_id": "your-deployment-id"}], ) tools = session.tools() except AuthenticationError: print("Check your METORIAL_API_KEY") except NotFoundError: print("Deployment not found - verify your deployment ID") except RateLimitError: print("Rate limited - try again later") except Exception as e: print(f"Unexpected error: {e}") ``` --- # Custom providers URL: https://metorial.com/docs/build/custom-providers Deploy your own MCP server code on Metorial. --- * What custom providers are * How to get started Beyond the pre-built integrations in the Metorial catalog, you can upload your own MCP server code and let Metorial host it. Metorial handles scaling and monitoring. Custom providers currently support TypeScript and JavaScript on Node.js. **Best for:** * Custom business logic and internal APIs * Teams that want to focus on code rather than infrastructure * Prototyping and deployment ## Getting started [#getting-started] Deploy code hosted by Metorial. --- # Deploying custom providers URL: https://metorial.com/docs/build/custom-providers/managed-deployment Learn how to deploy and manage versions of your custom providers. --- * How deployments work * Version management * Rollback strategies Deploy your custom provider code and manage different versions over time. Metorial automatically builds and deploys your code when you upload it. ## Creating deployments [#creating-deployments] Upload your provider code through the dashboard. Editing custom provider code Metorial builds your provider and installs dependencies automatically. The build process validates your code and reports any errors. Your provider deploys and becomes available for use. ## Version management [#version-management] Each deployment creates a new version with a unique version number. View all versions in the dashboard, including deployment timestamps and the code changes in each version. You can switch between versions at any time. **Version History:** * Every deployment is preserved * View timestamps and changes * Switch between versions instantly ## Rolling back [#rolling-back] Revert to any previous version if you encounter issues with a new deployment. Select the version you want to restore and click Deploy. Rolling back is instant and doesn't require rebuilding your code. --- # Getting started with custom providers URL: https://metorial.com/docs/build/custom-providers/managed-quickstart Deploy custom MCP server code without managing infrastructure. --- * How to create a custom provider * Supported runtime Managed providers let you deploy custom MCP server code without managing infrastructure. Upload your code and Metorial handles hosting, scaling, and deployment. ## Supported runtime [#supported-runtime] Hosted custom providers currently run on Node.js. **TypeScript/JavaScript (Node.js)** * Full npm package support * Automatic TypeScript compilation * Node.js 18+ runtime ## Creating a custom provider [#creating-a-custom-provider] Go to **Providers** → **Custom Providers**. Navigate to custom providers Click **Create Custom Provider**. Upload your MCP server code. Create custom provider Click **Deploy** and wait for the build to complete. --- # Using with AI SDK URL: https://metorial.com/docs/build/providers/ai-sdk Use Metorial with Vercel AI SDK (recommended for TypeScript) --- The Vercel AI SDK integration (`@metorial/ai-sdk`) is the recommended way to use Metorial in TypeScript applications. It provides excellent streaming support, a unified interface across multiple model providers, and seamless tool integration. This integration works with any model provider supported by the AI SDK, including Anthropic, OpenAI, Google, and more. You get all the benefits of the AI SDK's streaming and tool handling while Metorial manages your provider connections. **What this example does:** 1. Creates a Metorial session connected to your provider deployment 2. Passes MCP tools directly to the AI SDK's `streamText` function 3. Streams the AI's response in real-time ## Example [#example] TypeScript Python ```typescript import { Metorial } from 'metorial'; import { metorialAiSdk } from '@metorial/ai-sdk'; import { anthropic } from '@ai-sdk/anthropic'; import { streamText } from 'ai'; let metorial = new Metorial({ apiKey: process.env.METORIAL_API_KEY }); let session = await metorial.connect({ adapter: metorialAiSdk(), providers: [{ providerDeploymentId: 'your-provider-deployment-id' }] }); let result = streamText({ model: anthropic("claude-sonnet-4-20250514"), prompt: "Help me", tools: session.tools(), }); for await (let text of result.textStream) { process.stdout.write(text); } ``` ```python # AI SDK is TypeScript-only # Use provider-specific client for Python (see Anthropic, OpenAI pages) ``` --- # Using with Anthropic URL: https://metorial.com/docs/build/providers/anthropic Use Metorial with Claude models --- The Anthropic provider integration lets you use Metorial's MCP tools with Claude models (Sonnet, Opus, and Haiku). The integration converts Metorial tools into Anthropic's tool format and handles tool call execution. Claude excels at complex reasoning and following instructions precisely, making it a great choice for AI agents that use multiple tools. You'll need both a Metorial API key and an Anthropic API key to use this integration. **What this example does:** 1. Initializes both Metorial and Anthropic clients 2. Creates a Metorial session that provides tools in Anthropic's format 3. Passes those tools to Claude's messages API 4. Provides a `callTools` function to execute any tool calls Claude requests ## Example [#example] TypeScript Python ```typescript import { Metorial } from 'metorial'; import { metorialAnthropic } from '@metorial/anthropic'; import Anthropic from '@anthropic-ai/sdk'; let metorial = new Metorial({ apiKey: process.env.METORIAL_API_KEY }); let anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); let session = await metorial.connect({ adapter: metorialAnthropic(), providers: [ { providerDeploymentId: 'your-provider-deployment-id' } ] }); let response = await anthropic.messages.create({ model: "claude-sonnet-4-20250514", max_tokens: 1024, messages: [{ role: "user", content: "Help me" }], tools: session.tools() }); ``` ```python from metorial import Metorial, metorial_anthropic from anthropic import AsyncAnthropic import asyncio import os metorial = Metorial(api_key=os.getenv("METORIAL_API_KEY")) anthropic = AsyncAnthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) async def main(): session = await metorial.connect( adapter=metorial_anthropic(), providers=[{"provider_deployment_id": "your-provider-deployment-id"}], ) response = await anthropic.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, tools=session.tools(), messages=[{"role": "user", "content": "What's trending on Hacker News?"}], ) if response.stop_reason == "tool_use": tool_calls = [b for b in response.content if b.type == "tool_use"] results = await session.call_tools(tool_calls) # Add results to messages and continue conversation... asyncio.run(main()) ``` --- # Using with Google URL: https://metorial.com/docs/build/providers/google Use Metorial with Google Gemini models --- The Google provider integration lets you use Metorial's MCP tools with Google's Gemini models. The integration converts Metorial tools into Google's function calling format, allowing Gemini to use tools from your providers. Gemini models offer competitive performance and pricing, with strong multimodal capabilities. You'll need both a Metorial API key and a Google AI API key to use this integration. **What this example does:** 1. Initializes both Metorial and Google AI clients 2. Creates a Metorial session that provides tools in Google's format 3. Passes those tools when creating a Gemini model instance 4. The model can then call tools as needed during generation ## Example [#example] TypeScript Python ```typescript import { Metorial } from 'metorial'; import { metorialGoogle } from '@metorial/google'; import { GoogleGenerativeAI } from '@google/generative-ai'; let metorial = new Metorial({ apiKey: process.env.METORIAL_API_KEY }); let genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY); let session = await metorial.connect({ adapter: metorialGoogle(), providers: [ { providerDeploymentId: 'your-provider-deployment-id' }, ], }); let model = genAI.getGenerativeModel({ model: "gemini-1.5-pro", tools: session.tools() }); ``` ```python from metorial import Metorial, metorial_google import google.generativeai as genai import asyncio import os metorial = Metorial(api_key=os.getenv("METORIAL_API_KEY")) genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) async def main(): session = await metorial.connect( adapter=metorial_google(), providers=[{"provider_deployment_id": "your-provider-deployment-id"}], ) model = genai.GenerativeModel("gemini-2.5-pro", tools=session.tools()) chat = model.start_chat() response = chat.send_message("What's trending on Hacker News?") for part in response.parts: if fn := part.function_call: result = await session.call_tool(fn.name, dict(fn.args)) # Continue conversation with result... asyncio.run(main()) ``` --- # Provider runtime URL: https://metorial.com/docs/build/providers Metorial works with all major AI providers --- * Available providers * Provider comparison * Choosing a provider Metorial is provider-agnostic, meaning you can use it with any AI model from any provider. The SDK provides integration packages for major AI providers, making it easy to connect Metorial's MCP tools with your chosen model. Each provider integration handles the specifics of formatting tools and responses in the way that provider expects, so you can focus on building your AI application rather than dealing with API differences. ## Supported providers [#supported-providers] * **OpenAI** - GPT-4, GPT-3.5, and other OpenAI models * **Anthropic** - Claude Sonnet, Opus, and Haiku models * **Google** - Gemini Pro and other Google models * **Vercel AI SDK** - Unified interface for multiple providers (recommended for TypeScript) * **Mistral** - Mistral models * **DeepSeek** - DeepSeek models via OpenAI-compatible API * **TogetherAI** - Models hosted on TogetherAI * **XAI** - Grok models * **Any OpenAI-compatible API** - Use the OpenAI provider with custom base URLs ## Choosing a provider [#choosing-a-provider] **For TypeScript projects**, we recommend using the **Vercel AI SDK** provider (`@metorial/ai-sdk`). It offers the best developer experience with built-in streaming support, unified tool handling, and compatibility with multiple model providers. **For Python projects**, use `metorial.connect(adapter=metorial_anthropic())` or `metorial.connect(adapter=metorial_openai())` to create a session. The adapter formats tools automatically for the provider you specify, and you use the official Python SDK from each provider to make model calls. **For production applications**, consider factors like model capabilities, pricing, rate limits, and latency when choosing your provider. Metorial makes it easy to switch providers later if your needs change. ## Provider guides [#provider-guides] Use with OpenAI GPT models Use with Claude models Use with Google Gemini models Use with Vercel AI SDK (recommended for TypeScript) --- # OpenAI-compatible providers URL: https://metorial.com/docs/build/providers/openai-compatible Use Metorial with DeepSeek, TogetherAI, XAI, and other OpenAI-compatible APIs --- Many AI providers offer APIs that are compatible with OpenAI's format. This means you can use the same code patterns and tools, just by changing the base URL and API key. Metorial supports all of these OpenAI-compatible providers. **Supported OpenAI-compatible providers:** * **DeepSeek** - Cost-effective models with strong performance * **TogetherAI** - Open-source models with flexible deployment * **XAI (Grok)** - Models from xAI * **Any custom OpenAI-compatible endpoint** - Self-hosted or proprietary models The integration uses the OpenAI SDK with a custom base URL, making it easy to switch between different OpenAI-compatible providers without changing your code structure. **What this example does:** 1. Creates an OpenAI client pointed at DeepSeek's API endpoint 2. Uses Metorial's DeepSeek provider integration (or OpenAI provider for generic APIs) 3. Passes tools to the model in OpenAI's function calling format 4. The model can call tools just like with OpenAI's GPT models ## Example [#example] TypeScript Python ```typescript import { Metorial } from 'metorial'; import { createOpenAICompatibleMcpSdk } from "@metorial/openai-compatible"; import OpenAI from 'openai'; let metorial = new Metorial({ apiKey: process.env.METORIAL_API_KEY }); let deepseek = new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: 'https://api.deepseek.com' }); let session = await metorial.connect({ adapter: createOpenAICompatibleMcpSdk()(), providers: [ { providerDeploymentId: 'your-provider-deployment-id' }, ], }); let response = await deepseek.chat.completions.create({ model: "deepseek-chat", messages: [{ role: "user", content: "Help me" }], tools: session.tools() }); ``` ```python from metorial import Metorial, metorial_openai_compatible from openai import AsyncOpenAI import asyncio import os metorial = Metorial(api_key=os.getenv("METORIAL_API_KEY")) deepseek = AsyncOpenAI( api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com" ) async def main(): session = await metorial.connect( adapter=metorial_openai_compatible(), providers=[{"provider_deployment_id": "your-provider-deployment-id"}], ) response = await deepseek.chat.completions.create( model="deepseek-chat", tools=session.tools(), messages=[{"role": "user", "content": "What's trending on Hacker News?"}], ) if response.choices[0].message.tool_calls: results = await session.call_tools(response.choices[0].message.tool_calls) # Add results to messages and continue conversation... asyncio.run(main()) ``` --- # Using with OpenAI URL: https://metorial.com/docs/build/providers/openai Use Metorial with OpenAI GPT models --- The OpenAI provider integration lets you use Metorial's MCP tools with OpenAI's GPT models, including GPT-4 and GPT-3.5. The integration handles converting Metorial tools into OpenAI's function calling format and processing tool call responses. You'll need both a Metorial API key and an OpenAI API key to use this integration. The Metorial SDK manages your provider connections, while the OpenAI SDK handles the model interactions. **What this example does:** 1. Initializes both Metorial and OpenAI clients 2. Creates a Metorial session that provides tools in OpenAI's format 3. Passes those tools to OpenAI's chat completions API 4. Provides a `callTools` function to execute any tool calls the model requests ## Example [#example] TypeScript Python ```typescript import { Metorial } from 'metorial'; import { metorialOpenAI } from '@metorial/openai'; import OpenAI from 'openai'; let metorial = new Metorial({ apiKey: process.env.METORIAL_API_KEY }); let openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); let session = await metorial.connect({ adapter: metorialOpenAI.chatCompletions(), providers: [ { providerDeploymentId: 'your-provider-deployment-id' } ] }); let response = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "Help me" }], tools: session.tools() }); ``` ```python from metorial import Metorial, metorial_openai from openai import AsyncOpenAI import asyncio import os metorial = Metorial(api_key=os.getenv("METORIAL_API_KEY")) openai = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY")) async def main(): session = await metorial.connect( adapter=metorial_openai(), providers=[{"provider_deployment_id": "your-provider-deployment-id"}], ) response = await openai.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Help me"}], tools=session.tools(), ) if response.choices[0].message.tool_calls: results = await session.call_tools(response.choices[0].message.tool_calls) # Add results to messages and continue conversation... asyncio.run(main()) ``` --- # Use providers with SDKs URL: https://metorial.com/docs/build/providers/with-sdks Connect Metorial provider tools to an LLM and build a chat-enabled app. --- After you configure provider access in Metorial, you can connect those tools to an LLM like ChatGPT. In this guide, you'll learn how to build a chat-enabled app that automatically handles tool calls from your Metorial providers. * How to use a Metorial provider * How to use the Metorial SDKs * Create a Metorial project * Configure at least one provider or integration * Create an API key Run the installer for your language of choice: TypeScript Python npm install metorial @metorial/openai openai pip install metorial openai Instantiate both clients with your API keys and your provider deployment ID. ```typescript TypeScript import { Metorial } from 'metorial'; import { metorialOpenAI } from '@metorial/openai'; import OpenAI from 'openai'; let metorial = new Metorial({ apiKey: 'metorial_sk_io2h4...' }); let openai = new OpenAI({ apiKey: '...your-openai-api-key...' }); ``` ```python Python from metorial import Metorial, metorial_openai from openai import AsyncOpenAI metorial = Metorial(api_key="metorial_sk_io2h4...") openai = AsyncOpenAI(api_key="...your-openai-api-key...") ``` Create a session that exposes your deployed provider tools. ```typescript TypeScript let session = await metorial.connect({ adapter: metorialOpenAI.chatCompletions(), providers: [ { providerDeploymentId: '...your-provider-deployment-id...' } ] }); let tools = session.tools(); ``` ```python Python session = await metorial.connect( adapter=metorial_openai(), providers=[{"provider_deployment_id": "...your-provider-deployment-id..."}], ) tools = session.tools() ``` Kick off the loop by sending an initial message. ```typescript TypeScript let messages = [ { role: "user", content: "Summarize the README.md file of the metorial/websocket-explorer repository on GitHub." } ]; ``` ```python Python messages = [ {"role": "user", "content": "Summarize the README.md file of the metorial/websocket-explorer repository on GitHub."} ] ``` 1. Send `messages` to OpenAI, passing the tools. 2. If the assistant response contains `tool_calls`, invoke it: ```typescript TypeScript let response = await openai.chat.completions.create({ model: 'gpt-4o', messages, tools }); let choice = response.choices[0]!; let toolCalls = choice.message.tool_calls; let toolResults = await session.callTools(toolCalls); ``` ```python Python response = await openai.chat.completions.create( model="gpt-4o", messages=messages, tools=session.tools(), ) choice = response.choices[0] tool_calls = choice.message.tool_calls tool_results = await session.call_tools(tool_calls) ``` 3. Append both the tool call requests and their results to `messages`. 4. Repeat until the assistant's response has no more `tool_calls`. Once there are no more tool calls, your assistant's final reply is in: ```typescript TypeScript console.log(choice.message.content); ``` ```python Python print(choice.message.content) ``` --- # Code review bot with GitHub URL: https://metorial.com/docs/build/samples/code-review-bot Create an AI-powered bot that analyzes pull requests for security issues, code smells, and style violations --- Build an AI-powered code review bot that reads changes from your git repository, analyzes them for security vulnerabilities, code smells, and style issues, then posts general and line-specific review comments to GitHub — requesting changes when it finds problems and approving clean pull requests. * Configuring the GitHub provider * Setting up OAuth for GitHub repositories * Creating an AI agent that reviews code * Posting GitHub review comments programmatically * [Review Workforce concepts](/docs/platform/get-started/core-concepts) * [Create API keys](/docs/build/api) * GitHub account with a repository and at least one PR * Anthropic API key (Claude Sonnet 4 or newer recommended) **Time to complete:** 10-15 minutes ## Prerequisites [#prerequisites] Before building the code review bot, ensure you have: 1. **Metorial setup**: * Active Metorial account at [platform.metorial.com](https://platform.metorial.com) * Project created in your organization * Metorial API key (generate in Dashboard → Developer → API Keys) 2. **GitHub repository**: * Local clone of your GitHub repository * Repository with write access on GitHub * At least one pull request for testing * Admin access to authorize OAuth 3. **AI provider**: * Anthropic API key (Claude Sonnet 4 or newer recommended for code analysis) 4. **Development environment**: * Node.js 18+ (TypeScript) or Python 3.9+ installed * Basic knowledge of async/await patterns ## Architecture overview [#architecture-overview] The code review bot workflow: 1. **Input**: User provides local repository path, branch names, and PR number 2. **Fetch Code Changes**: Bot reads git diff locally from your repository 3. **AI Analysis**: Claude analyzes the diff for: * Security vulnerabilities (SQL injection, XSS, exposed secrets) * Code smells (duplication, long functions, complexity) * Style violations (naming, formatting, consistency) * Best practices (error handling, documentation, testing) 4. **Post Review**: Bot posts the review to GitHub via the GitHub provider: * Overall summary comment * Line-specific feedback on issues * Review decision (approve or request changes) **Tools used**: Local git (read code changes) + AI Model (code analysis) + GitHub provider (post reviews) ## Step 1: Configure GitHub provider [#step-1-configure-github-provider] Configure the GitHub provider from Metorial's catalog to enable your bot to interact with GitHub. In the Metorial Dashboard, go to **Providers** and search for "GitHub". Click the **GitHub** provider, then click **Use Provider** → **Integration**. Choose the GitHub auth method, create or select auth credentials, and review the tool filters for repository, issue, pull request, and file-content access. After setup, copy the provider deployment or integration ID shown in the dashboard. You'll need this for OAuth setup and in your bot code. Save your GitHub provider ID—you'll need it for OAuth setup (Step 2) and in your bot code (Step 3). ## Step 2: Set up OAuth authentication [#step-2-set-up-oauth-authentication] Your code review bot needs permission to access your GitHub repositories. Install the Metorial SDK and Anthropic: TypeScript Python npm install metorial @metorial/anthropic @anthropic-ai/sdk pip install metorial anthropic Run this code to generate the GitHub OAuth URL: ```typescript TypeScript import { Metorial } from 'metorial'; const metorial = new Metorial({ apiKey: "YOUR-METORIAL-API-KEY" }); async function setupGitHubOAuth() { const githubSetup = await metorial.providers.setupSessions.create({ providerId: 'YOUR-GITHUB-PROVIDER-ID', providerAuthMethodId: 'oauth' }); console.log('Authorize GitHub here:', githubSetup.url); // Wait for authorization const completed = await metorial.waitForSetupSession([githubSetup]); console.log('✓ GitHub authorized!'); // Save the auth config ID for future use return completed.authConfig.id; } setupGitHubOAuth(); ``` ```python Python import asyncio from metorial import Metorial async def setup_github_oauth(): metorial = Metorial(api_key="YOUR-METORIAL-API-KEY") github_setup = await metorial.providers.setup_sessions.create( provider_id="YOUR-GITHUB-PROVIDER-ID", provider_auth_method_id="oauth" ) print(f"Authorize GitHub here: {github_setup.url}") # Wait for authorization completed = await metorial.wait_for_setup_session(github_setup) print("✓ GitHub authorized!") # Save the auth config ID for future use return completed.auth_config.id asyncio.run(setup_github_oauth()) ``` 1. Open the printed OAuth URL in your browser 2. Sign in to GitHub if needed 3. Review and approve the permissions (the bot needs `repo` scope to read PRs and post comments) 4. You'll be redirected to your callback URL (or see a confirmation page) Save the auth config ID securely. You'll reuse it for all future bot operations without re-authorizing. For production apps, store auth config IDs in your database per user/repository. **Required OAuth Scopes:** The GitHub provider requires the `repo` scope for posting reviews, which provides: * Write access to post review comments and line-specific feedback * Permission to approve PRs or request changes on your repositories **Note**: If you plan to implement webhook-based automation (mentioned in Production Considerations), you may need additional scopes like `admin:repo_hook`. The basic bot functionality shown in this tutorial only requires `repo`. The required scopes are automatically requested when you authorize via the OAuth URL. ## Step 3: Build the code review bot [#step-3-build-the-code-review-bot] Create the main bot that analyzes pull requests and posts review comments. TypeScript Python ```typescript import { Metorial } from 'metorial'; import { metorialAnthropic } from '@metorial/anthropic'; import Anthropic from '@anthropic-ai/sdk'; import { execSync } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; const metorial = new Metorial({ apiKey: "YOUR-METORIAL-API-KEY" }); const anthropic = new Anthropic({ apiKey: "YOUR_ANTHROPIC_API_KEY" }); // GitHub integration credentials const GITHUB_DEPLOYMENT_ID = "GITHUB_DEPLOYMENT_ID"; const GITHUB_AUTH_CONFIG_ID = "GITHUB_AUTH_CONFIG_ID"; async function reviewPullRequest( repoPath: string, owner: string, repo: string, branchName: string, prNumber: number, baseBranch: string = 'main' ) { console.log(`Starting review of PR #${prNumber} (${branchName})`); // Validate repository path if (!fs.existsSync(repoPath)) { throw new Error(`Repository path does not exist: ${repoPath}`); } const gitDir = path.join(repoPath, '.git'); if (!fs.existsSync(gitDir)) { throw new Error(`Not a git repository: ${repoPath}`); } // Get diff from local repository console.log(`Fetching diff: ${baseBranch}...${branchName}`); const gitDiffCmd = `cd "${repoPath}" && git diff ${baseBranch}...${branchName}`; let diffContent: string; try { diffContent = execSync(gitDiffCmd, { encoding: 'utf-8' }); } catch (error) { throw new Error(`Git diff failed: ${error}`); } if (!diffContent.trim()) { console.log('No changes found in diff'); return; } console.log(`Analyzing ${diffContent.length} characters of diff`); // Create Metorial session with GitHub integration await metorial.withProviderSession( metorialAnthropic, { providers: [ { providerDeploymentId: GITHUB_DEPLOYMENT_ID, providerAuthConfigId: GITHUB_AUTH_CONFIG_ID } ] }, async session => { // Prepare review prompt with diff content const messages: Anthropic.MessageParam[] = [ { role: 'user', content: `You are an expert code reviewer. You MUST post a review to GitHub after analyzing the code. Repository: ${owner}/${repo} Branch: ${branchName} PR Number: #${prNumber} DIFF CONTENT: \`\`\`diff ${diffContent} \`\`\` CRITICAL: You MUST call the create_pull_request_review tool. Do NOT just provide analysis - you must POST the review to GitHub. INSTRUCTIONS: 1. Analyze the diff for: - Security vulnerabilities (SQL injection, XSS, exposed secrets, unsafe operations) - Code quality issues (duplication, complexity, error handling) - Style problems (naming, formatting, consistency) - Best practices (documentation, testing, edge cases) 2. IMMEDIATELY call create_pull_request_review tool with: - owner: "${owner}" - repo: "${repo}" - pull_number: ${prNumber} - body: Your summary of findings - event: "APPROVE" (if no issues) or "REQUEST_CHANGES" (if issues found) - comments: Array of line-specific comments if you found issues You MUST use the tool to post the review. START NOW.` } ]; // Send initial request to Claude let response = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 8192, tools: session.tools, messages }); // Handle tool calls in agentic loop while (response.stop_reason === 'tool_use') { const toolUseBlocks = response.content.filter( (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use' ); console.log(`Executing ${toolUseBlocks.length} tool(s)...`); // Execute tools via Metorial const toolResults = await session.callTools(toolUseBlocks); // Add assistant response and tool results to conversation messages.push({ role: 'assistant', content: response.content }); messages.push(toolResults); // Continue conversation response = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 8192, messages, tools: session.tools }); } // Get final summary const finalText = response.content .filter((block): block is Anthropic.TextBlock => block.type === 'text') .map(block => block.text) .join('\n'); console.log(`Review completed: ${finalText}`); } ); } ``` ```python import asyncio import subprocess from pathlib import Path from metorial import Metorial from anthropic import AsyncAnthropic # Initialize clients metorial = Metorial(api_key="YOUR_METORIAL_API_KEY") anthropic = AsyncAnthropic(api_key="YOUR_ANTHROPIC_API_KEY") # GitHub integration credentials GITHUB_DEPLOYMENT_ID = "GITHUB_DEPLOYMENT_ID" GITHUB_AUTH_CONFIG_ID = "GITHUB_AUTH_CONFIG_ID" async def review_pull_request( repo_path: str, owner: str, repo: str, branch_name: str, pr_number: int, base_branch: str = "main" ): """ Review a pull request by analyzing local git diff and posting to GitHub. Args: repo_path: Local path to the git repository owner: GitHub repository owner repo: GitHub repository name branch_name: Branch with changes to review pr_number: GitHub PR number base_branch: Base branch to compare against (default: "main") """ print(f"Starting review of PR #{pr_number} ({branch_name})") # Validate repository path repo_dir = Path(repo_path) if not repo_dir.exists(): raise ValueError(f"Repository path does not exist: {repo_path}") git_dir = repo_dir / ".git" if not git_dir.exists(): raise ValueError(f"Not a git repository: {repo_path}") # Get diff from local repository print(f"Fetching diff: {base_branch}...{branch_name}") git_diff_cmd = f"cd '{repo_path}' && git diff {base_branch}...{branch_name}" diff_result = subprocess.run(git_diff_cmd, shell=True, capture_output=True, text=True) if diff_result.returncode != 0: raise RuntimeError(f"Git diff failed: {diff_result.stderr}") diff_content = diff_result.stdout if not diff_content.strip(): print("No changes found in diff") return print(f"Analyzing {len(diff_content)} characters of diff") # Create Metorial session with GitHub integration async with metorial.provider_session( provider="anthropic", providers=[ { "provider_deployment_id": GITHUB_DEPLOYMENT_ID, "provider_auth_config_id": GITHUB_AUTH_CONFIG_ID } ], ) as session: # Prepare review prompt with diff content messages = [ { "role": "user", "content": f"""You are an expert code reviewer. You MUST post a review to GitHub after analyzing the code. Repository: {owner}/{repo} Branch: {branch_name} PR Number: #{pr_number} DIFF CONTENT: {diff_content} CRITICAL: You MUST call the create_pull_request_review tool. Do NOT just provide analysis - you must POST the review to GitHub. INSTRUCTIONS: 1. Analyze the diff for: - Security vulnerabilities (SQL injection, XSS, exposed secrets, unsafe operations) - Code quality issues (duplication, complexity, error handling) - Style problems (naming, formatting, consistency) - Best practices (documentation, testing, edge cases) 2. IMMEDIATELY call create_pull_request_review tool with: - owner: "{owner}" - repo: "{repo}" - pull_number: {pr_number} - body: Your summary of findings - event: "APPROVE" (if no issues) or "REQUEST_CHANGES" (if issues found) - comments: Array of line-specific comments if you found issues You MUST use the tool to post the review. START NOW.""" } ] # Send initial request to Claude response = await anthropic.messages.create( model="claude-sonnet-4-5", max_tokens=8192, tools=session.tools, messages=messages, ) # Handle tool calls in agentic loop while response.stop_reason == "tool_use": # Extract tool use blocks from response tool_use_blocks = [ block for block in response.content if block.type == "tool_use" ] print(f"Executing {len(tool_use_blocks)} tool(s)...") # Execute tools via Metorial tool_results = await session.call_tools(tool_use_blocks) # Add assistant response to conversation messages.append({"role": "assistant", "content": response.content}) # Format and add tool results to conversation formatted_results = [] for tool_block, result in zip(tool_use_blocks, tool_results): # Extract content from MCP-style or simple dict results if isinstance(result, dict) and "contents" in result: content = result["contents"][0]["text"] if result["contents"] else str(result) elif isinstance(result, dict) and "content" in result: content = result["content"] else: content = str(result) formatted_results.append({ "type": "tool_result", "tool_use_id": tool_block.id, "content": content }) messages.append({"role": "user", "content": formatted_results}) # Continue conversation response = await anthropic.messages.create( model="claude-sonnet-4-5", max_tokens=8192, tools=session.tools, messages=messages, ) # Extract final response final_text = "\n".join( block.text for block in response.content if block.type == "text" ) print(f"Review completed: {final_text}") ``` **What this code does:** 1. **Reads git diff locally** from your repository between the base and feature branches 2. **Creates a provider session** with GitHub provider for posting reviews 3. **Sends the diff to Claude** with analysis instructions 4. **AI analyzes the code** and identifies issues 5. **AI posts review to GitHub** using `create_pull_request_review` tool with findings 6. **Handles multi-step workflow** through agentic loop until review is complete 7. **Handles errors gracefully**: If tool calls fail, the AI receives error messages and can retry or adjust its approach This uses **Claude's agentic capabilities**—the AI decides which tools to call and when. You don't need to write explicit logic for fetching files, analyzing code, or posting comments. ## Step 4: Test with a security issue [#step-4-test-with-a-security-issue] Let's test the bot with a PR containing a security vulnerability. **Scenario**: Create a test PR with SQL injection vulnerability. **Test PR content** (example): ```javascript function getUserData(userId) { const query = `SELECT * FROM users WHERE id = ${userId}`; return database.query(query); } ``` **Run the bot**: TypeScript Python ```typescript reviewPullRequest( '/path/to/your/local/repo', 'your-username', 'your-repo', 'feature-branch', 123 ); ``` ```python asyncio.run(review_pull_request( repo_path="/path/to/your/local/repo", owner="your-username", repo="your-repo", branch_name="feature-branch", pr_number=123 )) ``` **Expected behavior**: 1. Bot reads git diff from local repository for PR #123 2. AI detects SQL injection vulnerability in the query string 3. Bot posts review with: * **General comment**: "Found 1 security vulnerability that needs immediate attention." * **Line-specific comment** on the SQL query line: "🚨 SQL injection vulnerability detected. User input is directly interpolated into the query. Use parameterized queries instead: `SELECT * FROM users WHERE id = ?` with bound parameters." 4. Bot submits review with **REQUEST\_CHANGES** status The bot workflow: 1. Reads git diff from your local repository 2. Sends diff content to AI for analysis 3. AI identifies the security issue in the code 4. AI calls `create_pull_request_review` tool to post review to GitHub with `REQUEST_CHANGES` status and detailed comments The AI autonomously analyzes code and posts reviews—no manual orchestration needed! ## Step 5: Test with clean code [#step-5-test-with-clean-code] Test the bot with a clean PR to verify the approval workflow. **Scenario**: PR with well-written code. **Test PR content** (example): ```typescript export function isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } export function sanitizeInput(input: string): string { return input .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } ``` **Run the bot**: TypeScript Python ```typescript reviewPullRequest( '/path/to/your/local/repo', 'your-username', 'your-repo', 'feature-branch', 124 ); ``` ```python asyncio.run(review_pull_request( repo_path="/path/to/your/local/repo", owner="your-username", repo="your-repo", branch_name="feature-branch", pr_number=124 )) ``` **Expected behavior**: 1. Bot reads git diff from local repository for PR #124 2. AI finds: * ✓ Proper documentation with JSDoc comments * ✓ Clear function names following conventions * ✓ Security-conscious implementation (XSS prevention) * ✓ No code smells or style violations 3. Bot posts review comment: "Code looks excellent! Clean implementation with proper documentation, security considerations, and clear naming. The XSS sanitization is thorough and the email validation regex is appropriate." 4. Bot submits review with **APPROVE** status ## Troubleshooting [#troubleshooting] Common issues and solutions when building your code review bot: **Possible causes**: * OAuth session expired or invalid * PR doesn't exist or has no changed files * AI model didn't call the review tools **Solutions**: 1. Verify your OAuth session is active: re-run the OAuth setup if needed 2. Check the PR exists and has commits: `gh pr view ` 3. Increase `max_tokens` in the AI request (try 8192 instead of 4096) 4. Review Metorial dashboard logs to see which tools were called 5. Ensure your prompt explicitly instructs the AI to post reviews (see code examples) **Possible causes**: * Incorrect tool name in code * GitHub provider deployment not active * OAuth permissions insufficient **Solutions**: 1. Verify you're using the correct tool name: `create_pull_request_review` 2. Check your GitHub server deployment is running in Metorial dashboard 3. Confirm OAuth session is associated with the correct deployment ID 4. Verify the `repo` scope was granted during OAuth authorization **Possible causes**: * AI prompt lacks specific guidelines * Code context is truncated * Model capabilities insufficient **Solutions**: 1. Add specific coding standards to your prompt (see "Custom Review Rules" in Advanced Customization) 2. Increase `max_tokens` to allow longer analysis (8192-16384 for large PRs) 3. Use Claude Sonnet 4 or newer for better code understanding 4. Provide example issues in the prompt to guide the AI's analysis style **Possible causes**: * Callback URL mismatch * Missing repository access * GitHub App permissions not configured **Solutions**: 1. Verify your callback URL matches exactly (including http/https) 2. Ensure you have admin access to the repository you're testing with 3. Check the OAuth authorization screen shows the `repo` scope 4. Try revoking and re-authorizing the OAuth connection 5. Confirm your Metorial account and GitHub account are properly linked **Possible causes**: * Too many changed files * AI context window exceeded * API rate limits **Solutions**: 1. Filter files by extension or directory (modify the prompt to focus on specific file types) 2. Implement batching: review files in chunks rather than all at once 3. Set a maximum file size limit (skip files >1000 lines) 4. Use streaming responses to handle longer processing times 5. For PRs with >20 files, consider the performance optimizations in Production Considerations **GitHub API limits**: 5,000 requests/hour for authenticated apps **Solutions**: 1. Implement exponential backoff when rate limit errors occur 2. Cache PR data when running multiple reviews on the same PR 3. Use conditional requests with ETags to avoid fetching unchanged data 4. For production deployment, consider GitHub Enterprise with higher limits 5. Track your usage in the Metorial dashboard to identify bottlenecks **Prevention**: Queue bot reviews rather than triggering all at once, especially during peak hours. If you encounter errors not covered here, check the Metorial dashboard logs (Monitoring section) to see detailed tool execution traces and error messages. You can also inspect the actual API requests being made. ## Advanced customization [#advanced-customization] Enhance your code review bot with these customizations: Add company-specific coding standards to the AI prompt (e.g., "all public functions must have JSDoc comments", "use async/await instead of promises"). Customize prompts for different languages: * Python: PEP 8 compliance, type hints * TypeScript: strict mode, interface usage * JavaScript: ESLint rules, modern syntax Categorize issues as CRITICAL, WARNING, or SUGGESTION and adjust review status accordingly. Only block PRs for critical security issues. Generate suggested code fixes for common issues. The AI can propose corrections in review comments (e.g., reformatted code, added error handling). **Example: Adding custom rules** Update the AI prompt with your standards: ```typescript const customPrompt = `You are an expert code reviewer following these standards: Company Coding Standards: - All functions must have JSDoc comments - Use async/await instead of .then() promises - Maximum function length: 50 lines - All API responses must include error handling - Never use "any" type in TypeScript Review pull request #${prNumber}...`; ``` ## Production considerations [#production-considerations] Before deploying to production: 1. **Webhook Integration**: Set up GitHub webhooks to trigger reviews automatically when PRs are opened or updated. You'll need the `admin:repo_hook` OAuth scope and a webhook endpoint that receives GitHub events. See GitHub's [Webhook documentation](https://docs.github.com/webhooks) for implementation details. 2. **Rate Limiting**: Implement rate limiting to avoid hitting GitHub API limits (5000 requests/hour for authenticated apps) 3. **Concurrency**: Queue reviews to handle multiple PRs simultaneously without overwhelming the AI API 4. **Error Handling**: Add try/catch blocks and retry logic for API failures 5. **Review History**: Store review results in a database for analytics and team insights 6. **Configurable Rules**: Allow teams to customize review criteria per repository via config files 7. **Cost Management**: Monitor AI API usage and token costs, especially for large PRs with many files 8. **Privacy**: Ensure sensitive code doesn't get logged or sent to unauthorized services **Performance Tip:** For large PRs (>20 files), consider: * Reviewing only changed lines instead of full files * Batching file reviews to reduce token usage * Implementing a maximum file size limit * Allowing users to request specific file reviews --- # HR interview coordinator URL: https://metorial.com/docs/build/samples/interview-coordinator-bot Learn how to build an AI agent that schedules interviews, sends professional emails, and coordinates with your team using Google Calendar and Gmail --- **What You'll Build**: An intelligent interview coordinator that automatically schedules interviews, sends confirmation emails to candidates, notifies your interview panel, and handles all follow-up communications. ## Introduction [#introduction] Coordinating interviews involves juggling multiple calendars, sending professional emails, and ensuring everyone has the right information at the right time. This tutorial shows you how to build an AI-powered interview coordinator that handles all of this automatically. The bot will: * Check calendar availability (using the authenticated user's calendar) * Create calendar events with multiple attendees * Send professional confirmation emails to candidates * Notify interview panel members with prep materials * Request feedback from interviewers after completion By the end of this tutorial, you'll have a working interview coordinator that can handle the entire scheduling and communication workflow with natural language commands. **What You'll Learn**: * Deploying Google Calendar and Gmail providers * Setting up OAuth for multiple Google services * Creating an AI agent that coordinates between multiple tools * Sending professional, AI-generated emails * Managing complex multi-step workflows with Claude ## Prerequisites [#prerequisites] Before starting, make sure you have: * **Metorial Account**: [Sign up](https://platform.metorial.com) and get your API key * **Google Workspace Account**: With Calendar and Gmail access * **Anthropic API Key**: Get one from [Anthropic Console](https://console.anthropic.com) * **Development Environment**: Node.js 18+ or Python 3.9+ ## Architecture overview [#architecture-overview] Here's how the interview coordinator works: 1. **User provides interview details** (candidate, role, date preferences, interviewers) 2. **AI checks calendar availability** using the Google Calendar provider (checks the authenticated user's calendar) 3. **AI creates calendar event** and adds interviewers as attendees 4. **AI sends confirmation email** to candidate via Gmail 5. **AI sends prep email** to interview panel via Gmail **Tools used**: Google Calendar provider (scheduling) + Gmail provider (communications) + AI Model (coordination) The AI autonomously decides which tools to use and in what order, handling complex multi-step workflows without explicit programming. ## Step 1: Configure Google Calendar provider [#step-1-configure-google-calendar-provider] First, configure the Google Calendar provider from the Metorial catalog: Go to [platform.metorial.com](https://platform.metorial.com) and click **Providers** Search for "Google Calendar" in the catalog Open the provider, click **Use Provider** → **Integration**, then note the provider deployment or integration ID shown in the dashboard. Save your Calendar deployment ID - you'll need it in Step 3. It looks like: `pdp_abc123def456` **Note on Multiple Interviewers**: This deployment accesses **one** Google Calendar (the authenticated user's calendar). To check availability across multiple interviewers' calendars, each interviewer would need their own Calendar provider deployment and auth config. For simplicity, this tutorial uses a single calendar instance. ## Step 1b: Configure Gmail provider [#step-1b-configure-gmail-provider] Next, configure the Gmail provider for email communications: In the same catalog, search for "Gmail" Open Gmail, click **Use Provider** → **Integration**, and note your Gmail provider deployment or integration ID. You now have two providers deployed. Both will run in the same session, allowing the AI to coordinate between scheduling and email tools seamlessly. ## Step 2: Set up OAuth authentication [#step-2-set-up-oauth-authentication] Both Calendar and Gmail require OAuth authentication to access your Google Workspace. TypeScript Python npm install metorial @metorial/anthropic @anthropic-ai/sdk pip install metorial anthropic ```typescript TypeScript import { Metorial } from "metorial"; const metorial = new Metorial({ apiKey: "YOUR_METORIAL_API_KEY", }); async function setupOAuth() { // Calendar OAuth const calendarSetup = await metorial.providers.setupSessions.create({ providerId: "YOUR_CALENDAR_PROVIDER_ID", providerAuthMethodId: "oauth", }); console.log("Authorize Calendar here:", calendarSetup.url); // Wait for authorization const calendarCompleted = await metorial.waitForSetupSession([calendarSetup]); console.log("✓ Calendar authorized!"); // Gmail OAuth const gmailSetup = await metorial.providers.setupSessions.create({ providerId: "YOUR_GMAIL_PROVIDER_ID", providerAuthMethodId: "oauth", }); console.log("\nAuthorize Gmail here:", gmailSetup.url); // Wait for authorization const gmailCompleted = await metorial.waitForSetupSession([gmailSetup]); console.log("✓ Gmail authorized!"); console.log("\nSave these auth config IDs for your bot:"); console.log("Calendar:", calendarCompleted.authConfig.id); console.log("Gmail:", gmailCompleted.authConfig.id); } setupOAuth(); ``` ```python Python from metorial import Metorial metorial = Metorial(api_key="YOUR_METORIAL_API_KEY") # Calendar OAuth calendar_setup = await metorial.providers.setup_sessions.create( provider_id="YOUR_CALENDAR_PROVIDER_ID", provider_auth_method_id="oauth", ) print("Authorize Calendar here:", calendar_setup.url) # Wait for authorization calendar_completed = await metorial.wait_for_setup_session(calendar_setup) print("✓ Calendar authorized!") # Gmail OAuth gmail_setup = await metorial.providers.setup_sessions.create( provider_id="YOUR_GMAIL_PROVIDER_ID", provider_auth_method_id="oauth", ) print("\nAuthorize Gmail here:", gmail_setup.url) # Wait for authorization gmail_completed = await metorial.wait_for_setup_session(gmail_setup) print("✓ Gmail authorized!") print("\nSave these auth config IDs for your bot:") print("Calendar:", calendar_completed.auth_config.id) print("Gmail:", gmail_completed.auth_config.id) ``` Visit both URLs in your browser and complete the Google OAuth flow for Calendar and Gmail access. The script will wait for you to authorize before continuing. The script outputs your auth config IDs (starting with `pac_`). Save these for use in Step 3. **OAuth Scopes**: Calendar needs `calendar.events` and `calendar.readonly`. Gmail needs `gmail.send` and `gmail.readonly`. These are configured automatically by Metorial. ## Step 3: Build the interview coordinator [#step-3-build-the-interview-coordinator] Now let's build the main coordinator that orchestrates interview scheduling and communications. TypeScript Python ```typescript import { Metorial } from "metorial"; import { metorialAnthropic } from "@metorial/anthropic"; import Anthropic from "@anthropic-ai/sdk"; // Initialize clients const metorial = new Metorial({ apiKey: "YOUR_METORIAL_API_KEY", }); const anthropic = new Anthropic({ apiKey: "YOUR_ANTHROPIC_API_KEY", }); // Server deployment credentials const CALENDAR_DEPLOYMENT_ID = "CALENDAR_DEPLOYMENT_ID"; const CALENDAR_AUTH_CONFIG_ID = "CALENDAR_AUTH_CONFIG_ID"; const GMAIL_DEPLOYMENT_ID = "GMAIL_DEPLOYMENT_ID"; const GMAIL_AUTH_CONFIG_ID = "GMAIL_AUTH_CONFIG_ID"; interface InterviewRequest { candidateName: string; candidateEmail: string; role: string; interviewers: string[]; // Email addresses durationMinutes: number; preferredDates: string[]; // ISO format dates interviewType: "technical" | "behavioral" | "panel"; } async function scheduleInterview(request: InterviewRequest) { console.log(`Scheduling ${request.interviewType} interview for ${request.candidateName}...`); await metorial.withProviderSession( metorialAnthropic, { providers: [ { providerDeploymentId: CALENDAR_DEPLOYMENT_ID, providerAuthConfigId: CALENDAR_AUTH_CONFIG_ID, }, { providerDeploymentId: GMAIL_DEPLOYMENT_ID, providerAuthConfigId: GMAIL_AUTH_CONFIG_ID, }, ], }, async session => { const messages: Anthropic.MessageParam[] = [ { role: "user", content: `Schedule interview and send confirmations: Candidate: ${request.candidateName} (${request.candidateEmail}) Role: ${request.role} Interviewers: ${request.interviewers.join(", ")} Duration: ${request.durationMinutes} minutes Preferred dates: ${request.preferredDates.join(", ")} Type: ${request.interviewType} Steps to complete: 1. Check calendar availability for all interviewers on preferred dates 2. Create calendar event with: - Title: "${request.role} Interview - ${request.candidateName}" - Add all interviewers as attendees 3. Send confirmation email to candidate (${request.candidateEmail}) with: - Subject: "Interview Scheduled - ${request.role}" - Professional email including: interview date/time, what to prepare, who they'll meet 4. Send prep email to interviewers (${request.interviewers.join(", ")}) with: - Subject: "Interview Prep - ${request.candidateName}" - Candidate background, interview format, evaluation guidelines CRITICAL: Only use actual calendar data. If no slots available, inform user. DO NOT create events without checking availability first.`, }, ]; let response = await anthropic.messages.create({ model: "claude-sonnet-4-5", max_tokens: 8192, tools: session.tools, messages: messages, }); // Agentic loop - let AI use tools autonomously while (response.stop_reason === "tool_use") { const toolUseBlocks = response.content.filter( (block): block is Anthropic.ToolUseBlock => block.type === "tool_use" ); console.log(`AI using ${toolUseBlocks.length} tool(s)...`); const toolResults = await session.callTools(toolUseBlocks); messages.push({ role: "assistant", content: response.content }); messages.push(toolResults); response = await anthropic.messages.create({ model: "claude-sonnet-4-5", max_tokens: 8192, tools: session.tools, messages: messages, }); } // Extract final response const finalText = response.content .filter((block): block is Anthropic.TextBlock => block.type === "text") .map((block) => block.text) .join("\n"); console.log("Interview coordination complete:", finalText); } ); } // Example usage scheduleInterview({ candidateName: "John Novak", candidateEmail: "john.novak@sample.com", role: "Senior Software Engineer", interviewers: [ "user@yourCompany.com", // Hiring manager ], durationMinutes: 45, preferredDates: [ "2026-02-7T14:00:00Z", // First preference "2026-02-7T5:00:00Z", // Second preference "2026-02-7T15:00:00Z", // Third preference ], interviewType: "technical", }); ``` ```python import asyncio from metorial import Metorial from anthropic import AsyncAnthropic from typing import List, Literal # Initialize clients metorial = Metorial(api_key="YOUR_METORIAL_API_KEY") anthropic = AsyncAnthropic(api_key="YOUR_ANTHROPIC_API_KEY") # Server deployment credentials CALENDAR_DEPLOYMENT_ID = "YOUR_CALENDAR_DEPLOYMENT_ID" CALENDAR_AUTH_CONFIG_ID = "YOUR_CALENDAR_AUTH_CONFIG_ID" GMAIL_DEPLOYMENT_ID = "YOUR_GMAIL_DEPLOYMENT_ID" GMAIL_AUTH_CONFIG_ID = "YOUR_GMAIL_AUTH_CONFIG_ID" async def schedule_interview( candidate_name: str, candidate_email: str, role: str, interviewers: List[str], duration_minutes: int, preferred_dates: List[str], interview_type: Literal["technical", "behavioral", "panel"] ): print(f"Scheduling {interview_type} interview for {candidate_name}...") async with metorial.provider_session( provider="anthropic", providers=[ { "provider_deployment_id": CALENDAR_DEPLOYMENT_ID, "provider_auth_config_id": CALENDAR_AUTH_CONFIG_ID }, { "provider_deployment_id": GMAIL_DEPLOYMENT_ID, "provider_auth_config_id": GMAIL_AUTH_CONFIG_ID } ], ) as session: messages = [ { "role": "user", "content": f"""Schedule interview and send confirmations: Candidate: {candidate_name} ({candidate_email}) Role: {role} Interviewers: {", ".join(interviewers)} Duration: {duration_minutes} minutes Preferred dates: {", ".join(preferred_dates)} Type: {interview_type} Steps to complete: 1. Check calendar availability for all interviewers on preferred dates 2. Create calendar event with: - Title: "{role} Interview - {candidate_name}" - Add all interviewers as attendees 3. Send confirmation email to candidate ({candidate_email}) with: - Subject: "Interview Scheduled - {role}" - Professional email including: interview date/time, what to prepare, who they'll meet 4. Send prep email to interviewers ({", ".join(interviewers)}) with: - Subject: "Interview Prep - {candidate_name}" - Candidate background, interview format, evaluation guidelines CRITICAL: Only use actual calendar data. If no slots available, inform user. DO NOT create events without checking availability first.""" } ] response = await anthropic.messages.create( model="claude-sonnet-4-5", max_tokens=8192, tools=session.tools, messages=messages, ) # Agentic loop - let AI use tools autonomously while response.stop_reason == "tool_use": tool_use_blocks = [ block for block in response.content if block.type == "tool_use" ] print(f"AI using {len(tool_use_blocks)} tool(s)...") tool_results = await session.call_tools(tool_use_blocks) messages.append({"role": "assistant", "content": response.content}) messages.append(tool_results) response = await anthropic.messages.create( model="claude-sonnet-4-5", max_tokens=8192, tools=session.tools, messages=messages, ) # Extract final response final_text = "\n".join( block.text for block in response.content if block.type == "text" ) print(f"Interview coordination complete: {final_text}") # Example usage asyncio.run(schedule_interview( candidate_name="John Novak", candidate_email="john.novak@sample.com", role="Senior Software Engineer", interviewers=[ "user@yourCompany.com", # Hiring manager ], duration_minutes=45, preferred_dates=[ "2026-02-7T14:00:00Z", # First preference "2026-02-7T5:00:00Z", # Second preference "2026-02-7T15:00:00Z", # Third preference ], interview_type="technical" )) ``` **Key Implementation Details**: * **Single Session**: Both Calendar and Gmail servers run in one session, allowing the AI to seamlessly switch between scheduling and emailing tools * **Agentic Workflow**: The AI autonomously decides which tools to use and in what order - no explicit programming needed * **Anti-Hallucination**: The prompt explicitly instructs using only actual calendar data to prevent AI from making up availability * **Professional Communications**: AI generates context-appropriate emails for candidates and interviewers **Multiple Interviewer Calendars**: The current implementation uses a single Google Calendar provider instance, which only accesses **one** Google Calendar (the calendar of the authenticated user). To check availability across **multiple interviewers' calendars**, you would need to: * Deploy a separate Calendar provider instance for each interviewer * Set up OAuth for each interviewer's calendar * Pass multiple calendar provider deployments to the session For now, this tutorial demonstrates scheduling with a single calendar owner who can view their own availability. The `interviewers` array is used for adding attendees to the calendar event and sending prep emails, but **not** for checking their individual calendar availability. ## Email templates [#email-templates] The AI automatically generates professional emails tailored to each recipient. Here are examples of what the coordinator sends: **Subject**: Interview Scheduled - Senior Software Engineer **Body**: ``` Hi Sarah, Great news! We've scheduled your interview for the Senior Software Engineer position. Interview Details: - Date: Tuesday, February 10, 2026 - Time: 2:00 PM - 3:00 PM EST - Duration: 60 minutes - Format: Technical Interview You'll be meeting with: - Alex Chen, Hiring Manager - Jordan Smith, Technical Lead What to Prepare: - Please have your development environment ready for a live coding session - We'll be discussing system design and your recent project experience - Feel free to ask questions about our team and tech stack If you need to reschedule, please let us know as soon as possible. Looking forward to speaking with you! Best regards, Hiring Team ``` **Subject**: Interview Prep - Sarah Johnson **Body**: ``` Hi Team, You have an upcoming interview scheduled: Candidate: Sarah Johnson Position: Senior Software Engineer Interview Type: Technical Date: Tuesday, February 10, 2026 at 2:00 PM EST Duration: 60 minutes Interview Format: - First 10 minutes: Introductions and company overview - Next 35 minutes: Technical assessment (live coding + system design) - Final 15 minutes: Candidate questions Evaluation Focus: - Problem-solving approach and code quality - System design thinking - Communication and collaboration style - Cultural fit and team dynamics Please review the candidate's background before the interview and come prepared with your assessment criteria. If you need to reschedule, please notify the team immediately. Thanks, Recruiting Team ``` ## Step 4: Test the coordinator [#step-4-test-the-coordinator] Let's test the coordinator with a realistic scenario: TypeScript Python ```typescript // Test scenario: Schedule technical interview await scheduleInterview({ candidateName: "Alex Rivera", candidateEmail: "alex.rivera@example.com", role: "Staff Frontend Engineer", interviewers: [ "engineering.director@company.com", "senior.engineer@company.com", "product.manager@company.com", ], durationMinutes: 90, // Panel interview preferredDates: [ "2026-02-15T13:00:00Z", "2026-02-16T14:00:00Z", "2026-02-17T10:00:00Z", ], interviewType: "panel", }); ``` ```python # Test scenario: Schedule technical interview asyncio.run(schedule_interview( candidate_name="Alex Rivera", candidate_email="alex.rivera@example.com", role="Staff Frontend Engineer", interviewers=[ "engineering.director@company.com", "senior.engineer@company.com", "product.manager@company.com", ], duration_minutes=90, # Panel interview preferred_dates=[ "2026-02-15T13:00:00Z", "2026-02-16T14:00:00Z", "2026-02-17T10:00:00Z", ], interview_type="panel" )) ``` **Expected Output**: ``` Scheduling panel interview for Alex Rivera... AI using 3 tool(s)... AI using 2 tool(s)... AI using 2 tool(s)... Interview coordination complete: Successfully scheduled panel interview for Alex Rivera on February 15, 2026 at 1:00 PM EST. Calendar event created. Confirmation email sent to candidate and prep email sent to all 3 interviewers (Engineering Director, Senior Engineer, Product Manager). ``` The coordinator will: 1. Check availability on the authenticated user's calendar (not all 3 interviewers - see note below) 2. Find the first available slot (Feb 15 at 1:00 PM) 3. Create a 90-minute calendar event with all 3 interviewers as attendees 4. Send confirmation email to Alex with interview details 5. Send prep email to all interviewers with candidate info **Calendar Availability Limitation**: This example checks only the authenticated user's calendar availability. To check all 3 interviewers' calendars, you would need to deploy a separate Calendar MCP instance for each interviewer with their OAuth credentials. The interviewers array is used for adding attendees to the event and sending emails, not for checking their individual availability. ## Troubleshooting [#troubleshooting] **Possible causes**: * OAuth session expired or invalid * Calendar API not enabled in Google Workspace * Insufficient permissions (need `calendar.events` scope) * No availability found in provided date range **Solutions**: * Re-run OAuth setup to refresh credentials * Verify Calendar API is enabled at [Google Cloud Console](https://console.cloud.google.com) * Check OAuth session has correct scopes in Metorial dashboard * Expand preferred date range or reduce number of required attendees **Possible causes**: * OAuth session expired for Gmail * Gmail API not enabled * Insufficient permissions (need `gmail.send` scope) * Daily sending limit reached (500 emails/day for standard accounts) **Solutions**: * Re-run OAuth setup for Gmail specifically * Enable Gmail API in Google Cloud Console * Verify OAuth session includes `gmail.send` scope * Check your Gmail sending quota at Google Workspace Admin **Possible causes**: * Sending from unverified domain * Email content triggers spam filters * High volume of emails in short period **Solutions**: * Set up SPF and DKIM records for your sending domain * Use professional, well-formatted email templates * Add delays between bulk sends * Test email content with spam checking tools * Whitelist your sending address with recipients **Issue**: Calendar event created despite conflicts **Solution**: This is likely a prompt issue. Ensure your prompt includes: ``` CRITICAL: Only use actual calendar data. If no slots available, inform user. DO NOT create events without checking availability first. ``` The anti-hallucination instruction forces the AI to actually check calendars before proceeding. **Issue**: Extra or missing attendees **Solution**: Be explicit in your prompt about who should attend: ``` Add ALL these interviewers as attendees: {interviewers.join(", ")} Do not add anyone else as an attendee. ``` Also verify the email addresses are correct in your request object. **Issue**: Need to verify availability across multiple interviewers' calendars **Current Limitation**: A single Google Calendar provider instance can only access one calendar (the authenticated user's calendar). **Solution**: To check availability across multiple interviewers: 1. Deploy a separate Calendar provider instance for each interviewer 2. Set up individual OAuth sessions for each interviewer's calendar 3. Pass all calendar deployments to `providers` array in your session 4. Update your prompt to check all calendar instances before scheduling Example with multiple calendars: ```typescript providers: [ { providerDeploymentId: CALENDAR_1_ID, providerAuthConfigId: AUTH_CONFIG_1_ID }, { providerDeploymentId: CALENDAR_2_ID, providerAuthConfigId: AUTH_CONFIG_2_ID }, { providerDeploymentId: GMAIL_DEPLOYMENT_ID, providerAuthConfigId: GMAIL_AUTH_CONFIG_ID } ] ``` For this tutorial, we use a single calendar and add interviewers as attendees without checking their availability. ## Advanced customization [#advanced-customization] Customize email templates based on role, interview type, or company culture: ```typescript const emailTemplates = { technical: "Include coding prep tips", behavioral: "Focus on company values", panel: "List all interviewers with bios" }; ``` Pass templates to your prompt for context-aware emails. Coordinate complex interview pipelines: ```typescript async function scheduleInterviewPipeline( candidate: Candidate, stages: InterviewStage[] ) { for (const stage of stages) { await scheduleInterview({ ...candidate, ...stage }); } } ``` Schedule phone screen, technical, and panel interviews in sequence. Request and aggregate interviewer feedback: ```typescript async function collectFeedback( interviewId: string, interviewers: string[] ) { // Send feedback forms after interview // AI analyzes responses for hiring decision } ``` Automate post-interview feedback workflow. Respect interviewer working hours and preferences: ```typescript const preferences = { "eng.dir@co.com": { hours: "9-17", timezone: "America/New_York" } }; ``` Include preferences in AI prompt for smarter scheduling. Send emails in candidate's preferred language: ```typescript const request = { ...interviewDetails, candidateLanguage: "es" // Spanish }; ``` AI automatically generates emails in requested language. Check availability across multiple interviewers' calendars: ```typescript // Deploy separate Calendar MCP for each interviewer const INTERVIEWER_CALENDARS = [ { providerDeploymentId: "pdp_eng_dir", providerAuthConfigId: "pac_eng_dir" }, { providerDeploymentId: "pdp_senior_eng", providerAuthConfigId: "pac_senior" } ]; ``` Each interviewer needs their own Calendar MCP instance with OAuth. ## Production considerations [#production-considerations] Before deploying to production: ### Error handling [#error-handling] Add comprehensive error handling for common failures: ```typescript try { await scheduleInterview(request); } catch (error) { if (error.message.includes("OAuth")) { // Refresh OAuth tokens } else if (error.message.includes("quota")) { // Handle rate limits } else { // Log and alert on-call team } } ``` ### Monitoring and logging [#monitoring-and-logging] Track key metrics for reliability: * Interview scheduling success rate * Email delivery rate * Calendar availability check duration * AI tool call patterns Use Metorial's built-in monitoring to track these metrics. ### Testing strategy [#testing-strategy] Test edge cases before production: * All interviewers unavailable on all dates * Invalid email addresses * Time zone edge cases (DST transitions) * Calendar quota limits * Gmail sending limits ### Privacy and compliance [#privacy-and-compliance] Ensure GDPR/CCPA compliance: * Get consent before sending emails * Don't store candidate data longer than necessary * Encrypt sensitive information in transit and at rest ### Scalability [#scalability] For high-volume hiring: * Implement request queuing to avoid rate limits * Cache calendar availability queries * Batch similar operations * Use Metorial's webhook support for async processing ## Related sample projects [#related-sample-projects] Build an AI code reviewer that posts comments on pull requests Create a bot that collects and summarizes team standup updates --- # PM Slack standup bot URL: https://metorial.com/docs/build/samples/slack-standup-bot Create an AI-powered bot that collects daily standup responses, analyzes team progress, and posts executive summaries --- Build an AI-powered Slack standup bot that posts daily standup prompts to your team channel, collects responses in organized threads, analyzes progress and blockers, and posts executive summaries to leadership channels — tracking recurring blockers and patterns over time. * Configuring the Slack provider * Setting up OAuth for Slack workspace access * Creating an AI agent that processes team updates * Posting and reading Slack messages programmatically * [Review Workforce concepts](/docs/platform/get-started/core-concepts) * [Create API keys](/docs/build/api) * Slack workspace with admin access * Anthropic API key (Claude Sonnet 4 or newer recommended) **Time to complete:** 10-15 minutes ## Prerequisites [#prerequisites] Before building the standup bot, ensure you have: 1. **Metorial setup**: * Active Metorial account at [platform.metorial.com](https://platform.metorial.com) * Project created in your organization * Metorial API key (generate in Dashboard → Developer → API Keys) 2. **Slack workspace**: * Admin access to install apps * Channel where standups will be posted * Leadership/executive channel for summaries (optional) 3. **AI provider**: * Anthropic API key (Claude Sonnet 4 or newer recommended for analysis) 4. **Development environment**: * Node.js 18+ (TypeScript) or Python 3.9+ installed * Basic knowledge of async/await patterns ## Architecture overview [#architecture-overview] The standup bot workflow: 1. **Trigger**: Bot posts standup prompt to team channel (scheduled or manual) 2. **Collection**: Team members reply in thread with their updates 3. **Analysis**: AI analyzes all responses for: * Individual progress and accomplishments * Blockers and dependencies between team members * Team sentiment and morale * Recurring issues 4. **Summary**: Bot generates and posts executive summary to leadership channel 5. **Storage**: Optionally stores historical data for trend analysis **Tools used**: Slack provider (post messages, read threads) + AI Model (analysis and summarization) ## Step 1: Configure Slack provider [#step-1-configure-slack-provider] Configure the Slack provider from Metorial's catalog to enable your bot to interact with Slack. In the Metorial Dashboard, go to **Providers** and search for "Slack". Click the **Slack** provider, then click **Use Provider** → **Integration**. Choose the Slack auth method, create or select auth credentials, and review tool filters for message and channel access. After setup, copy the provider deployment or integration ID shown in the dashboard. You'll need this for OAuth setup and in your bot code. Save your Slack deployment ID—you'll need it for OAuth setup (Step 2) and in your bot code (Step 3). ## Step 2: Set up OAuth authentication [#step-2-set-up-oauth-authentication] Your standup bot needs permission to post messages and read thread replies in your Slack workspace. Install the Metorial SDK and Anthropic: TypeScript Python npm install metorial @metorial/anthropic @anthropic-ai/sdk pip install metorial anthropic Run this code to generate the Slack OAuth URL: ```typescript TypeScript import { Metorial } from 'metorial'; const metorial = new Metorial({ apiKey: "YOUR-METORIAL-API-KEY" }); async function setupSlackOAuth() { const slackSetup = await metorial.providers.setupSessions.create({ providerId: 'YOUR-SLACK-PROVIDER-ID', providerAuthMethodId: 'oauth' }); console.log('Authorize Slack here:', slackSetup.url); // Wait for authorization const completed = await metorial.waitForSetupSession([slackSetup]); console.log('✓ Slack authorized!'); // Save the auth config ID for future use return completed.authConfig.id; } setupSlackOAuth(); ``` ```python Python import asyncio from metorial import Metorial async def setup_slack_oauth(): metorial = Metorial(api_key="YOUR-METORIAL-API-KEY") slack_setup = await metorial.providers.setup_sessions.create( provider_id="YOUR-SLACK-PROVIDER-ID", provider_auth_method_id="oauth" ) print(f"Authorize Slack here: {slack_setup.url}") # Wait for authorization completed = await metorial.wait_for_setup_session(slack_setup) print("✓ Slack authorized!") # Save the auth config ID for future use return completed.auth_config.id asyncio.run(setup_slack_oauth()) ``` 1. Open the printed OAuth URL in your browser 2. Sign in to Slack if needed 3. Review and approve the permissions (the bot needs to post messages and read channels) 4. You'll be redirected to your callback URL (or see a confirmation page) Save the auth config ID securely. You'll reuse it for all future bot operations without re-authorizing. For production apps, store auth config IDs in your database or environment variables. **Required OAuth Scopes:** The Slack provider requires these scopes: * `chat:write` - Post messages to channels * `channels:read` - Read public channel information * `channels:history` - Read message history to collect thread replies * `users:read` - Get user information for mentions The required scopes are automatically requested when you authorize via the OAuth URL. ## Step 3: Build the standup bot [#step-3-build-the-standup-bot] Create the main bot that collects standup responses and generates summaries. TypeScript Python ```typescript import { Metorial } from 'metorial'; import { metorialAnthropic } from '@metorial/anthropic'; import Anthropic from '@anthropic-ai/sdk'; const metorial = new Metorial({ apiKey: "YOUR-METORIAL-API-KEY" }); const anthropic = new Anthropic({ apiKey: "YOUR_ANTHROPIC_API_KEY" }); // Slack integration credentials const SLACK_DEPLOYMENT_ID = "YOUR_SLACK_DEPLOYMENT_ID"; const SLACK_AUTH_CONFIG_ID = "YOUR_SLACK_AUTH_CONFIG_ID"; async function runStandup( channelId: string, summaryChannelId: string, collectionTimeMinutes: number = 5 ) { console.log(`Starting standup in channel ${channelId}`); // Post standup prompt const standupPrompt = `Good morning team! Time for daily standup. Please reply to this thread with: 1. What you accomplished yesterday 2. What you're working on today 3. Any blockers or help needed You have ${collectionTimeMinutes} minutes to respond.`; await metorial.withProviderSession( metorialAnthropic, { providers: [ { providerDeploymentId: SLACK_DEPLOYMENT_ID, providerAuthConfigId: SLACK_AUTH_CONFIG_ID } ] }, async session => { // Post the standup prompt const messages: Anthropic.MessageParam[] = [ { role: 'user', content: `Post a message to Slack channel ${channelId} with this text: "${standupPrompt}" Use the chat_postMessage tool.` } ]; console.log('Posting standup prompt...'); let response = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 4096, tools: session.tools, messages }); // Handle tool calls while (response.stop_reason === 'tool_use') { const toolUseBlocks = response.content.filter( (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use' ); const toolResults = await session.callTools(toolUseBlocks); messages.push({ role: 'assistant', content: response.content }); messages.push(toolResults); response = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 4096, messages, tools: session.tools }); } console.log('Standup prompt posted successfully!'); console.log(`Waiting ${collectionTimeMinutes} minutes for responses...`); // Wait for responses await new Promise(resolve => setTimeout(resolve, collectionTimeMinutes * 60 * 1000)); console.log('Collecting and analyzing responses...'); messages.push({ role: 'user', content: `Collect standup responses and post summary: 1. List recent messages in channel ${channelId} to find the standup prompt 2. Get thread replies for that message 3. Analyze ONLY actual user responses (skip bot's prompt) 4. Post executive summary to ${summaryChannelId} Summary format: - Team Progress: Actual accomplishments mentioned - Today's Focus: Actual plans shared - Blockers: Actual issues raised - Action Items: Specific help requested CRITICAL: Base summary ONLY on real messages. If no responses, say "No responses received." DO NOT invent content.` }); response = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 8192, tools: session.tools, messages }); // Agentic loop while (response.stop_reason === 'tool_use') { const toolUseBlocks = response.content.filter( (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use' ); console.log(`Executing ${toolUseBlocks.length} tool(s)...`); const toolResults = await session.callTools(toolUseBlocks); messages.push({ role: 'assistant', content: response.content }); messages.push(toolResults); response = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 8192, messages, tools: session.tools }); } // Get final summary const finalText = response.content .filter((block): block is Anthropic.TextBlock => block.type === 'text') .map(block => block.text) .join('\n'); console.log(`Standup complete: ${finalText}`); } ); } // Example usage runStandup( 'C1234567890', // Your team channel ID 'C0987654321', // Your executive channel ID 5 // Minutes to wait for responses ); ``` ```python import asyncio from metorial import Metorial from anthropic import AsyncAnthropic # Initialize clients metorial = Metorial(api_key="YOUR-METORIAL-API-KEY") anthropic = AsyncAnthropic(api_key="YOUR-ANTHROPIC-API-KEY") # Slack integration credentials SLACK_DEPLOYMENT_ID = "SLACK_DEPLOYMENT_ID" SLACK_AUTH_CONFIG_ID = "SLACK_AUTH_CONFIG_ID" async def run_standup( channel_id: str, summary_channel_id: str, collection_time_minutes: int = 5 ): print(f"Starting standup in channel {channel_id}") # Post standup prompt standup_prompt = f"""Good morning team! Time for daily standup. Please reply to this thread with: 1. What you accomplished yesterday 2. What you're working on today 3. Any blockers or help needed You have {collection_time_minutes} minutes to respond.""" async with metorial.provider_session( provider="anthropic", providers=[ { "provider_deployment_id": SLACK_DEPLOYMENT_ID, "provider_auth_config_id": SLACK_AUTH_CONFIG_ID } ], ) as session: # Post the standup prompt messages = [ { "role": "user", "content": f"""Post a message to Slack channel {channel_id} with this text: "{standup_prompt}" Use the chat_postMessage tool.""" } ] print("Posting standup prompt...") response = await anthropic.messages.create( model="claude-sonnet-4-5", max_tokens=4096, tools=session.tools, messages=messages, ) # Handle tool calls while response.stop_reason == "tool_use": tool_use_blocks = [ block for block in response.content if block.type == "tool_use" ] tool_results = await session.call_tools(tool_use_blocks) messages.append({"role": "assistant", "content": response.content}) messages.append(tool_results) response = await anthropic.messages.create( model="claude-sonnet-4-5", max_tokens=4096, tools=session.tools, messages=messages, ) print(f"Standup prompt posted successfully!") print(f"Waiting {collection_time_minutes} minutes for responses...") # Wait for responses await asyncio.sleep(collection_time_minutes * 60) print("Collecting and analyzing responses...") messages.append({ "role": "user", "content": f"""Collect standup responses and post summary: 1. List recent messages in channel {channel_id} to find the standup prompt 2. Get thread replies for that message 3. Analyze ONLY actual user responses (skip bot's prompt) 4. Post executive summary to {summary_channel_id} Summary format: - Team Progress: Actual accomplishments mentioned - Today's Focus: Actual plans shared - Blockers: Actual issues raised - Action Items: Specific help requested CRITICAL: Base summary ONLY on real messages. If no responses, say "No responses received." DO NOT invent content.""" }) response = await anthropic.messages.create( model="claude-sonnet-4-5", max_tokens=8192, tools=session.tools, messages=messages, ) # Agentic loop while response.stop_reason == "tool_use": tool_use_blocks = [ block for block in response.content if block.type == "tool_use" ] print(f"Executing {len(tool_use_blocks)} tool(s)...") tool_results = await session.call_tools(tool_use_blocks) messages.append({"role": "assistant", "content": response.content}) messages.append(tool_results) response = await anthropic.messages.create( model="claude-sonnet-4-5", max_tokens=8192, tools=session.tools, messages=messages, ) # Get final summary final_text = "\n".join( block.text for block in response.content if block.type == "text" ) print(f"Standup complete: {final_text}") # Example usage asyncio.run(run_standup( channel_id="C1234567890", # Your team channel ID summary_channel_id="C0987654321", # Your executive channel ID collection_time_minutes=5 # Minutes to wait for responses )) ``` **What this code does:** 1. **Posts standup prompt** to the team channel with clear instructions 2. **Waits for responses** (configurable time) 3. **Collects all thread replies** using the Slack provider 4. **AI analyzes responses** for progress, blockers, and dependencies 5. **Generates executive summary** with key insights 6. **Posts summary** to leadership channel 7. **Uses agentic workflow** - AI decides which Slack tools to call and when This uses **Claude's agentic capabilities**—the AI decides when to read the thread, how to analyze the data, and when to post the summary. You don't need to write explicit logic for parsing responses or formatting summaries. ## Step 4: Test the bot [#step-4-test-the-bot] Let's test the bot with example standup responses. **Scenario**: Run standup in a test channel with your team. **Example responses**: ``` @alice: Yesterday finished the user auth feature. Today working on password reset. No blockers. @bob: Completed API endpoints for /users. Today starting the /products endpoints. Blocked on database schema approval. @charlie: Fixed 5 bugs in the dashboard. Today continuing bug fixes. Could use help reviewing PR #234. ``` **Run the bot**: TypeScript Python ```typescript runStandup( 'C1234567890', // Team channel ID 'C0987654321', // Executive channel ID 5 // Wait 5 minutes ); ``` ```python asyncio.run(run_standup( channel_id="C1234567890", # Your team channel ID summary_channel_id="C0987654321", # Your executive channel ID collection_time_minutes=5 # Minutes to wait for responses )) ``` **Expected behavior**: 1. Bot posts standup prompt to team channel 2. Team members reply in thread 3. After 5 minutes, bot collects all responses 4. AI analyzes and generates summary: ``` 📊 Daily Standup Summary - [Date] ✅ Team Progress (Yesterday): • Alice: Completed user authentication feature • Bob: Finished API endpoints for /users module • Charlie: Resolved 5 dashboard bugs 🎯 Today's Focus: • Alice: Password reset functionality • Bob: /products API endpoints • Charlie: Continue bug fixing 🚧 Blockers & Dependencies: • Bob: Waiting on database schema approval (blocking /products work) • Charlie: Needs code review on PR #234 📈 Team Health: Positive momentum, steady progress ⚠️ Action Items: 1. Expedite database schema approval for Bob 2. Assign reviewer to PR #234 for Charlie ``` 5. Summary is posted to executive channel ## Troubleshooting [#troubleshooting] Common issues and solutions when building your standup bot: **Possible causes**: * OAuth session expired or invalid * Incorrect channel ID * Bot doesn't have permission to post in channel **Solutions**: 1. Verify your OAuth session is active: re-run the OAuth setup if needed 2. Check the channel ID is correct (not channel name) 3. Ensure the bot is invited to the channel: `/invite @YourBot` 4. Verify OAuth scopes include `chat:write` 5. Check Metorial dashboard logs for API errors **Possible causes**: * Missing `channels:history` scope * Incorrect thread timestamp * Bot not in channel **Solutions**: 1. Verify OAuth scopes include `channels:history` 2. Check the thread\_ts value matches the original message 3. Ensure bot is member of the channel 4. Try in a public channel first (private channels need additional setup) **Possible causes**: * Prompt lacks specific instructions * Not enough context provided * Model running out of tokens **Solutions**: 1. Add more specific analysis guidelines to the prompt 2. Increase `max_tokens` to 8192 or higher 3. Use Claude Sonnet 4 or newer for better understanding 4. Provide example summaries in the prompt 5. Include team-specific context (project names, terminology) **Possible causes**: * Not a Slack admin * Callback URL mismatch * App not approved for workspace **Solutions**: 1. Ensure you have admin privileges in Slack workspace 2. Verify callback URL matches in Metorial dashboard 3. Check workspace settings allow app installations 4. Try revoking and re-authorizing 5. Contact your Slack workspace admin if permissions are restricted **Possible causes**: * Collection time too short * Wrong thread timestamp * API rate limiting **Solutions**: 1. Increase `collection_time_minutes` to give team more time 2. Verify the thread\_ts is correct 3. Check Slack API rate limits (50+ requests per minute) 4. Add error handling to retry failed API calls 5. Log the thread\_ts immediately after posting to debug **Slack API limits**: * Tier 1 methods: 1 request per minute * Tier 2 methods: 20 requests per minute * Tier 3 methods: 50 requests per minute **Solutions**: 1. Implement exponential backoff for rate limit errors 2. Cache channel and user information 3. Batch operations when possible 4. Use Slack's rate limit headers to track usage 5. For high-frequency bots, consider Slack's paid tiers If you encounter errors not covered here, check the Metorial dashboard logs (Monitoring section) to see detailed tool execution traces and Slack API responses. ## Advanced customization [#advanced-customization] Enhance your standup bot with these customizations: Customize standup questions for your team's needs (e.g., "What are you learning today?", "Team shoutouts", "Health check: 1-5"). Run standups across multiple teams with different channels and schedules. Store team configs in a database. Store historical standup data to track: * Recurring blockers * Team velocity trends * Common challenges Generate weekly/monthly reports Send DMs to team members who haven't responded. Use Slack's users.list to track participation rates. Connect with project management tools (Linear, Jira) to: * Link updates to specific tasks * Auto-update task status * Cross-reference blockers with tickets Track team morale over time by analyzing sentiment in standup responses. Alert leadership to significant drops. **Example: Custom questions** Update the standup prompt: ```typescript const standupPrompt = `Good morning team! 🌅 Daily standup time: 1. 🎯 What are you working on today? 2. 🚧 Any blockers? 3. 💡 What are you learning? 4. 🙌 Team shoutouts (optional) 5. ❤️ Health check: 1-5 (1=struggling, 5=great) Reply in thread, you have ${collectionTimeMinutes} minutes.`; ``` ## Production considerations [#production-considerations] Before deploying to production: 1. **Scheduling**: Set up daily automated runs: * Use cron jobs or cloud schedulers (AWS EventBridge, GCP Cloud Scheduler) * Typical schedule: 9:00 AM team local time, Monday-Friday * Consider time zones for distributed teams 2. **Error Handling**: Add robust error handling: * Retry failed Slack API calls with exponential backoff * Alert admins if standup fails to post or collect responses * Handle missing or malformed responses gracefully 3. **Non-Responders**: Send reminders: * Track who responded vs. who didn't * Send DM reminders 2-3 minutes before deadline * Include non-responder list in summary for follow-up 4. **Data Storage**: Store historical data: * Save standup responses and summaries to database * Track participation rates over time * Enable trend analysis and reporting 5. **Privacy & Security**: * Store OAuth tokens securely (environment variables, secret managers) * Be mindful of sensitive information in standups * Consider data retention policies for historical standups * Allow team members to edit/delete their responses 6. **Customization per Team**: * Store team configs (channel IDs, questions, timing) * Allow teams to opt-in/opt-out * Support different schedules for different teams 7. **Performance**: * Cache Slack user info to reduce API calls * Implement request queuing for multiple teams * Monitor token usage and costs 8. **Testing**: * Test in a sandbox channel first * Have a manual override to skip days (holidays, etc.) * Implement dry-run mode for testing prompts **Scheduling Tip:** Use a scheduling service to trigger the bot daily: ```typescript // Example with node-cron import cron from 'node-cron'; // Run at 9 AM Monday-Friday cron.schedule('0 9 * * 1-5', () => { runStandup('C1234567890', 'C0987654321', 5); }, { timezone: "America/New_York" }); ``` --- # Connect with the CLI URL: https://metorial.com/docs/build/sandboxes/connect-cli Open a shell, run commands, and move files from your terminal. --- The CLI gives you a terminal in any running machine and copies files in both directions. Use it to debug a machine, seed it with data, or script work against it. * How to open a shell and run one-off commands * How to list, read, and transfer files * How to open a port in your browser * A running machine, saved as `MACHINE` ([quickstart](/docs/build/sandboxes/quickstart)) Connections need a running machine. Run `metorial-admin sandbox machines start $MACHINE` first if the machine is paused or hibernated. ## Open a shell [#open-a-shell] ```bash metorial-admin sandbox machines shell $MACHINE ``` You get an interactive terminal with full TTY support, so editors, `top`, and Ctrl+C work as usual. If your network drops, the CLI reconnects and resumes the same session. Type `exit` to close it. ## Run commands [#run-commands] Put the command after `--`. Output streams to your terminal, and the CLI exits with the command's exit code, so it works in scripts and CI. ```bash # Run a single command metorial-admin sandbox machines shell $MACHINE -- python --version # Set the working directory and environment variables metorial-admin sandbox machines shell $MACHINE --cwd /app -e NODE_ENV=production -- npm test # Pipe a local script into the machine cat setup.sh | metorial-admin sandbox machines shell $MACHINE -- bash -s ``` Pass `-t` to force a TTY, for example to get colored output, or `-T` to turn it off. Repeat `-e` for each variable. ## Work with files [#work-with-files] Remote paths are always absolute. Use `-` as the local path to read from stdin or write to stdout. ```bash # Browse metorial-admin sandbox machines files ls $MACHINE /app -r --max-depth 2 metorial-admin sandbox machines files stat $MACHINE /app/package.json metorial-admin sandbox machines files cat $MACHINE /app/package.json # Copy a directory in and results out metorial-admin sandbox machines files upload $MACHINE ./src /app/src -r metorial-admin sandbox machines files download $MACHINE /app/output ./output -r # Manage files metorial-admin sandbox machines files mkdir $MACHINE /app/logs metorial-admin sandbox machines files mv $MACHINE /app/old.txt /app/new.txt metorial-admin sandbox machines files rm $MACHINE /app/tmp -r ``` Uploads create missing parent directories. Recursive transfers copy files in parallel, keep file modes, and recreate symlinks. Useful options: * `upload --mode 0755`: sets the mode of uploaded files. * `upload --no-overwrite`: fails instead of replacing an existing file. * `rm -f`: ignores paths that don't exist. * `mv --overwrite`: replaces the destination if it exists. * `ls --limit`: caps the number of entries (10,000 by default). ## Open a port [#open-a-port] Generate a temporary URL for a port the machine listens on: ```bash metorial-admin sandbox machines access ports $MACHINE --ports '[3000]' --ttl-seconds 3600 ``` The output contains the URL for each port and a token. Send the token in the `Metorial-Sandbox-Token` header. [Networking](/docs/build/sandboxes/networking) covers public ports, which need no token. --- # Connect with the SDK URL: https://metorial.com/docs/build/sandboxes/connect-sdk Run commands, stream output, manage files, and call ports from your code. --- The SDK lets your app or agent drive a machine directly: run commands and read their output, keep long-running processes going, read and write files, and send HTTP requests to servers inside the machine. The SDK handles access tokens for you. * How to run commands and stream their output * How to run background processes * How to read and write files * How to call a server running in the machine * A running machine ([quickstart](/docs/build/sandboxes/quickstart)) * The [TypeScript or Python SDK](/docs/build/sandboxes#install-the-tools) Each helper opens one connection that you can reuse for many calls. Close it when you're done. In Python, the helpers are async and work as async context managers. Connections need a running machine. Call `machines.start()` first if the machine is paused or hibernated. ## Run commands [#run-commands] `command()` runs a command to completion and returns its output. A string runs through `/bin/sh -c`, so pipes and `&&` work. Pass an array to run a program directly without a shell. TypeScript Python ```typescript let shell = await metorial.sandbox.machines.shell(machineId); let result = await shell.command('npm install && npm test', { cwd: '/app', env: { CI: 'true' }, timeoutMs: 5 * 60_000 }); console.log(result.exitCode, result.stdout, result.stderr); let list = await shell.command(['ls', '-la', '/app']); ``` ```python async with await metorial.sandbox.machines.shell(machine_id) as shell: result = await shell.command( "npm install && npm test", cwd="/app", env={"CI": "true"}, timeout_ms=5 * 60_000, ) print(result.exit_code, result.stdout, result.stderr) listing = await shell.command(["ls", "-la", "/app"]) ``` Commands that exit with a non-zero code return normally, so check the exit code yourself. When the timeout hits, the command is stopped and `timedOut` is `true`. ### Stream output and send input [#stream-output-and-send-input] Pass output handlers to see output as it arrives, and `stdin` to feed the command input. Handlers receive raw bytes. TypeScript Python ```typescript let decoder = new TextDecoder(); await shell.command('python train.py', { onStdout: data => process.stdout.write(decoder.decode(data)), onStderr: data => process.stderr.write(decoder.decode(data)) }); let sorted = await shell.command('sort', { stdin: 'banana\napple\ncherry\n' }); ``` ```python import sys await shell.command( "python train.py", on_stdout=lambda data: sys.stdout.write(data.decode()), on_stderr=lambda data: sys.stderr.write(data.decode()), ) sorted_result = await shell.command("sort", stdin="banana\napple\ncherry\n") ``` ## Run background processes [#run-background-processes] `spawn()` starts a process and returns right away. Use it for dev servers, watchers, or interactive programs you write to over time. One connection can run many processes at once. TypeScript Python ```typescript let server = await shell.spawn('npm run dev', { cwd: '/app' }); let pid = await server.started(); server.onStdout(data => console.log(new TextDecoder().decode(data))); // later await server.signal('TERM'); let exit = await server.wait(); ``` ```python server = await shell.spawn("npm run dev", cwd="/app") pid = await server.started() server.on_stdout(lambda data: print(data.decode(), end="")) # later await server.signal("TERM") exit_info = await server.wait() ``` To write to a process, spawn it with `stdin: true`, then call `write()` and finish with `closeStdin()` (`close_stdin()` in Python). `kill()` sends `KILL`. ## Work with files [#work-with-files] TypeScript Python ```typescript let files = await metorial.sandbox.machines.files(machineId); await files.write('/app/config.json', JSON.stringify({ debug: true }), { parents: true }); let config = await files.readText('/app/config.json'); let { entries, truncated } = await files.list('/app', { recursive: true, maxDepth: 2 }); let info = await files.stat('/app/config.json'); if (await files.exists('/app/tmp')) { await files.delete('/app/tmp', { recursive: true }); } await files.move('/app/out.log', '/app/logs/out.log'); files.close(); ``` ```python import json async with await metorial.sandbox.machines.files(machine_id) as files: await files.write("/app/config.json", json.dumps({"debug": True}), parents=True) config = await files.read_text("/app/config.json") listing = await files.list("/app", recursive=True, max_depth=2) info = await files.stat("/app/config.json") if await files.exists("/app/tmp"): await files.delete("/app/tmp", recursive=True) await files.move("/app/out.log", "/app/logs/out.log") ``` A few details: * `write()` accepts text or bytes and replaces existing files unless you pass `overwrite: false`. Set `mode` (such as `'0755'`) for executables. * `read()` returns bytes and takes an `offset` and `length` to read part of a large file. * `stat()` returns the entry's `type` (`file`, `directory`, `symlink`, or `other`), `size`, `mode`, and modification time. * `list()` reports `truncated` when it hits its `limit`. ## Call a server in the machine [#call-a-server-in-the-machine] `connect()` gives you an HTTP client for one port. It adds the access token to every request and refreshes it when it expires. Your server must listen on `0.0.0.0`. TypeScript Python ```typescript let port = await metorial.sandbox.machines.connect(machineId, 3000); let health = await port.get('/health'); let created = await port.post('/items', { name: 'first' }); console.log(port.url, await created.json()); ``` ```python async with await metorial.sandbox.machines.connect(machine_id, 3000) as port: health = await port.get("/health") created = await port.post("/items", json={"name": "first"}) print(port.url, created.json()) ``` To call the port from somewhere else, such as a browser, read the URL from `port.url` and a token from `getToken()` (`get_token()` in Python), and send the token in the `Metorial-Sandbox-Token` header. [Networking](/docs/build/sandboxes/networking) explains tokens and public ports. ## Handle errors [#handle-errors] Connection and protocol problems throw `SandboxAccessError`. Failures tied to a single command throw `SandboxCommandError`, a subclass. Both carry a `code` and a `retryable` flag. File operations add the `path`, with codes such as `not_found`, `already_exists`, and `not_empty`. TypeScript Python ```typescript import { SandboxAccessError } from 'metorial'; try { await files.readText('/missing.txt'); } catch (error) { if (error instanceof SandboxAccessError && error.code == 'not_found') { // create it } else throw error; } ``` ```python from metorial import SandboxAccessError try: await files.read_text("/missing.txt") except SandboxAccessError as error: if error.code != "not_found": raise ``` Idle connections close after the machine's connection idle timeout. Pass `onNotice` (`on_notice` in Python) to `shell()` or `files()` to get a warning before that happens. --- # Custom images URL: https://metorial.com/docs/build/sandboxes/custom-images Build a machine image from your own Dockerfile. --- A custom image boots machines with your tools, packages, and files already in place. You build it from a Dockerfile, and Metorial makes it available in every region. * How to build an image in the dashboard * How images, versions, and builds relate * How to build from the CLI or SDK * A Dockerfile that builds locally with `docker build` * [Run your first machine](/docs/build/sandboxes/quickstart) ## How images work [#how-images-work] An image is a named container for versions. Every build creates a new numbered version of the image, and new machines boot from the latest version that built successfully. A machine's `image.version_id` shows which version it runs. An image stays `pending` until its first build succeeds. After that it's `active`, and you can create machines from it. ## Build an image in the dashboard [#build-an-image-in-the-dashboard] In the dashboard, go to **Developer**, then **Sandbox**, then **Machine Images**. The machine images list in the dashboard Click **Create Image** and fill in the form: * **Name**: a name you'll recognize in lists. * **Build Context**: a single Dockerfile, or a `.zip`, `.tar`, `.tar.gz`, or `.tgz` archive up to 50 MB. An archive must contain the Dockerfile and every file it copies. * **Dockerfile Path** and **Context Path**: where the Dockerfile and the build context sit inside the archive. They default to `Dockerfile` and `.`. Under **Advanced**, you can set a build **Target** stage, up to 64 **Build Arguments**, a **Timeout** (30 minutes by default), and the **Machine Disk Size**, which is the root disk size of machines that boot from this version (16 GiB by default). The Create Image dialog with the advanced options expanded A build records the names of its build arguments but never their values. The image opens on its **Versions** tab. Each version shows its build status and the regions it's available in. The Versions tab showing version 1 succeeded in two regions Open a version to follow the build log live. A version page with build attributes and the build log When the build succeeds, click **Create Machine** on the image or version page. To ship changes, click **New Version** on the image page and upload a new build context. **Rebuild** on a version page builds the same context again as a new version. ## Build with the CLI [#build-with-the-cli] Pass a directory and the CLI zips it for you: ```bash metorial-admin files upload ./my-image --purpose sandbox_image_build_context ``` Save the file ID from the output as `FILE`. ```bash metorial-admin sandbox images create --name python-data-agent metorial-admin sandbox images builds create $IMAGE --file-id $FILE ``` `builds create` also takes `--dockerfile-path`, `--context-path`, `--target`, `--build-args '{"PYTHON_VERSION": "3.12"}'`, `--timeout-seconds`, and `--disk-size-bytes`. ```bash metorial-admin sandbox image-builds get $BUILD metorial-admin sandbox image-builds logs $BUILD ``` Stop a build with `image-builds cancel $BUILD`. List an image's versions with `images versions list $IMAGE`. ## Build with the SDK [#build-with-the-sdk] Start builds from code to rebuild an image when your dependencies change. Upload the build context with the CLI first, then pass its file ID. TypeScript Python ```typescript let build = await metorial.sandbox.images.builds.create(imageId, { fileId: 'your_file_id', buildArgs: { PYTHON_VERSION: '3.12' } }); let done = ['succeeded', 'failed', 'cancelled', 'timed_out']; while (!done.includes(build.status)) { await new Promise(resolve => setTimeout(resolve, 5000)); build = await metorial.sandbox.imageBuilds.get(build.id); } console.log(build.status, build.versionNumber); ``` ```python import time build = metorial.sandbox.images.builds.create( image_id, file_id="your_file_id", build_args={"PYTHON_VERSION": "3.12"}, ) done = {"succeeded", "failed", "cancelled", "timed_out"} while build.status not in done: time.sleep(5) build = metorial.sandbox.image_builds.get(build.id) print(build.status, build.version_number) ``` When a build fails, its `failure` field holds a code and a message, and the build log shows the Docker output. --- # Disks URL: https://metorial.com/docs/build/sandboxes/disks Store data that outlives a machine, and grow a machine's storage. --- Every machine comes with a writable root disk. Add more disks when data needs to outlive a machine, move between machines, or be copied for a new task. * How the root disk works * How to create, attach, and detach disks * How to grow and copy a disk * [Run your first machine](/docs/build/sandboxes/quickstart) ## The root disk [#the-root-disk] Metorial creates a root disk for every machine. The machine's file system lives on it, so everything you install or write outside of attached disks ends up here. The root disk: * Keeps its files when the machine pauses, hibernates, or restarts * Is deleted together with the machine * Can't be detached, deleted on its own, or copied * Can grow with [`resize`](#grow-a-disk) Find its ID in the machine's `root_disk` field, or list it with `metorial-admin sandbox disks list --type root_fs --machine-id $MACHINE`. ## Attach a disk [#attach-a-disk] Disks you create yourself exist independently of machines. Delete a machine and its attached disks stay, ready for the next one. Create the disk in the region your machine runs in. Sizes are in bytes; this one is 10 GiB. ```bash metorial-admin sandbox disks create --region iad1 --name workspace --size-bytes 10737418240 ``` Save the disk ID from the output as `DISK`. Pass the disk and a mount path in `mounts`, either when you create the machine or later: ```bash metorial-admin sandbox machines update $MACHINE \ --mounts '[{"disk_id": "'$DISK'", "mount_path": "/data"}]' ``` Mount changes apply when the machine restarts. A running machine restarts on its own; run `machines restart` to apply the change immediately. ```bash metorial-admin sandbox machines shell $MACHINE -- df -h /data ``` The machine's `mounts` field shows each disk with its mount path and whether it's mounted. To detach a disk, update `mounts` without it. `mounts` replaces the whole list, so include every disk you want to keep. TypeScript Python ```typescript let disk = await metorial.sandbox.disks.create({ region: 'iad1', name: 'workspace', sizeBytes: 10 * 1024 ** 3 }); await metorial.sandbox.machines.update(machine.id, { mounts: [{ diskId: disk.id, mountPath: '/data' }] }); ``` ```python disk = metorial.sandbox.disks.create( region="iad1", name="workspace", size_bytes=10 * 1024**3, ) metorial.sandbox.machines.update( machine.id, mounts=[{"disk_id": disk.id, "mount_path": "/data"}], ) ``` ## Grow a disk [#grow-a-disk] Disks can grow but never shrink. Resizing briefly stops the machines the disk is mounted into, then starts them again. ```bash metorial-admin sandbox disks resize $DISK --size-bytes 21474836480 metorial-admin sandbox disks resizes list $DISK ``` A resize moves through `pending`, `draining`, `resizing`, `verifying`, and `restarting` before it's `complete`. If it ends `failed`, retry it with `disks resizes retry $DISK `. ## Copy a disk [#copy-a-disk] `clone` creates an independent copy of a disk in the same region. Use it to give several machines the same starting data, such as a prepared repository or dataset. ```bash metorial-admin sandbox disks clone $DISK --name workspace-copy ``` TypeScript Python ```typescript let copy = await metorial.sandbox.disks.clone(disk.id, { name: 'workspace-copy' }); ``` ```python copy = metorial.sandbox.disks.clone(disk.id, name="workspace-copy") ``` ## Find disks [#find-disks] ```bash metorial-admin sandbox disks list --attached=false # disks no machine uses metorial-admin sandbox disks mounts list $DISK # every machine the disk was mounted into ``` `disks list` also filters by `--type`, `--machine-id`, `--region`, and `--search`. A disk's `machine` field shows the machine it's attached to and its mount path. --- # Sandboxes URL: https://metorial.com/docs/build/sandboxes Give your agents isolated Linux machines to run code, store files, and serve apps. --- Sandboxes give your agents their own Linux machines. Start a machine in seconds, run commands and move files from the CLI or the SDKs, and let it pause or hibernate when it goes idle. * What machines, images, disks, and firewalls are * How to install the CLI and the SDKs * Where to go next ## How sandboxes fit together [#how-sandboxes-fit-together] A sandbox setup has four resources: * Machines are isolated Linux VMs. Each one has its own CPU, memory, and a writable root disk. * Machine images decide what a machine boots with. Start from a managed image such as Debian, Node.js, or Python, or build your own from a Dockerfile. * Disks store data that outlives a machine. Attach them to any machine in the same region. * Firewalls control which hosts a machine can reach on the internet. You also pick a region for every machine and a size (CPU and memory). Images and disks must be available in the machine's region. ## Install the tools [#install-the-tools] Sandbox commands live in the `metorial-admin` CLI. Install it and sign in: ```bash npm install -g @metorial/admin metorial-admin login ``` Homebrew (`brew install metorial/tap/metorial-admin`) and the install script (`curl -fsSL https://cli.metorial.com/install.sh | bash`) work too. To manage sandboxes from code, install the SDK: TypeScript Python {`npm install metorial`} {`pip install metorial`} The SDKs authenticate with an [API key](/docs/build/api). ## Guides [#guides] Create a machine, run a command, and clean up. Pick a size and region, and set the environment and startup commands. Pause idle machines, hibernate them, and run hooks on every transition. Keep data across machines with attachable disks. Build your own image from a Dockerfile. Open a shell and transfer files from your terminal. Run commands, manage files, and call ports from code. Expose ports, issue access tokens, and restrict outbound traffic. Read logs, events, errors, and resource usage. Build your own client for exec, shell, and files. --- # Lifecycle, pausing, and hibernation URL: https://metorial.com/docs/build/sandboxes/lifecycle Control when sandbox machines pause or hibernate, and run hooks on every transition. --- Machines don't need to run all the time. Pause a machine to freeze it exactly as it is, or hibernate it to stop using compute while its files stay on disk. Idle policies do this for you, and lifecycle hooks let your code react to each transition. * The states a machine moves through * How pausing and hibernation differ * How to pause and hibernate idle machines automatically * How to run code on start, pause, resume, and terminate * [Run your first machine](/docs/build/sandboxes/quickstart) ## Machine states [#machine-states] Transitions pass through short in-between states: `pausing`, `resuming`, `hibernating`, `restarting`, and `terminating`. A machine that can't boot ends up `failed`; its `failure` field says why (see [Monitoring](/docs/build/sandboxes/monitoring#failures)). | Command | Effect | | ----------- | ------------------------------------------------ | | `start` | Starts a paused or hibernated machine | | `pause` | Freezes a running machine, memory included | | `hibernate` | Stops the machine; files on disk stay | | `restart` | Boots the machine again on a fresh instance | | `touch` | Records activity so idle policies wait longer | | `delete` | Terminates the machine and deletes its root disk | ```bash metorial-admin sandbox machines pause $MACHINE metorial-admin sandbox machines start $MACHINE ``` Call `start` before you connect to a paused or hibernated machine. ## Pause or hibernate? [#pause-or-hibernate] Pausing keeps everything: memory, running processes, open files. When you start the machine again, it continues exactly where it stopped, and it does so faster than a cold boot. A paused machine still holds its memory, so keep pauses short. Hibernation stops the machine. The root disk and attached disks keep every file, but processes are gone. Starting a hibernated machine boots it fresh, runs your `setup` commands and entrypoint, and fires `start` hooks. For most agents, hibernation plus a good startup routine is the better choice. Write state to disk as you go, and let the entrypoint or a `start` hook bring services back up. An idle machine then uses no compute, and it comes back in a clean, known state. Use pausing for short breaks where an in-memory process can't be rebuilt quickly. A machine that stays paused for a long time (8 hours or more) may be restarted by the platform. Set a hibernation policy so that happens on your terms. ## Pause and hibernate idle machines automatically [#pause-and-hibernate-idle-machines-automatically] A lifecycle policy moves idle machines along for you: | Field | What it does | | -------------------------------- | --------------------------------------------------------------------------------- | | `pause_after_idle_seconds` | Pause the machine after it's been idle this long | | `hibernate_after_paused_seconds` | Hibernate the machine after it's been paused this long | | `connection_idle_seconds` | How long a connection can go without traffic before it stops counting as activity | Set a field to `null` to turn it off. If pausing is off and hibernation is on, the machine hibernates directly once it has been idle for the hibernation time. Activity is anything that uses the machine: an open shell, file, or port connection with traffic, or a `touch` call. A connection that's silent for `connection_idle_seconds` no longer keeps the machine awake. This policy hibernates a machine after 10 minutes without activity, which suits most agent workloads: ```bash metorial-admin sandbox machines update $MACHINE --lifecycle-policy '{ "pause_after_idle_seconds": null, "hibernate_after_paused_seconds": 600, "connection_idle_seconds": 300 }' ``` TypeScript Python ```typescript await metorial.sandbox.machines.update(machine.id, { lifecyclePolicy: { pauseAfterIdleSeconds: null, hibernateAfterPausedSeconds: 600, connectionIdleSeconds: 300 } }); // Keep the machine awake during long work that doesn't use a connection await metorial.sandbox.machines.touch(machine.id); ``` ```python metorial.sandbox.machines.update( machine.id, lifecycle_policy={ "pause_after_idle_seconds": None, "hibernate_after_paused_seconds": 600, "connection_idle_seconds": 300, }, ) # Keep the machine awake during long work that doesn't use a connection metorial.sandbox.machines.touch(machine.id) ``` The machine object tells you what comes next: `last_activity_at`, `pause_at`, `hibernates_at`, and `upcoming_events` (pauses, hibernations, and restarts scheduled within the next hour). ## Lifecycle hooks [#lifecycle-hooks] Hooks run your code when a machine changes state. Each hook runs on one transition: | `on` | Runs when | | ----------- | ------------------------------------------------------------------------------------ | | `start` | The machine boots: after create, when starting from hibernation, and after a restart | | `pause` | The machine is about to pause | | `resume` | The machine continues after a pause | | `terminate` | The machine is about to be deleted | A hook is either a script inside the machine or an HTTP request to a port inside the machine: * `wait`: the transition waits for the hook to finish. Use it for hooks that must complete first, such as flushing state before a pause. * `timeout_seconds`: how long the hook may run before it counts as failed. ```bash metorial-admin sandbox machines update $MACHINE --lifecycle-hooks-file hooks.json ``` TypeScript Python ```typescript await metorial.sandbox.machines.update(machine.id, { lifecycleHooks: [ { type: 'script', on: 'start', path: '/app/bin/restore.sh', arguments: ['--from', '/data'], wait: true, timeoutSeconds: 60 }, { type: 'http', on: 'pause', port: 3000, path: '/hooks/flush', wait: true, timeoutSeconds: 30 } ] }); ``` ```python metorial.sandbox.machines.update( machine.id, lifecycle_hooks=[ {"type": "script", "on": "start", "path": "/app/bin/restore.sh", "arguments": ["--from", "/data"], "wait": True, "timeout_seconds": 60}, {"type": "http", "on": "pause", "port": 3000, "path": "/hooks/flush", "wait": True, "timeout_seconds": 30}, ], ) ``` Images can ship their own hooks. Set `ignore_image_hooks` to `true` to skip them. ## A good startup routine [#a-good-startup-routine] Combine the runtime and hooks so a machine recovers on its own after every boot: * Bake dependencies into a [custom image](/docs/build/sandboxes/custom-images) so boots stay fast. * Use `runtime.setup` for quick preparation, such as creating directories or writing config from environment variables. * Use the `entrypoint` for the long-running service your agent talks to. * Use a `start` hook for work that depends on saved state, such as restoring a checkout from an attached [disk](/docs/build/sandboxes/disks). * Keep anything you need after a hibernation on disk, never only in memory. --- # Configure a machine URL: https://metorial.com/docs/build/sandboxes/machines Set the size, region, environment, and startup commands of a sandbox machine. --- Every machine is configured when you create it, and most settings can change later. This page covers size, region, the runtime (what runs when the machine boots), and how updates apply to a running machine. * How to size a machine * How to set environment variables and startup commands * What happens when you update a running machine * [Run your first machine](/docs/build/sandboxes/quickstart) ## Size [#size] Pick a size in one of two ways: * Use a spec, which is a predefined size. `specs list` shows each spec's memory, CPUs, regions, and whether your plan allows it (`available`). Pass `--spec-identifier`. * Set custom memory and CPUs in steps of 0.5. Pass `--memory-gib` and `--cpus` together. ```bash metorial-admin sandbox specs list metorial-admin sandbox machines create --region iad1 --image-id $IMAGE --memory-gib 4 --cpus 2 ``` If you set neither, the machine gets the default size. ## Region [#region] Every machine runs in one region. Its image must be available there, and its disks must live there. List regions with `metorial-admin sandbox regions list` and filter machines by region with `machines list --region iad1`. ## Runtime [#runtime] The runtime decides what runs when the machine boots. All fields are optional, and fields you leave out use the image's defaults. | Field | What it does | | ------------- | ------------------------------------------------------------------------ | | `setup` | Commands that run before the entrypoint starts | | `entrypoint` | The main process of the machine | | `cmd` | Arguments passed to the entrypoint | | `env` | Environment variables for the entrypoint | | `workdir` | Working directory of the entrypoint | | `user` | User the entrypoint runs as | | `stop_signal` | Signal the entrypoint receives when the machine stops, such as `SIGTERM` | `entrypoint_wait_seconds` sets how long startup waits for the entrypoint before the machine counts as running. The default is `0`. This machine installs dependencies, then runs a web server on port 3000: ```bash metorial-admin sandbox machines create \ --region iad1 \ --image-id $IMAGE \ --runtime '{ "setup": ["npm ci --prefix /app"], "entrypoint": ["node", "/app/server.js"], "env": { "PORT": "3000", "NODE_ENV": "production" }, "workdir": "/app" }' ``` Use `--runtime-file runtime.json` to keep larger configs in a file. TypeScript Python ```typescript let machine = await metorial.sandbox.machines.create({ region: 'iad1', imageId: 'sbi_your_image_id', memoryGib: 4, cpus: 2, runtime: { setup: ['npm ci --prefix /app'], entrypoint: ['node', '/app/server.js'], env: { PORT: '3000', NODE_ENV: 'production' }, workdir: '/app' } }); ``` ```python machine = metorial.sandbox.machines.create( region="iad1", image_id="sbi_your_image_id", memory_gib=4, cpus=2, runtime={ "setup": ["npm ci --prefix /app"], "entrypoint": ["node", "/app/server.js"], "env": {"PORT": "3000", "NODE_ENV": "production"}, "workdir": "/app", }, ) ``` Install packages in a [custom image](/docs/build/sandboxes/custom-images) instead of in `setup`. Machines boot faster when the image already has everything. ## Names and metadata [#names-and-metadata] Give machines a `name` and attach your own `metadata`, such as the user or task a machine belongs to. Metorial stores metadata but never reads it. ```bash metorial-admin sandbox machines update $MACHINE --metadata '{"user_id": "usr_42", "task": "nightly-report"}' metorial-admin sandbox machines list --search nightly --state running ``` `machines list` filters by `--state`, `--image-id`, `--region`, and `--search` (name, slug, or ID). Deleted machines only show up with `--status deleted`. ## Update a machine [#update-a-machine] `machines update` changes a machine in place. Names, metadata, [lifecycle policies](/docs/build/sandboxes/lifecycle), [firewalls](/docs/build/sandboxes/networking#control-outbound-traffic), and public ports apply immediately. Changes to the size, runtime, or [mounts](/docs/build/sandboxes/disks) need a restart. Until it happens, the machine reports `restart_required: true`. Metorial restarts a running machine on its own and applies the change to a paused machine by hibernating it. To apply it right away, restart the machine yourself: ```bash metorial-admin sandbox machines update $MACHINE --memory-gib 8 --cpus 4 metorial-admin sandbox machines restart $MACHINE ``` A restart boots the machine on a fresh instance. Files on the root disk and attached disks stay; running processes and memory don't. --- # Monitoring and debugging URL: https://metorial.com/docs/build/sandboxes/monitoring Read console logs, trace what happened to a machine, find out why it failed, and track resource usage. --- When a machine fails to start, stops unexpectedly, or runs slowly, these tools tell you what happened and why. * How to read a machine's console output * How to trace events, operations, and errors * How to read CPU, memory, network, and disk usage ## When a machine fails [#when-a-machine-fails] A machine in the `failed` state has a `failure` field with a code and a message: | Code | Meaning | | ------------------- | ------------------------------------------------------------------------------------- | | `provision_failed` | The platform couldn't allocate the machine | | `start_failed` | The machine booted but didn't come up, often because a startup command or hook failed | | `disk_unavailable` | An attached disk couldn't be mounted | | `image_unavailable` | The image isn't available in the machine's region | | `resize_failed` | A disk couldn't be resized | | `timeout` | An operation took too long | | `cancelled` | The operation was cancelled | | `internal_error` | Something went wrong on Metorial's side | For `start_failed`, check the errors and console output below. Fix the cause, for example with `machines update`, then run `machines start` again. ## Console output [#console-output] Metorial tracks each run of a machine as an instance, numbered from one. A new instance appears when the machine boots fresh, for example after a restart. An instance's logs hold its console output, including your startup commands and entrypoint. ```bash metorial-admin sandbox machines instances list $MACHINE metorial-admin sandbox machines instances logs $MACHINE $INSTANCE --direction backward --limit 200 ``` Logs are paged by line. Pass the returned cursor back with `--cursor` to read further. ## Errors [#errors] Errors are failures reported from inside the machine, such as a hook script that exited non-zero. Each error records the operation and stage it happened in, the exit code, and the process output. ```bash metorial-admin sandbox machines errors list $MACHINE metorial-admin sandbox machines errors get $MACHINE $ERROR ``` ## Events and operations [#events-and-operations] Events form a timeline of everything that happened to a machine. Filter them by what caused the event: `api` for your requests, `policy` for the lifecycle policy, `agent` and `guest` for activity inside the machine, and `system` for the platform. ```bash metorial-admin sandbox machines events list $MACHINE --source policy ``` Operations are the actions behind those events: starts, pauses, hibernations, restarts, and terminations. Each records what triggered it (for example `api`, `idle_policy`, `paused_policy`, or `failure`) and how it ended. Use them to answer questions such as why a machine paused. ```bash metorial-admin sandbox machines operations list $MACHINE --trigger idle_policy ``` ## Resource usage [#resource-usage] `machines usage` returns the latest samples of CPU, memory, network, and disk activity, so you can see whether a machine is busy right now. ```bash metorial-admin sandbox machines usage $MACHINE ``` For trends, use the usage history. `summary` totals a time range, and `timeline` splits it into buckets for charts. ```bash metorial-admin sandbox machines usage-history summary $MACHINE \ --from 2026-10-15T00:00:00Z --to 2026-10-16T00:00:00Z metorial-admin sandbox machines usage-history timeline $MACHINE \ --from 2026-10-15T00:00:00Z --to 2026-10-16T00:00:00Z --interval 1h ``` Each bucket reports runtime, CPU core seconds, average and peak cores, average and peak memory, and bytes sent, received, read, and written. Leave out `--interval` to get about 500 points. Minute-level history is kept for 7 days and hourly history for 30 days. Usage history measures what the machine used and isn't billing data. TypeScript Python ```typescript let timeline = await metorial.sandbox.machines.usageHistory.timeline(machineId, { from: new Date(Date.now() - 24 * 60 * 60 * 1000), to: new Date(), interval: '1h' }); ``` ```python from datetime import datetime, timedelta, timezone now = datetime.now(timezone.utc) timeline = metorial.sandbox.machines.usage_history.timeline( machine_id, from_=now - timedelta(days=1), to=now, interval="1h", ) ``` The dashboard shows the same data as charts on the machine's page. --- # Networking URL: https://metorial.com/docs/build/sandboxes/networking Expose ports with access tokens or publicly, and restrict outbound traffic with firewalls. --- Servers inside a machine are reachable over HTTPS, either with a short-lived access token or publicly. Outbound traffic is open by default, and firewalls let you limit it to the addresses you allow. * How to reach a port with an access token * How to make a port public * How to restrict outbound traffic with firewalls * A running machine with a server listening on a port ## Reach a port [#reach-a-port] Every port gets its own HTTPS URL. Your server must listen on `0.0.0.0`, since servers bound to `localhost` can't receive outside traffic. By default, a port only accepts requests that carry an access token in the `Metorial-Sandbox-Token` header. Generate a token and the URLs for the ports you need: ```bash metorial-admin sandbox machines access ports $MACHINE --ports '[3000]' --ttl-seconds 3600 ``` Use `--ports '"*"'` to get a token that works for every port. ```json { "object": "sandbox.machine.access", "token": "…", "ports": [3000], "urls": { "ports": { "3000": "https://.metorial.run" } }, "expires_at": "2026-10-16T15:00:00.000Z" } ``` ```bash curl -H "Metorial-Sandbox-Token: $TOKEN" https://.metorial.run/health ``` Tokens last one hour by default and up to 24 hours. In the dashboard, open the machine and use **Generate URL** to get a link for a port. The SDK's `connect()` adds and refreshes the token for you. See [Connect with the SDK](/docs/build/sandboxes/connect-sdk#call-a-server-in-the-machine). ## Make a port public [#make-a-port-public] Public ports accept requests without a token. Use them for webhooks, previews you share with people, or anything else that can't send a header. ```bash metorial-admin sandbox machines update $MACHINE --public-ports '[3000]' ``` TypeScript Python ```typescript await metorial.sandbox.machines.update(machineId, { publicPorts: [3000] }); ``` ```python metorial.sandbox.machines.update(machine_id, public_ports=[3000]) ``` The port keeps the same URL. Anyone with the URL can reach it, so put your own authentication in front of anything sensitive. To make the port private again, update `public_ports` without it. ## Restrict outbound traffic [#restrict-outbound-traffic] Machines can reach any public address on the internet until you attach a firewall. A firewall bundles one or more network policies, and each policy holds rules. You can reuse the same firewall across many machines. Once a machine has a firewall, outbound traffic is denied unless an `allow` rule matches it. When rules overlap, the one with the higher `priority` decides, so a `deny` rule can carve exceptions out of a broader `allow` rule. Private, loopback, link-local, and reserved addresses are always blocked, with or without a firewall. This policy allows HTTPS to GitHub's address range: ```bash metorial-admin network policies create --name allow-github --rules '[ { "effect": "allow", "direction": "egress", "cidrs": ["140.82.112.0/20"], "ports": [{ "from": 443, "to": 443 }], "description": "GitHub over HTTPS" } ]' ``` Save the policy ID from the output as `POLICY`. ```bash metorial-admin network firewalls create --name agent-egress --network-policy-ids $POLICY ``` Save the firewall ID as `FIREWALL`. Pass several policy IDs separated by commas to combine them. ```bash metorial-admin sandbox machines update $MACHINE --firewall-ids $FIREWALL ``` The rules apply to running machines right away, without a restart. Pass `--firewall-ids` to `machines create` to start a machine with a firewall from the first boot. In the SDK, attach firewalls you've created with `firewallIds` (`firewall_ids` in Python) on `machines.create` or `machines.update`. ### Rule fields [#rule-fields] | Field | Description | | ------------- | --------------------------------------------------------------------------- | | `effect` | `allow` or `deny` | | `direction` | `egress` for outbound traffic. Machines ignore `ingress` rules. | | `cidrs` | IP ranges in CIDR notation. Domain names aren't supported. | | `ports` | Port ranges as `{ "from": 443, "to": 443 }`. Leave out to match every port. | | `priority` | Higher numbers win when rules overlap | | `enabled` | Turns a rule off without deleting it | | `description` | A note for your team | The firewalls on a machine can open at most 98 address ranges in total, and each port range on an address counts separately. To allow all outbound traffic again, detach the firewall or add an `allow` rule for `0.0.0.0/0` and `::/0` on every port. Firewalls belong to Network Access and also protect integrations and providers. List the firewalls on a machine with `metorial-admin network firewalls list --machine-id $MACHINE`. --- # Run your first machine URL: https://metorial.com/docs/build/sandboxes/quickstart Create a sandbox machine, run a command in it, and clean up. --- This guide takes you from nothing to a running machine you can run commands in. It takes about two minutes. * How to pick an image and a region * How to create and start a machine * How to run commands and move files * How to delete a machine * [Install the CLI and sign in](/docs/build/sandboxes#install-the-tools) * For the SDK part, create an [API key](/docs/build/api) ## With the CLI [#with-the-cli] List the regions you can run machines in and note an identifier such as `iad1`: ```bash metorial-admin sandbox regions list ``` Metorial manages a set of images for common stacks: Debian, Alpine, Node.js, Bun, Deno, Python, Go, Java, PHP, .NET, and images with browser automation preinstalled. Find one and copy its ID (`sbi_...`): ```bash metorial-admin sandbox images list --owner global --search debian ``` Machines start right away unless you pass `--start=false`. Without a size, the machine gets the default size. ```bash metorial-admin sandbox machines create \ --region iad1 \ --image-id sbi_your_image_id \ --name first-machine ``` Save the machine ID from the output and check its status. It moves from `starting` to `running` within a few seconds. ```bash MACHINE=sma_your_machine_id metorial-admin sandbox machines get $MACHINE ``` Anything after `--` runs inside the machine. The CLI exits with the command's exit code. ```bash metorial-admin sandbox machines shell $MACHINE -- uname -a ``` Leave out the command to open an interactive shell. ```bash echo "hello from my laptop" | metorial-admin sandbox machines files upload $MACHINE - /tmp/hello.txt metorial-admin sandbox machines files cat $MACHINE /tmp/hello.txt ``` Deleting a machine also deletes its root disk. Attached [disks](/docs/build/sandboxes/disks) are kept. ```bash metorial-admin sandbox machines delete $MACHINE ``` ## With the SDK [#with-the-sdk] The same flow in code. Machine operations return the machine object; `shell()` and `files()` open a live connection to the machine. TypeScript Python ```typescript import { Metorial } from 'metorial'; let metorial = new Metorial({ apiKey: process.env.METORIAL_API_KEY! }); let machine = await metorial.sandbox.machines.create({ region: 'iad1', imageId: 'sbi_your_image_id', name: 'first-machine' }); while (machine.status != 'running') { if (machine.status == 'failed') throw new Error(machine.failure?.message); await new Promise(resolve => setTimeout(resolve, 1000)); machine = await metorial.sandbox.machines.get(machine.id); } let shell = await metorial.sandbox.machines.shell(machine.id); let result = await shell.command('uname -a'); console.log(result.exitCode, result.stdout); let files = await metorial.sandbox.machines.files(machine.id); await files.write('/tmp/hello.txt', 'hello from my app'); console.log(await files.readText('/tmp/hello.txt')); shell.close(); files.close(); await metorial.sandbox.machines.delete(machine.id); ``` ```python import asyncio import os from metorial import Metorial metorial = Metorial(api_key=os.environ["METORIAL_API_KEY"]) async def main(): machine = metorial.sandbox.machines.create( region="iad1", image_id="sbi_your_image_id", name="first-machine", ) while machine.status != "running": if machine.status == "failed": raise RuntimeError(machine.failure.message) await asyncio.sleep(1) machine = metorial.sandbox.machines.get(machine.id) async with await metorial.sandbox.machines.shell(machine.id) as shell: result = await shell.command("uname -a") print(result.exit_code, result.stdout) async with await metorial.sandbox.machines.files(machine.id) as files: await files.write("/tmp/hello.txt", "hello from my app") print(await files.read_text("/tmp/hello.txt")) metorial.sandbox.machines.delete(machine.id) asyncio.run(main()) ``` In Python, resource calls such as `create` and `get` are synchronous. The live helpers `shell()`, `files()`, and `connect()` are async. ## Next steps [#next-steps] * [Configure a machine](/docs/build/sandboxes/machines): size, environment variables, and startup commands * [Lifecycle and hibernation](/docs/build/sandboxes/lifecycle): pause and hibernate idle machines automatically * [Connect with the SDK](/docs/build/sandboxes/connect-sdk): stream output, run background processes, and call ports --- # Connection wire protocol URL: https://metorial.com/docs/build/sandboxes/wire-protocol Connect to machines from any language over WebSockets and HTTPS. --- If you use our SDKs or CLI, you don't need to read this. The SDKs handle the wire protocol for you. If you want to manually access a machine's file system, connect to its shell, or run a command, this page explains the message formats and connection details. * How to get an access token and connection URLs * The message formats for commands, interactive shells, and files * Close codes and error handling * An [API key](/docs/build/api) * [Connect with the SDK](/docs/build/sandboxes/connect-sdk), which shows what each protocol does ## Get an access token [#get-an-access-token] Request a token for the kind of access you need: `shell`, `files`, or `ports`. Tokens last 3600 seconds by default and up to 86400. ```bash curl -X POST https://api.metorial.com/sandbox/machines/$MACHINE/access/shell \ -H "Authorization: Bearer $METORIAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ttl_seconds": 3600}' ``` ```json { "object": "sandbox.machine.access", "token": "…", "scope": "shell", "urls": { "shell": "wss://…", "exec": "wss://…", "files": "wss://…", "ports": {} }, "expires_at": "2026-10-16T15:00:00.000Z" } ``` A `shell` token works for both the `shell` and `exec` URLs. For `ports`, pass `"ports": [3000]` or `"ports": "*"` in the body, and `urls.ports` maps each port to its URL. ### Authenticate a WebSocket [#authenticate-a-websocket] Send the token as a WebSocket subprotocol, next to the protocol name. Headers and query parameters aren't accepted. Browsers can connect this way, since they can't set WebSocket headers. ```typescript let socket = new WebSocket(access.urls.exec, [ 'metorial-sandbox-v1', `metorial-sandbox-token.${access.token}` ]); socket.binaryType = 'arraybuffer'; ``` ### Call a port [#call-a-port] Ports are plain HTTPS. Send the token in the `Metorial-Sandbox-Token` header. [Public ports](/docs/build/sandboxes/networking#make-a-port-public) need no token. ```bash curl -H "Metorial-Sandbox-Token: $TOKEN" "$PORT_URL/health" ``` ## Multiplexed framing [#multiplexed-framing] The exec and files protocols run many requests over one socket. Every request has a client-chosen `id`, and every reply carries it. * Text frames are JSON objects with an `id`. * Binary frames start with one byte holding the length of the `id`, then the `id` in UTF-8, then the payload. ```text [id length: 1 byte][id: UTF-8][payload] ``` ## Exec [#exec] Use the `urls.exec` socket to run commands and stream their output. The SDKs' `command()` and `spawn()` use it. ### Run a command [#run-a-command] ```json { "id": "r1", "op": "run", "v": 1, "command": ["/bin/sh", "-c", "npm test"], "cwd": "/app", "env": { "CI": "true" }, "timeoutMs": 300000 } ``` `command` is an argv array. `cwd`, `env`, and `timeoutMs` are optional. Set `"stdin": true` to keep standard input open for writing. ### Messages from the client [#messages-from-the-client] | Message | Purpose | | ---------------------------------------------- | ---------------------------------------- | | Binary frame `[id][bytes]` | Writes to the command's stdin | | `{ "id", "type": "stdin_eof" }` | Closes stdin | | `{ "id", "type": "signal", "signal": "TERM" }` | Sends a signal. Use names without `SIG`. | ### Messages from the server [#messages-from-the-server] | Message | Meaning | | ------------------------------------------------------------------------ | ------------------------------------------------------------- | | `{ "id", "type": "started", "pid" }` | The process started | | Binary frame `[id][stream][bytes]` | Output. The stream byte is `1` for stdout and `2` for stderr. | | `{ "id", "type": "exit", "code", "signal", "timedOut" }` | The process ended | | `{ "id", "type": "error", "error": { "code", "message", "retryable" } }` | The request failed | ## Interactive shell [#interactive-shell] Use the `urls.shell` socket for a terminal session. The CLI's `shell` command uses it. Each socket carries one session. ### Open a session [#open-a-session] Send `open` as the first message: ```json { "type": "open", "v": 1, "tty": true, "cols": 120, "rows": 40, "cwd": "/app" } ``` `command` (an argv array), `cwd`, and `env` are optional. Leave out `command` for an interactive shell. `cols` and `rows` go from 1 to 1000 and only apply when `tty` is `true`. The server answers with `{ "type": "session_init", "sessionId": "shs_…", "offset": 0 }`. ### Streams [#streams] * Client binary frames are stdin. * With `tty: true`, server binary frames are raw terminal output. * Without a TTY, the first byte of each server binary frame is `0x01` for stdout or `0x02` for stderr. Control messages from the client are `{ "type": "resize", "cols", "rows" }`, `{ "type": "signal", "signal": "INT" }`, and `{ "type": "stdin_eof" }`. When the process ends, the server sends `{ "type": "exit", "code", "signal" }`. ### Resume after a dropped connection [#resume-after-a-dropped-connection] Track how many output bytes you've received. If the socket drops without a normal close, open a new socket and attach to the session instead of opening a new one: ```json { "type": "attach", "v": 1, "sessionId": "shs_…", "offset": 18342 } ``` The server replays output from `offset`. If some of it is no longer buffered, it first sends `{ "type": "output_gap", "from", "to" }`. Closing the socket with code `1000` ends the session for good. ## Files [#files] Use the `urls.files` socket to read and write files. Requests use the [multiplexed framing](#multiplexed-framing), and file content travels in binary frames of up to 64 KiB. All paths are absolute. | Request | Replies | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `{ "id", "op": "stat", "path", "followSymlinks" }` | `result` with `entry` | | `{ "id", "op": "list", "path", "recursive", "maxDepth", "limit" }` | One or more `entries` messages, then `result` with `count` and `truncated` | | `{ "id", "op": "read", "path", "offset", "length" }` | `start` with `size`, `mtime`, and `offset`, then binary chunks, then `result` with `bytes` | | `{ "id", "op": "write", "path", "overwrite", "parents", "mode": "0644" }` | `result` with `bytes` and `sha256` | | `{ "id", "op": "mkdir", "path", "parents", "mode" }` | `result` with `created` | | `{ "id", "op": "move", "from", "to", "overwrite" }` | `result` | | `{ "id", "op": "delete", "path", "recursive", "missingOk" }` | `result` with `deleted` | ### Write a file [#write-a-file] After the `write` request, send the content as binary frames, then finish with the SHA-256 of the whole content. The server rejects the write with `checksum_mismatch` if the hashes differ. ```json { "id": "w1", "type": "end", "sha256": "9f86d08…" } ``` `write` also accepts `append`, `sync`, and the expected total `size`. ### File entries [#file-entries] `stat` and `list` return entries with `type` (`file`, `directory`, `symlink`, or `other`), `size`, `mode`, `mtime` in milliseconds, and `target` for symlinks. Non-recursive listings set `name`. Recursive listings set `path`, relative to the listed directory. ### Cancel a request [#cancel-a-request] Send `{ "id": "c1", "op": "cancel", "target": "r1" }` to cancel request `r1`, or `{ "id": "w1", "type": "abort" }` to abort an upload in progress. ### Errors [#errors] ```json { "id": "r1", "type": "error", "error": { "code": "not_found", "message": "…", "path": "/app/x" } } ``` | Code | Meaning | | ------------------------------------------------- | ------------------------------------------------ | | `not_found` | The path doesn't exist | | `already_exists` | The destination exists and `overwrite` is off | | `not_a_directory`, `not_a_file`, `is_a_directory` | The path has the wrong type for the operation | | `not_empty` | The directory isn't empty and `recursive` is off | | `checksum_mismatch` | The uploaded content doesn't match its `sha256` | | `unknown_op` | The server doesn't support the operation | | `io_error` | The file system returned an error | | `connection_interrupted` | The connection dropped. Retry the request. | ## Connection notices [#connection-notices] Idle sockets close after the machine's connection idle timeout. Before that, the server sends notices: ```json { "type": "connection_notice", "notice": "idle_warning", "idleInSeconds": 30 } ``` `notice` is `idle_warning`, `idle`, or `closing`, and `closing` can include a `reason`. ## Close codes [#close-codes] | Code | Meaning | What to do | | ------ | ------------------------------------------------------------------------ | --------------------------------------------- | | `1000` | Normal close. Reason `idle_timeout` means the socket was idle. | Reconnect when needed | | `1012` | The machine paused, is restarting, or terminated. The reason says which. | Start the machine, then reconnect | | `4401` | The token is invalid or expired | Request a new token and reconnect | | `4404` | The machine or session wasn't found | Check the ID | | `4410` | The machine was terminated, or the shell session ended | Don't retry | | `4429` | Too many requests | Back off and retry | | `4503` | The machine is unavailable. Reason `machine_failed` means it failed. | Retry later, or check the machine's `failure` | The shell socket adds these: | Code | Meaning | | ------ | ------------------------------------------ | | `4000` | The session was detached | | `4400` | The `open` or `attach` message was invalid | | `4409` | Another socket is attached to the session | | `4500` | The session failed to start | | `4503` | Too many sessions are open on the machine | --- # Error handling URL: https://metorial.com/docs/build/sdk/error-handling Handle errors gracefully in your Metorial SDK applications --- * Common error types * Error handling patterns The Metorial SDK throws typed errors that help you identify and handle specific failure scenarios. All API-related errors are instances of `MetorialAPIError`, which includes the error message, HTTP status code, and error type. ## Catching errors [#catching-errors] Wrap your Metorial SDK calls in try-catch blocks to handle errors gracefully. Check if the error is a `MetorialAPIError` to access structured error information. TypeScript Python ```typescript import { MetorialAPIError } from 'metorial'; try { await metorial.withProviderSession( provider, { providers: [{ providerDeploymentId: 'pdp_123' }] }, async session => { // Your logic } ); } catch (error) { if (error instanceof MetorialAPIError) { console.error(`API Error: ${error.message}`); } } ``` ```python from metorial import ( Metorial, AuthenticationError, NotFoundError, RateLimitError, OAuthRequiredError, ) import os metorial = Metorial(api_key=os.getenv("METORIAL_API_KEY")) try: async with metorial.provider_session( provider="openai", providers=[{"provider_deployment_id": "your-provider-deployment-id"}], ) as session: tools = session.tools except AuthenticationError: # Invalid API key print("Check your METORIAL_API_KEY") except NotFoundError: # Deployment doesn't exist or not accessible print("Deployment not found - verify your deployment ID") except OAuthRequiredError: # Server requires OAuth but no session was provided print("This server requires OAuth - see the OAuth section above") except RateLimitError: # Too many requests print("Rate limited - try again later") ``` The error types you are most likely to hit, and how to recover from each. You have provided an invalid Metorial API key. The deployment ID doesn't exist or you don't have access to it. Double-check your deployment ID in the Metorial dashboard. The user's OAuth authorization has expired. Prompt them to re-authorize through the OAuth flow. You're making too many requests. Implement exponential backoff and respect rate limit headers. --- # JavaScript SDK URL: https://metorial.com/docs/build/sdk/javascript Learn how to use the Metorial JavaScript SDK for TypeScript and JavaScript applications --- * How to install the Metorial JavaScript SDK * How to use the SDK in the browser, Node.js, and Deno environments * How to make requests to the Metorial API using the SDK **Before you start** * [Create your API keys](/docs/build/api) **External resources** * [JavaScript SDK on GitHub](https://github.com/metorial/metorial-node) The Metorial JavaScript SDK provides a type-safe interface for interacting with the Metorial API. Use it to manage provider deployments, create sessions, handle OAuth flows, and integrate MCP tools into your applications. The SDK works across all JavaScript environments including Node.js, browser applications, and Deno. **Explore Node.js Examples:** Check out practical examples and sample code in the [metorial-node examples directory](https://github.com/metorial/metorial-node/tree/main/examples) on GitHub. Install the SDK using your preferred package manager: npm yarn npm install metorial yarn add metorial To use the SDK, import the `Metorial` class and create an instance with your API key: ```javascript import { Metorial } from 'metorial'; let metorial = new Metorial({ apiKey: 'metorial_sk_io2h4...' }); ``` You can now use the `metorial` instance to make requests to the Metorial API. The SDK organizes API resources into logical namespaces. Each resource provides methods for common operations like `get()`, `list()`, `create()`, `update()`, and `delete()`. ```javascript // List provider deployments let deployments = await metorial.providerDeployments.list(); // Create a provider setup session (OAuth flow) let setupSession = await metorial.providerDeployments.setupSessions.create({ providerId: 'your-provider-id', providerAuthMethodId: 'oauth' }); ``` Each method returns a Promise that resolves with the response data. Use `async/await` or `.then()` to handle the results. The SDK is written in TypeScript and includes type definitions for all API resources. You can use TypeScript to benefit from type checking and IntelliSense support: ```typescript import { Metorial } from 'metorial'; let metorial = new Metorial({ apiKey: 'metorial_sk_io2h4...' }); let deployment = await metorial.providerDeployments.get('pdp_Rm4Mnheq2bfEPhBhP7SY'); ``` --- # OAuth URL: https://metorial.com/docs/build/sdk/oauth Handle user authorization for OAuth-enabled providers using Provider Setup Sessions --- * How OAuth works with Metorial * Creating Provider Setup Sessions * Storing and reusing Provider Auth Configs * [OAuth integrations](/docs/platform/integrations/create-custom-oauth-credentials) OAuth lets your users authorize access to their accounts on services like Slack, GitHub, Google Calendar, and more. This enables your AI agents to perform actions on behalf of your users with their explicit permission. ## How OAuth works [#how-oauth-works] The OAuth flow uses **Provider Setup Sessions**: 1. **Create a Provider Setup Session** for the provider the user needs to authorize 2. **Redirect the user** to the session URL to complete OAuth in their browser 3. **Wait for completion** — returns the completed setup session with an auth config 4. **Store the Provider Auth Config ID** for that user in your database 5. **Pass the auth config ID** when creating sessions for that user ## Creating provider setup sessions [#creating-provider-setup-sessions] TypeScript Python ```typescript let setupSession = await metorial.providerDeployments.setupSessions.create({ providerId: 'your-provider-id', providerAuthMethodId: 'oauth', // Optional: specify deployment if you have multiple for the same provider // providerDeploymentId: 'your-deployment-id', // Optional: redirect user here after authorizing // callbackUri: 'https://yourapp.com/oauth/callback' }); // Redirect your user to this URL console.log('Authorize here:', setupSession.url); ``` ```python setup_session = await metorial.providers.setup_sessions.create( provider_id="your-provider-id", provider_auth_method_id="oauth", # Optional: specify deployment if you have multiple for the same provider # provider_deployment_id="your-deployment-id", # Optional: redirect user here after authorizing # redirect_url="https://yourapp.com/oauth/callback" ) # Redirect your user to this URL print(f"Authorize here: {setup_session.url}") ``` ## Waiting for completion [#waiting-for-completion] TypeScript Python ```typescript // Waits until the user completes OAuth — returns the completed setup sessions let completed = await metorial.providerDeployments.setupSessions.waitForCompletion([setupSession]); // Store the auth config ID for this user await db.users.update(userId, { slackAuthConfigId: completed[0]!.authConfig!.id }); ``` ```python # Waits until the user completes OAuth — returns the completed setup session completed = await metorial.wait_for_setup_session([setup_session]) # Store the auth config ID for this user await db.users.update(user_id, slack_auth_config_id=completed[0].auth_config.id) ``` ## Using auth configs in sessions [#using-auth-configs-in-sessions] Retrieve the stored auth config ID from your database and pass it when creating a session: TypeScript Python ```typescript let session = await metorial.connect({ adapter: metorialAnthropic(), providers: [ { providerDeploymentId: 'your-provider-deployment-id', providerAuthConfigId: storedAuthConfig } ] }); ``` ```python session = await metorial.connect( adapter=metorial_anthropic(), providers=[ { "provider_deployment_id": "slack-deployment-id", "provider_auth_config_id": stored_auth_config_id # From your database } ], ) tools = session.tools() ``` ## Multiple OAuth services [#multiple-oauth-services] You can combine multiple OAuth-enabled providers in a single session: TypeScript Python ```typescript providers: [ { providerDeploymentId: "github-deployment-id", providerAuthConfigId: githubAuthConfigId, }, { providerDeploymentId: "slack-deployment-id", providerAuthConfigId: slackAuthConfigId, }, { providerDeploymentId: "exa-deployment-id", // No OAuth needed }, ] ``` ```python session = await metorial.connect( adapter=metorial_openai(), providers=[ { "provider_deployment_id": "github-deployment-id", "provider_auth_config_id": github_auth_config_id }, { "provider_deployment_id": "slack-deployment-id", "provider_auth_config_id": slack_auth_config_id }, { "provider_deployment_id": "exa-deployment-id" # No OAuth needed } ], ) tools = session.tools() ``` ## Complete example [#complete-example] TypeScript Python ```typescript import { Metorial } from 'metorial'; import { metorialAnthropic } from '@metorial/anthropic'; let metorial = new Metorial({ apiKey: process.env.METORIAL_API_KEY }); // 1. Create setup session let setupSession = await metorial.providerDeployments.setupSessions.create({ providerId: 'your-slack-provider-id', providerAuthMethodId: 'oauth', callbackUri: 'https://yourapp.com/oauth/callback' }); // 2. Send URL to user console.log('Please authorize:', setupSession.url); // 3. Wait for completion let completed = await metorial.providerDeployments.setupSessions.waitForCompletion([setupSession]); // 4. Store auth config ID for this user // database.save(userId, completed[0].authConfig.id) // 5. Use in a session let session = await metorial.connect({ adapter: metorialAnthropic(), providers: [ { providerDeploymentId: 'your-slack-provider-id', providerAuthConfigId: completed[0]!.authConfig!.id } ] }); ``` ```python import asyncio from metorial import Metorial, metorial_anthropic async def main(): metorial = Metorial(api_key="your-metorial-api-key") # 1. Create setup session setup_session = await metorial.providers.setup_sessions.create( provider_id="your-slack-provider-id", provider_auth_method_id="oauth", redirect_url="https://yourapp.com/oauth/callback" ) # 2. Send URL to user print(f"Please authorize: {setup_session.url}") # 3. Wait for completion completed = await metorial.wait_for_setup_session([setup_session]) # 4. Store auth config ID for this user # database.save(user_id, completed[0].auth_config.id) # 5. Use in a session session = await metorial.connect( adapter=metorial_anthropic(), providers=[ { "provider_deployment_id": "your-slack-deployment-id", "provider_auth_config_id": completed[0].auth_config.id } ], ) # Your AI now has access to user's Slack via session.tools() asyncio.run(main()) ``` --- # Python SDK URL: https://metorial.com/docs/build/sdk/python Learn how to use the Metorial Python SDK for Python applications --- * How to install the Metorial Python SDK * How to make requests to the Metorial API using the SDK **Before you start** * [Create your API keys](/docs/build/api) **External resources** * [Python SDK on GitHub](https://github.com/metorial/metorial-python) The Metorial Python SDK provides a Pythonic interface for interacting with the Metorial API. Use it to manage provider deployments, create sessions, handle OAuth flows, and integrate MCP tools into your Python applications. The SDK is fully typed with type hints for all methods and resources, making it ideal for use with modern Python development tools. **Explore Python Examples:** Check out practical examples and sample code in the [metorial-python examples directory](https://github.com/metorial/metorial-python/tree/main/examples) on GitHub. Install the SDK using pip: pip install metorial To use the SDK, import the `Metorial` class and create an instance with your API key: ```python import asyncio from metorial import Metorial metorial = Metorial(api_key="your-api-key") ``` You can now use the `metorial` instance to make requests to the Metorial API. Connect to your deployed providers using `metorial.connect()`. Pass an adapter for your chosen AI provider and a list of provider deployments: ```python from metorial import Metorial, metorial_openai async def main(): session = await metorial.connect( adapter=metorial_openai(), providers=[{"provider_deployment_id": "your-provider-deployment-id"}], ) # See your available tools for tool in session.tools(): print(f"- {tool['function']['name']}") # Call a tool directly result = await session.call_tool("search", {"query": "python news"}) print(result) asyncio.run(main()) ``` The SDK includes type hints for all API resources. Use tools like mypy or your IDE's type checker to catch errors before runtime and get autocomplete suggestions for all API methods and parameters. --- # Core concepts URL: https://metorial.com/docs/platform/get-started/core-concepts Understand how Workforce connects AI agents and applications to user-authorized tools through portals, integrations, skills, MCP URLs, and activity logs. --- Workforce is the control plane for governed AI connectivity. Admins choose which integrations and skills are approved, users connect their tools through portals, AI clients and agents connect through standard MCP URLs, and activity logs trace usage, tool calls, and access events. * Who uses Workforce and what each person gets from it * What portals, integrations, skills, Magic MCP, accounts, agents, and activity mean * How the first portal setup fits together * Where to go after the concepts are clear ## The problem Workforce solves [#the-problem-workforce-solves] AI agents and applications need access to the tools where work happens, but unmanaged access quickly becomes difficult to approve, explain, and audit. Workforce gives teams one place to decide which integrations are allowed, who can use them, how users connect them, and how usage is traced. Use Workforce when you want to: * publish approved integrations such as GitHub or Linear * give users a portal where they can discover and connect those tools * package repeatable workflows as skills * give AI clients, MCP clients, and CLIs a standard MCP URL for approved access * review traces, sessions, tool calls, auth events, and errors ## Who uses Workforce [#who-uses-workforce] | Actor | What they need from Metorial | | ------------------------------------ | -------------------------------------------------------------------------------------- | | Admin | A control plane for approved integrations, portals, skills, access groups, and tracing | | User | A portal for connecting authorized tools and discovering shared skills | | Agent, AI client, MCP client, or CLI | A standard MCP connection URL with access scoped to authorized tools | | Developer | APIs and SDKs for automating the same governed access model | ## How Workforce works [#how-workforce-works] Open **Workforce** from the dashboard. This is where admins manage the connection layer that users, AI clients, and agents rely on. Workforce dashboard At a high level, the flow looks like this: ## Core concepts [#core-concepts] Read these from the user's point of view first: where users authorize tools, which integrations are approved, which workflows are packaged for them, how AI clients connect, and how admins trace usage. | Concept | What it means | | ------------- | ------------------------------------------------------------------------------------- | | Portal | A branded place where users connect authorized integrations and discover skills | | Integration | An approved connection to a tool such as GitHub or Linear | | Skill | A reusable workflow built on approved integrations, files, and instructions | | Magic MCP URL | A standard MCP connection URL for AI clients, agents, MCP clients, and CLIs | | Account | The employee or user record that receives portal access | | Agent | A non-human actor or linked client that needs durable access to approved integrations | | Activity | Traces and logs for sessions, connections, tool calls, errors, and auth events | ## The first portal pattern [#the-first-portal-pattern] The clearest first setup is small: one portal, one user group, one or two integrations, and one useful skill. That gives users a concrete place to authorize tools, while keeping the first review easy to test. Give users a recognizable place to connect authorized tools and discover approved skills. Publish the first approved tools, such as GitHub or Linear, and decide whether users connect their own accounts or admins manage shared credentials. Package one repeated workflow so users can understand the value of the portal without needing to learn the whole system at once. Open the portal from the user side and confirm the right integrations, skills, and standard MCP URL are visible. Check activity logs after testing so admins can see traces, sessions, tool calls, auth events, and errors. --- # Dashboard tour URL: https://metorial.com/docs/platform/get-started/dashboard-tour A product tour of the Metorial dashboard: Workforce, Portals, Skills, Integrations, Magic MCP, activity, and settings. --- The Metorial dashboard is where you configure governed access for employees, agents, and external users. Start with Workforce, then add portals, integrations, skills, and Magic MCP access as your users need them. * Where the main Workforce product surfaces live * How Portals, Skills, Integrations, and Magic MCP connect * Where to inspect activity, logs, and workspace settings ## Home [#home] The project home page gives you the current organization, project, and environment context. It also shows setup shortcuts and popular catalog providers such as GitHub and Linear. Dashboard home ## Workforce [#workforce] Workforce is the product home for access management. Use it to review accounts, portals, activity, Magic Skills, and access surfaces connected to your project. Workforce dashboard ## Portals [#portals] Portals let you publish a branded catalog of approved integrations and skills. Admins configure access groups, resource listings, authentication, and highlights; users open the portal to connect and discover what they can use. Portal admin overview Consumer portal home ## Magic Skills [#magic-skills] Magic Skills are reusable workflows your team can create, organize, and publish. Use skills for repeatable work such as support playbooks, engineering review steps, customer handoff flows, or customer-facing automations. Magic Skills dashboard ## Integrations and providers [#integrations-and-providers] Use **Providers** to browse catalog providers and **Integrations** to configure reusable access. Provider pages include tools, capabilities, versions, and setup actions for integrations or Magic MCP servers. Provider catalog GitHub integration setup ## Magic MCP [#magic-mcp] Magic MCP servers turn configured providers into connectable MCP endpoints for agents and clients. Use groups and tokens to control which clients can connect to which servers. Magic MCP servers ## Activity and logs [#activity-and-logs] Use Logs to inspect sessions, connections, tool calls, provider runs, errors, alerts, Protoguard events, and provider-auth events. This is where operators confirm that access is being used as expected. Logs activity ## Settings [#settings] Organization settings include general settings, projects, members, and invites. Project settings live under the organization project route. Organization settings --- # Get started with Metorial URL: https://metorial.com/docs/platform/get-started Let your team connect AI agents and MCP clients like Cursor, ChatGPT, and Claude to approved apps, tools, and shared skills, with access controls and activity logs. --- ## Introduction [#introduction] Use Metorial when AI systems need to call real tools with the right user-approved access. That can be apps like GitHub and Linear, internal APIs, or remote MCP servers. Instead of sharing broad credentials or over-scoped service accounts, each user connects their own apps and keeps the permissions they should have. Set up branded portals for employees, customers, or partners. Users enter a portal to connect approved integrations, discover shared skills, and copy a standard MCP URL for their AI client. Teams control which integrations, skills, and providers appear in the portal, then review traces, sessions, tool calls, auth events, and failures after work runs. ## How it works [#how-it-works] Teams create portals for specific users or groups. Users connect approved integrations and discover shared skills inside the portal. Agents, MCP clients, and CLIs use standard MCP URLs. Products can build against the API and SDKs. Teams review traces, sessions, tool calls, auth events, and failures after work runs. Workforce is the admin control plane. Portals are where users authorize integrations and discover skills. Magic MCP turns that approved access into a standard connection URL for AI clients, agents, MCP clients, and CLIs. ## What you can build [#what-you-can-build] Connect your product to the tools each user authorizes, then build with the API and SDKs. Let employees connect integrations once and reuse them from approved AI clients. Give company-run agents access to approved integrations and skills with activity logs. Provide a standard MCP URL for tool access without custom OAuth or one-off servers. Bring internal APIs, private tools, and remote MCP servers into the same runtime. Package repeatable workflows on top of approved integrations, files, and instructions. ## Getting started [#getting-started] Learn how portals, integrations, skills, Magic MCP, and activity fit together. Learn where Workforce, Portals, Skills, Integrations, Magic MCP, and activity logs live. Create a branded portal where users can discover approved integrations and skills. Preview the employee experience and confirm the right integrations and skills are visible. Monitor sessions, connections, tool calls, provider runs, alerts, and auth events. Build programmatic workflows once your product setup is clear. ## Product areas [#product-areas] The main product surfaces map to the way access is managed in production: who gets access, what they can use, where they discover it, and how you audit it. Manage accounts, agents, identities, portals, skills, and access activity. Publish a branded catalog of approved integrations and skills for users. Package reusable workflows and publish them through controlled marketplaces. Connect tools like GitHub and Linear through approved auth and tool policies. ## Build on Metorial [#build-on-metorial] When you are ready to automate Metorial or connect it to an application, use the developer docs. Use the Metorial API to manage integrations, portals, and access programmatically. Build TypeScript applications with Metorial and common AI provider adapters. Build Python agent workflows with typed async SDK support. Bring your own MCP servers and provider runtime into Metorial. --- # Preview portal access URL: https://metorial.com/docs/platform/get-started/preview-portal-access Preview the employee portal and confirm users can see the right integrations and skills. --- Test the portal from the employee point of view before sharing it. This catches missing access groups, hidden resources, and confusing portal copy early. * How users authenticate into a portal * How the portal home page presents approved resources * How to verify integrations and skills from the employee view * [Workforce concepts](/docs/platform/get-started/core-concepts) * [Portals](/docs/platform/workforce/portals) ## Open the portal [#open-the-portal] Open the portal URL and complete the configured authentication flow. If email verification or SSO is enabled, test the same path your users will use. Portal employee login ## Review the home page [#review-the-home-page] The portal home page should make the approved resources obvious. The Magic MCP URL is a standard MCP connection URL that employees can paste into an AI client or agent. When they connect, the client can use the integrations the employee has connected through the portal, and the connection can filter that access down further for the specific client or workflow. The home page should also show highlighted resources, available integrations, and shared skills. Employee portal home ## Check integrations [#check-integrations] Open the integrations view and confirm the right tools are visible. Resources that require user credentials should make the connection state clear. Employee portal integrations ## Check skills [#check-skills] Open the skills view and confirm the published skills appear with the integrations they use. If a skill is missing, return to the portal admin resource settings and verify the access group is allowed. Employee portal skills --- # Create custom OAuth credentials URL: https://metorial.com/docs/platform/integrations/create-custom-oauth-credentials Connect an OAuth app that you manage and choose the permissions it can use. --- Custom OAuth credentials let Metorial connect through an OAuth app that you manage. This gives your organization control over the client credentials, redirect URI, and requested permissions. In this guide, you will select a provider, add the details from your OAuth app, review its scopes, and save the credentials. * How to create custom OAuth credentials * How to configure the redirect URI * How to review the requested OAuth scopes * Create an OAuth app with the provider you want to connect * Make sure you can edit the app's redirect or callback URLs * Have the app's client ID and client secret ready Keep the client secret private. Do not share it in messages, documentation, or source code. ## Create the credentials [#create-the-credentials] In the dashboard, select **Settings**, then select **Organization Settings**. Settings menu with Organization Settings selected Under **Integrations and MCP**, select **Provider Auth**. This page lists the authentication credentials available to your organization. Select **Create Auth Credentials**. Choose the provider for your OAuth app. Use the search field if you do not see it in the list. Provider selection step in the Create Auth Credentials window Copy the **Redirect URI** shown in Metorial. In the provider's OAuth app settings, add it as an allowed redirect or callback URL. Use the exact URI shown. A missing or different URI will prevent the OAuth flow from completing. OAuth credential form with the redirect URI and credential fields Complete the credential form: * **Name:** Enter a clear name that helps your team identify the credentials. * **Client ID:** Enter the client ID from your OAuth app. * **Client Secret:** Enter the client secret from your OAuth app. Review the selected scopes. Make sure the same scopes are allowed in your OAuth app, and only grant permissions that the integration needs. Select **Create**. The new OAuth credentials now appear in the Auth Credentials list and are ready to use with integrations for the selected provider. --- # Create an integration URL: https://metorial.com/docs/platform/integrations/create-integration Create a reusable integration and control which tools it can use. --- An integration connects a provider to Metorial so you can reuse the same configuration across sessions and deployments. This guide shows you how to choose a provider, configure its authentication, and control which tools the integration can use. * How to create an integration * How to configure authentication * How to limit the available tools when needed * Choose the provider you want to connect * Have any required authentication details ready * Decide whether you need to limit the available tools ## Create the integration [#create-the-integration] In the dashboard, open **Integrations**. Under **Integrations**, select **Integrations** again to open the list of configured integrations. Integrations list with the Create Integration button Select **Create Integration** in the top-right corner. The setup window opens. Choose the provider you want to connect. Use the search field if you do not see it in the list. Provider selection step in the Create Integration window Under **Auth Method**, select the authentication method you want the integration to use. If the provider requires credentials, select or create the credentials for this integration. Authentication method and credentials selected for an integration To limit the tools this integration can use, open **Tool Filters**. Set a tool group to **Mixed**, then select the tools you want to allow from that group. You can set an entire tool group to **Allow** or **Reject**. Reject write or destructive tools if the integration does not need to change or delete data. If you do not need custom tool filters, keep the default settings. Tool filters with individual tools selected Check the authentication method and credentials. If you changed the tool filters, review those settings too. Then select **Create Integration**. The integration is now ready to use in sessions and deployments. --- # Integrations URL: https://metorial.com/docs/platform/integrations Connect the tools your users authorize, control which tools each integration can use, and trace every call. --- An integration is approved access to one app, such as GitHub, Linear, or a remote MCP server you run yourself. You decide which tools it exposes and how it authenticates, and every call made through it shows up in the connection logs. * How to create an integration and limit the tools it can use * How to connect a remote MCP server or an OAuth app you manage * How to test tools in Metorial Explorer and trace the calls afterwards ## Set up an integration [#set-up-an-integration] Create a reusable integration and control which tools it can use. Connect an OAuth app that you manage and choose the permissions it can use. Connect a remote MCP server to Metorial. Create a Magic MCP server and check it in Metorial Explorer. ## Test and trace [#test-and-trace] Connect a provider and test its tools in Metorial Explorer. Trace session activity and inspect individual tool calls in the dashboard. --- # Link a remote MCP server URL: https://metorial.com/docs/platform/integrations/link-remote-mcp-server Connect a remote MCP server to Metorial. --- Use a remote MCP server when the provider already offers an MCP endpoint. Metorial connects to it and you can use it like any other provider on Metorial. Connections are secure, auditable, and permissions and tool filters in Metorial apply like they do for any other provider. * How to link a remote MCP server * How to check that it deployed successfully * Get the remote server URL * Check whether the server uses Streamable HTTP or SSE ## Link the server [#link-the-server] In the dashboard, open **Integrations**. Under **Custom Providers**, select **Remote MCP Servers**. Remote MCP Servers page Select **Link Remote MCP Server** . Enter the remote MCP server URL. Select the transport the server supports: **Streamable HTTP** or **SSE**. Then select **Continue**. Remote MCP server URL and transport fields Enter a name and, if useful, a short description. Select **Link Remote MCP Server**. Remote MCP server name and description fields Metorial discovers the server and creates its first version. In **Version Details**, wait for the status to show **Available** and **Current**. The deployment log should say that the MCP server deployment succeeded. Successful remote MCP server deployment Linking a server might not ask you to authenticate. If the server protects its tools, complete authentication when the connection flow asks for it. --- # Review connection logs URL: https://metorial.com/docs/platform/integrations/review-connection-logs Trace session activity and inspect individual tool calls in the dashboard. --- Connection logs show how a session moved from connecting to calling a tool. Use them to confirm which provider handled a request, review connection details, and check the arguments and result of a tool call. * How to find a session in the connection logs * How to review connection and provider details * How to inspect tool calls across one or more sessions * Know roughly when the activity happened * Know the session or tool you want to inspect, if possible ## Trace activity through the logs [#trace-activity-through-the-logs] In the dashboard, select **Integrations**. Under **Connection Logs**, select **Sessions**. Review the sessions table. Each row shows the session name, connection status, provider count, and creation time. Use these details to find the activity you want to inspect. Sessions table with connection status, provider count, and creation time Select the session name. If the session has more than one connection, select the relevant entry under **Recent Connections**. Expand **Connection** to see the connection ID, transport, message counts, creation time, and last active time. Expand **Providers** to confirm which provider, deployment, configuration, and authentication configuration were used. In the session timeline, find a **Client called tool** event. Open its **Tool Call** card, then review the tool name, arguments, and result. Check that the arguments match the request and that the result contains the expected data. Tool call in a session timeline with its arguments and result Under **Connection Logs**, select **Tool Calls**. This table lists calls from all captured sessions. Use the tool name, status, source, agent, session, and creation time to find a call. Select a row to open its session and call details. Tool calls table with status, source, agent, session, and creation time Select **Filter** to focus the Sessions or Tool Calls table on the activity you need. To investigate failed calls, select **Tool Errors** under **Connection Logs**. ## What to check when troubleshooting [#what-to-check-when-troubleshooting] * Check the output: Metorial integrations usually provide detailed error messages often with a suggested fix. * Confirm that the expected provider, deployment, configuration, and authentication configuration were used. * Check the transport, message counts, and last active time for the connection. * Read the session timeline in order to see what happened before and after the tool call. * Compare the tool arguments and result to find invalid inputs, unexpected responses, or missing data. --- # Set up a Magic MCP server URL: https://metorial.com/docs/platform/integrations/setup-magic-mcp-server Create a Magic MCP server and check it in Metorial Explorer. --- A Magic MCP server gives you a hosted MCP endpoint for a provider. Metorial manages the server and makes the provider's tools available through one connection. In this guide, you will choose a provider, configure the server, create it, and check its tools in Metorial Explorer. * How to create a Magic MCP server * How to configure its authentication and tool access * How to open the server in Metorial Explorer * Choose the provider you want the server to use * Have any required authentication details ready * Decide whether you need to limit the available tools ## Create the server [#create-the-server] Under **Integrations**, select **Magic MCP**. Then select **Create Magic MCP Server**. Magic MCP page with the create server button Choose the provider whose tools you want to make available. Use the search field if you do not see it in the list. Provider selection step for a Magic MCP server Review the available settings. If the provider requires authentication, open **Auth Config** to choose how the server should connect. Magic MCP server configuration step Select an existing authentication configuration, or use the available sign-in or setup option to create one. The choices depend on the provider and your project. Available authentication configurations Check that the authentication setting is complete. Open **Tool Filters** if you need to limit which tools the server can use. Otherwise, keep the default tool settings. Completed server settings ready for review Select **Create Magic MCP Server**. Metorial creates the server and opens its overview page. This page shows the server endpoint and connection options. Keep the endpoint and its access token private. New Magic MCP server overview ## Check the server in Explorer [#check-the-server-in-explorer] On the server overview, open the menu next to **Open Explorer** and select **Manual Tool Calls**. Explorer mode menu with Manual Tool Calls Explorer opens in manual mode. Search the tool list or select a tool to review its description and inputs. You can call a tool here to confirm that the server works. Start with a read-only tool when possible. Review a tool carefully before you run one that creates, changes, or deletes data. Magic MCP server open in Manual Explorer Your Magic MCP server is now ready to connect to an MCP client or use in Metorial. --- # Use Metorial Explorer URL: https://metorial.com/docs/platform/integrations/use-metorial-explorer Connect a provider and test its tools in Metorial Explorer. --- Metorial Explorer lets you review and call a provider's tools from the dashboard before you add them to a workflow. In this guide, you will connect a provider, open the manual tool interface, and make a safe test call. * How to connect a provider to Metorial Explorer * How to review a provider's available tools * How to call a tool manually and check its result * Choose the provider you want to test * Make sure you have any authentication details the provider requires * Start with a read-only tool when possible ## Connect a provider and call a tool [#connect-a-provider-and-call-a-tool] In the Metorial dashboard, select **Explorer** in the left navigation. Explorer opens with a list of providers. Explorer showing the provider list Select the provider you want to test. The connection page shows any setup required before you can open Explorer. Provider connection page Choose an existing authentication configuration if the provider requires one. To connect a different account, use the login option instead. Provider connection page with an authentication configuration selected Select the arrow next to **Open Explorer**, then select **Manual Tool Calls**. This mode lets you review the provider's tools and call them directly. Open Explorer menu with Manual Tool Calls selected Find a tool in the list and open it. Read its description and instructions, then review any required inputs. Start with a read-only tool when you are testing a new connection. A read-only tool ready to run Enter any required inputs, then select **Call Tool**. Review the result to confirm that the provider is connected and the tool returned the expected information. Result from a tool call ## Next steps [#next-steps] Use the tool list to explore other tools from the same provider. Before you call a tool that changes data, review its instructions and inputs so you understand what it will do. --- # Grant Workforce access URL: https://metorial.com/docs/platform/workforce/grant-access Use groups to control access to integrations and skills in Workforce. --- ## Overview [#overview] Workforce groups let you give teams access to the integrations and skills they need. Group-based access is easier to maintain than separate grants for each person. This guide shows you how to create a group, assign it to accounts, and use it to control resource access. * How to create and assign groups * How to add integrations and skills to Workforce * How to control which groups can use a resource * Open **Workforce** from the dashboard * Decide which people and resources the group should include ## Create a group [#create-a-group] Under **Access**, select **Groups**. The page lists the groups that already exist. Workforce groups list Select **Create Group**. Enter a clear name and an optional description, then select **Create Group** again. Enable the default assignment option only if every new account should join this group automatically. Form for creating a Workforce group ## Assign the group to an account [#assign-the-group-to-an-account] Under **Identity**, select **Accounts**. Select the account, then select **Access**. The **Groups** section shows the account's current group assignments. Select **Assign Groups**, choose the group, and confirm the assignment. Use groups for shared access so permissions remain consistent across the team. Group assignments for a Workforce account ## Add resources to Workforce [#add-resources-to-workforce] Under **Access**, select **Integrations**, then select **Add Integration**. Choose **New Integration** to create and configure an integration. Choose **Existing Integration** to add a portal listing for an integration that already exists. Integration resources in Workforce Under **Access**, select **Skills**, then select **Add Skill Resource**. Choose **New Skill** to create a skill or skill group. Choose **Existing Skill** to publish a resource that already exists. Enter the required details and complete the setup. Skill resources in Workforce ## Control access to a resource [#control-access-to-a-resource] Under **Access**, select **Integrations** or **Skills**, then select the resource you want to manage. Both resource types use the same access controls. In the **Access** section, select **Add Group** and choose a group. Set the group to **Allow** or **Deny**, then select **Save**. Group access settings for a Workforce resource --- # Workforce URL: https://metorial.com/docs/platform/workforce Workforce is the control plane for connecting AI agents and applications to user-authorized integrations, skills, portals, and MCP URLs. --- Workforce is where admins turn user-authorized tool access into something AI agents and applications can use. It brings together portals, integrations, skills, Magic MCP, accounts, agents, and activity so access control and tracing live in one place instead of scattered across credentials and one-off setup. * What Workforce manages for admins, users, AI clients, MCP clients, CLIs, and agents * How portals, integrations, skills, accounts, agents, and Magic MCP fit together * Which dashboard areas support the main Workforce workflow ## What Workforce is for [#what-workforce-is-for] Use Workforce when you need to answer practical access questions: * Which users or agents can use approved integrations? * Which integrations and skills should users see? * Which portal should a customer, partner, or internal team open? * Which MCP clients and CLIs can connect through approved access? * Which traces, sessions, tool calls, and auth events happened? Workforce dashboard ## What you can manage [#what-you-can-manage] | Area | What it does | | ------------ | ------------------------------------------------------------- | | Portals | Publish branded catalogs of approved integrations and skills | | Skills | Create and govern reusable workflows | | Integrations | Configure approved access to tools such as GitHub and Linear | | Magic MCP | Expose approved provider access through managed MCP endpoints | | Accounts | Manage employees or users that receive portal access | | Agents | Manage non-human actors and linked clients | | Activity | Review traces, sessions, connections, tool calls, and errors | ## Core workflow [#core-workflow] Most teams start with this path: Start from the Workforce dashboard for the project you want to configure. Create the branded place where users will discover approved integrations and skills. Publish the tools and workflows users need. Start with a small set, then expand as usage becomes clear. Open the user portal and confirm the right integrations, skills, and MCP URL are visible. Inspect traces, sessions, connections, tool calls, auth events, and alerts. ## Dashboard surfaces [#dashboard-surfaces] The Workforce product spans these dashboard areas: | Area | Route | What it manages | | ------------ | --------------------------------------------------------------------------------- | --------------------------------------------------------------- | | Workforce | `/workforce` | Activity, portals, accounts, and access overview | | Portals | `/portals` | Branded resource catalogs for employees, partners, or customers | | Magic Skills | `/skills` | Skills, marketplaces, templates, groups, and skill policy | | Magic MCP | `/magic-mcp` | MCP servers, connections, groups, and tokens | | Accounts | `/consumers` | Employees or users that can receive access | | Agents | `/agents` | First-class agent actors and linked clients | | Identity | `/identities`, `/actors`, `/identity/delegations`, `/identity/delegation-configs` | Identities, actors, delegations, and delegation configs | Accounts page Agents page ## How the pieces fit [#how-the-pieces-fit] Portals are the user connection layer. Integrations provide approved tool connections. Skills package repeatable workflows. Magic MCP gives AI clients, agents, MCP clients, and CLIs a standard MCP endpoint for approved access. Accounts and agents represent who or what can use access. Activity and logs trace usage, tool calls, and access events. ## Identity and delegation [#identity-and-delegation] Workforce also includes lower-level identity and delegation pages. Use them when you need durable ownership for employees, agents, customer-facing users, or delegated access relationships. | Page | What it manages | | ------------------ | -------------------------------------------------- | | Accounts | Employees or users that can receive access | | Agents | Non-human actors and linked clients | | Identities | Identity records used for ownership and delegation | | Delegations | Access relationships between identities | | Delegation Configs | Reusable policies for identity delegation | Identities page ## Related pages [#related-pages] Publish approved integrations and skills through a branded user surface. Create reusable workflows and publish them to teams or customers. Connect the tools your users authorize. Monitor access usage and operational events. --- # Inspect Workforce agents URL: https://metorial.com/docs/platform/workforce/inspect-agents Review agent details, tool calls, and connection activity in Workforce. --- ## Overview [#overview] The Agents area shows which agent clients have connected to Workforce and what they have done. Use it to investigate a tool call, review a session, or check how an agent connected. * How to find and open an agent * How to review the agent's tool calls * How to inspect its connection history * Open **Workforce** from the dashboard * Under **Identity**, select **Agents** ## Review an agent [#review-an-agent] The **Agents** table shows each agent's name, type, status, linked actor, and creation time. Select the agent you want to inspect. Workforce agents table The overview identifies the agent and its linked actor. It also shows current or recent agent instances. Use these details to confirm that you opened the correct agent. Workforce agent overview ## Review tool calls and connections [#review-tool-calls-and-connections] Select **Operations** in the agent navigation. Each row represents a tool call and shows its status, source, agent, session, and time. Select a row to inspect a specific call. Operations recorded for a Workforce agent Select **Connections**. Review the participant, connection state, transport, session, and last active time to understand the agent's connection history. Connection history for a Workforce agent Start in **Operations** to find the tool call or session you want to investigate. Then open **Connections** and find the same session. Return to the overview if you need to confirm the agent or linked actor. --- # Magic Skills URL: https://metorial.com/docs/platform/workforce/magic-skills Magic Skills package repeatable workflows so teams can publish and govern useful agent work. --- Magic Skills turn repeatable work into reusable assets. Create a skill once, organize it with groups or marketplaces, and publish it through Workforce or a portal. * What Magic Skills are for * How skills, groups, templates, and marketplaces fit together * How skills appear in portals for users ## What skills are for [#what-skills-are-for] Use Magic Skills for work that should be repeatable, shareable, and governed: * support triage playbooks * engineering review workflows * customer handoff steps * research and reporting tasks * internal operations checklists Magic Skills dashboard ## Concepts [#concepts] | Concept | Meaning | | ----------------- | ----------------------------------------------------------------------------------------------------- | | Magic Skill | A reusable workflow with instructions, files, and optional scripts that an AI client or agent can use | | Skill Group | A collection of related skills, such as an engineering or support playbook | | Skill Template | A starting point for creating repeatable skills with a shared structure | | Skill Marketplace | A curated skill catalog that teams can publish and connect to compatible clients | ## Create a skill [#create-a-skill] Create a skill with a clear name and description. The name should describe the job the user wants done, not the implementation detail behind it. | Field | Good pattern | | ----------- | ------------------------------------------------------------------------------ | | Name | "Summarize Linear Escalations" | | Description | "Reviews recent Linear issues and drafts a customer-ready escalation summary." | After a skill exists, add the files, agents, resources, and integrations it needs. ## Connect integrations [#connect-integrations] Skills become more useful when they can use approved integrations. A customer support skill might use Linear and GitHub; an operations skill might use Google Drive and Slack. Linked integrations appear on the portal skill card so users can understand what access the workflow uses. ## Skill marketplaces [#skill-marketplaces] A skill marketplace is a curated library of skills and plugins that a team wants to make reusable. Use marketplaces when teams need one reviewed place to publish workflows instead of copying skill files between projects or clients. Marketplaces can be backed by a GitHub repository. Any change to a skill through Metorial is then automatically synced with the repo. This is a one-way sync from Metorial to GitHub, which is especially useful when non-technical teammates need to share skills without GitHub accounts. After a client that supports skills loads a marketplace skill, users can invoke it directly by name where the client supports that behavior, commonly with a slash command such as `/skill-name`. Learn how Claude Code discovers skills, where skills live, and how `/skill-name` invocation works. Learn how Cursor uses Agent Skills to package reusable knowledge, scripts, and commands. Learn how Cursor distributes skills, MCP servers, and related capabilities through plugins and marketplaces. ## Publish and share skills [#publish-and-share-skills] Skills can start from an admin in the dashboard or from a user in the portal. Use admin-published skills for approved workflows that should appear for a group. Use user-created skills when the person closest to the work wants to capture and share a workflow. ### Admin-published skills [#admin-published-skills] Admins publish skills from the dashboard when a workflow should be available through a portal. Open the portal in the dashboard, add a skill resource, and choose the skill, template, or group that should appear for users. Portal skill listings Choose the access groups that should see the skill. This keeps approved workflows visible to the right users without making them available to everyone. Users then see the approved skills inside the portal with their connected integrations. Employee portal skills ### User-created skills [#user-created-skills] Portal users can create personal skills when they know the workflow best. A support lead can draft a refund handling skill, a sales teammate can write an account research skill, and an operations teammate can turn a recurring checklist into something agents can follow. Collaborative skill editor Keep the skill private while drafting, then invite teammates to refine the instructions. Shared skills can be collaborative, so several teammates can improve the same workflow before it is used more broadly. Share the skill with the groups that should have access. The skill stays reusable for the team while access remains controlled through the portal. ## Govern skill execution [#govern-skill-execution] Settings define the default policy for skill files and execution behavior. Magic Skills settings | Setting | What it controls | | ------------------------------ | --------------------------------------------------------------------------- | | Allow scripts | Whether skills can include and execute script files | | Allow non-standard directories | Whether skill files can live outside the standard skill directory structure | | Allow all file extensions | Whether skill uploads are limited to an approved extension list | --- # Manage Workforce accounts URL: https://metorial.com/docs/platform/workforce/manage-accounts Invite people, review their access, and inspect their Workforce activity. --- ## Overview [#overview] Workforce accounts represent the people who can use your portal. From an account, you can review invitation status, manage access, and inspect activity. This guide shows you how to invite a person and manage their account after it is created. * How to invite a person to the portal * How to review and update account access * Where to find an account's servers, operations, and connections * Open **Workforce** from the dashboard * Make sure you have permission to manage accounts and access ## Invite a person [#invite-a-person] Under **Identity**, select **Accounts**. The table shows each account's email address, invitation status, and number of assigned groups. Workforce accounts table Select **Invite User** in the top-right corner. Enter the person's **Name** and **Email**. Add a message if needed, then select **Send Invite**. The account appears with an invited status until the person joins. Form for inviting a person to Workforce ## Review and manage an account [#review-and-manage-an-account] From **Accounts**, select the person's name. The overview shows the account's details and recent activity. Workforce account overview Select **Access**. Review the groups assigned to the account and any access granted directly to it. Use **Assign Groups** to add the account to a group. Grant direct access to an integration or skill only when the person needs an exception that should not apply to a whole group. Access settings for a Workforce account Remove group assignments and direct grants when they are no longer needed. Use groups for shared access so permissions stay consistent and are easier to review. ## Review account activity [#review-account-activity] Under **Identity**, select **Accounts**, then select the account you want to review. Select **Magic MCP Servers** in the account navigation to see the servers created for the account. Select **Operations** to see the account's tool calls, including their status and session details. Select **Connections** to inspect connection activity and session details for the account. --- # Portals URL: https://metorial.com/docs/platform/workforce/portals Portals are branded places where users discover and connect approved integrations, skills, and MCP access. --- Portals turn a Workforce setup into a user-facing experience. Create a portal for a customer, partner, department, or internal team, then publish the integrations and skills that group should use. * How admins build a portal * How users experience a portal * How access groups, integrations, skills, highlights, and authentication fit together ## Portal journey [#portal-journey] Admins create the portal and decide what appears inside it. Users open the portal, authenticate, and connect to the approved resources. ## Create a portal [#create-a-portal] Open **Portals** from Workforce and create a portal with a clear name and description. Use the description to explain who the portal is for and what kind of work it supports. Portal admin overview ## Add access groups [#add-access-groups] Access groups decide which users can see each portal resource. Groups can apply to every account by default or match SSO group IDs. Portal access groups After adding a resource, open the resource and explicitly allow the groups that should see it. ## Add integrations [#add-integrations] Publish integrations so users can connect approved tools. A portal integration can be: | Listing type | Meaning | | --------------- | ------------------------------------------------------------------ | | User-configured | Each user connects the integration with their own credentials | | Pre-configured | Administrators manage the shared credentials and provider settings | Portal integration listings ## Add skills [#add-skills] Publish skills so users can run approved workflows. Skills can show linked integrations, which helps users understand what access powers the workflow. Portal skill listings ## Highlight starter resources [#highlight-starter-resources] Highlights organize selected resources on the portal home page. Use them for recommended workflows, starter kits, or department-specific bundles. Portal highlights ## Configure authentication [#configure-authentication] Portal authentication controls who can enter the portal. Configure SSO tenants, email or domain allowlists, and session expiry settings. Portal authentication settings Users authenticate before they see portal resources. Portal employee login ## Test the employee experience [#test-the-employee-experience] Open the portal as a user and check the home page, integrations view, and skills view. Employee portal home Employee portal integrations Employee portal skills ## Magic MCP in portals [#magic-mcp-in-portals] Portals can expose portal-aware Magic MCP access. Use this when users should connect MCP clients through a branded, authenticated surface rather than a project-internal endpoint. Use portal-connected Magic MCP when: * access should be user-specific * the connection should honor portal authentication * users need a branded entry point * resource listings and requests should wrap MCP access ## Related pages [#related-pages] Confirm users can see the right integrations and skills. Publish reusable workflows through portals. Understand the broader Workforce access model. Configure the tools users can access. --- # Organizations, projects, and environments URL: https://metorial.com/docs/build/operate/organizations-and-projects How Metorial organizes teams, applications, and isolated environments. --- * How organizations, projects, and environments fit together * When to use production and sandbox environments Metorial organizes resources in three levels: ## Organizations and projects [#organizations-and-projects] An **organization** is the top-level workspace for your company or team. It holds members, shared settings, and projects. A **project** usually represents one application or product. Create separate projects when the work, access, or ownership should be separate. Create projects in Metorial ## Production and sandboxes [#production-and-sandboxes] Every project includes **one production environment** by default. Use it for resources used by your live applications and users. You can create **multiple sandbox environments** for development, testing, or experiments. Each environment has its own provider deployments, API keys, authentication configs, and logs, so changes in a sandbox do not affect production. Use the environment switcher in the dashboard to move between them. Use a separate API key for each environment. --- # Providers URL: https://metorial.com/docs/build/operate/providers Providers are catalog or custom integrations you connect to Metorial. Understanding integrations, Magic MCP servers, configs, auth configs, and provider deployments lets you control exactly how each provider is configured and authenticated. --- * What providers, integrations, Magic MCP servers, provider deployments, configs, and auth configs are * How these entities relate to each other * How to mix and match them when creating sessions * [Sessions guide](/docs/build/operate/sessions) * [OAuth integrations](/docs/platform/integrations/create-custom-oauth-credentials) * [Create an integration](/docs/platform/integrations/create-integration) ## Providers [#providers] A **provider** is an integration available in the Metorial catalog—for example Slack, GitHub, Exa, or Tavily. Each provider has one or more **versions**, and each version defines: * The tools the provider exposes (with input/output schemas) * The authentication methods it supports * The configuration fields it accepts You browse and discover providers through the Metorial dashboard catalog. Provider catalog ## Integrations and Magic MCP servers [#integrations-and-magic-mcp-servers] From a provider page, **Use Provider** gives you two common setup paths: | Path | Use when | | ---------------- | ---------------------------------------------------------------------- | | Integration | You want reusable provider setup for sessions and application code | | Magic MCP Server | You want a managed MCP endpoint that clients and agents can connect to | Use provider menu ## Provider deployments [#provider-deployments] A **provider deployment** is a running instance of a provider. By default, a provider deployment always uses the **latest version** of the provider. You can optionally **pin** a deployment to a specific version to prevent automatic updates—useful when you need stability across a production environment. ```text Provider (e.g. "Slack") └── Provider Deployment (your running instance, optionally pinned to a version) ``` Every deployment has a unique ID (`pdp_...`). You use this ID when creating sessions. ## Provider configs [#provider-configs] A **provider config** stores the configuration values for a provider deployment—things like endpoint URLs, feature flags, or other settings specific to that provider. Provider configs are linked to a provider and can optionally be locked to a specific provider deployment. This lets you have multiple configs for the same provider (e.g. one for a staging environment, one for production). You can also link a provider config to a **Provider Config Vault** entry, which stores a reusable config template you can apply across multiple deployments. Not all providers require configuration. If a provider has no required config fields, Metorial auto-creates a default config when you create a session. ## Provider auth configs [#provider-auth-configs] A **provider auth config** stores the authentication credentials for a specific user's connection to a provider. This is where OAuth tokens and manual API keys live. There are three types: | Type | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------- | | `manual` | Credentials entered directly (e.g. an API key for Exa or Tavily) | | `oauth_automated` | OAuth tokens obtained via a [Provider Setup Session](/docs/platform/integrations/create-custom-oauth-credentials) | | `oauth_manual` | OAuth tokens imported manually | Auth configs are stored per user. If 100 users connect their Slack accounts, you have 100 auth configs. This keeps credentials isolated and auditable. In the dashboard, provider setup asks you to choose or create auth credentials/auth configs before creating an integration or Magic MCP server. GitHub Magic MCP auth config ## Tool filters [#tool-filters] Provider setup also includes tool filters. You can allow, reject, or mix tools by category: * read-only tools * write tools * destructive tools For high-impact providers like GitHub and Linear, review destructive tools before exposing them to production agents. ## Provider setup sessions [#provider-setup-sessions] A **provider setup session** is the flow you use to get a user authenticated with a provider. You create a setup session and redirect your user to its URL. They complete the auth flow (OAuth or manual config entry), and when they finish, Metorial creates a provider auth config automatically. Setup sessions support three modes: | Mode | Use when | | ----------------- | ----------------------------------------------- | | `auth_only` | The provider needs authentication but no config | | `config_only` | The provider needs configuration but no auth | | `auth_and_config` | The provider needs both | Setup sessions expire if the user doesn't complete them. You can poll or webhook to check their status (`pending` → `completed` / `expired` / `failed`). See [OAuth integrations](/docs/platform/integrations/create-custom-oauth-credentials) for the full setup flow. ## How these entities compose [#how-these-entities-compose] When creating a session, you attach one or more **provider deployments** to it. Metorial uses the deployment to look up the provider and then figures out what else it needs: * If the provider requires **authentication**, you must supply a matching provider auth config—or create one first via a setup session. * If the provider requires **configuration**, you must supply a provider config—or Metorial creates a default one automatically. * You always need at least a **provider deployment** (or enough information to find one) for each provider in the session. Metorial auto-creates missing pieces where possible. For example, if a provider has no required config fields, you don't need to create a config manually since Metorial creates a default one for the session. A single session can attach to **multiple provider combinations** at once, mixing different providers and different users' auth configs. ## Custom providers [#custom-providers] Beyond the catalog, you can build and deploy your own providers. See [Custom providers](/docs/build/custom-providers) to get started. --- # Sessions URL: https://metorial.com/docs/build/operate/sessions Sessions are the runtime layer that connects your application to one or more providers. Each session gives your AI access to tools from all the providers attached to it. --- * What sessions are and how they work * Attaching multiple providers to a session * Tool filters * Connections, messages, and events * Session templates * [Providers guide](/docs/build/operate/providers) * [OAuth integrations](/docs/platform/integrations/create-custom-oauth-credentials) ## What is a session? [#what-is-a-session] A **session** is a runtime context that connects your AI application to one or more providers. When you create a session, Metorial: 1. Resolves each attached provider deployment and its config/auth 2. Returns a `connection_url` you can connect an MCP client to 3. Exposes the combined tools from all attached providers Sessions are designed to be created per-request or per-user interaction. They're lightweight and fast to start. ## Attaching providers [#attaching-providers] When creating a session, you pass a `providers` array. Each entry specifies a **provider deployment** and an optional **tool filter**: ```json POST /sessions { "providers": [ { "provider_deployment_id": "pdp_abc123" }, { "provider_deployment_id": "pdp_def456", "tool_filters": { "type": "allow", "keys": ["send_message", "list_channels"] } } ] } ``` You can attach **multiple providers**—from the same provider or different ones—in a single session. The tools from all of them are merged and exposed to your AI. Auth configs and provider configs are resolved automatically from your project. If a provider deployment requires auth and you have an auth config for it, Metorial uses it. You can also specify which auth config to use explicitly. ## Tool filters [#tool-filters] Tool filters let you control which tools from a provider are available during a session. This is useful for: * Restricting a session to only the tools your AI actually needs * Preventing destructive actions (e.g. allowing read-only tools only) * Building scoped agents with minimal permissions Filters are set per-provider within a session, so you can give different providers different access levels in the same session. ## Connections [#connections] A **connection** is created automatically the first time something communicates through a session—either when an MCP client connects or when a tool call is sent via the API. A single session can have multiple connections (e.g. multiple MCP clients, or a mix of MCP and direct tool calls). Each connection is tracked independently. Connections support four transport types: | Transport | Description | | ------------------- | --------------------------------------- | | `mcp` | Standard MCP protocol client connection | | `tool_call` | Direct tool call via the Metorial API | | `metorial_protocol` | Internal Metorial protocol | | `system` | System-generated connections | ## Messages and events [#messages-and-events] Within a session you can inspect: * **Messages**: Individual tool calls and MCP protocol messages exchanged during the session. Each message has an `input`, `output`, `status`, and timing information. * **Events**: Significant occurrences like connections opening, errors, or state changes. Useful for monitoring and debugging. * **Participants**: The clients and providers involved in the session (e.g. your application, specific provider instances). These are all read-only resources you access via the API for monitoring and debugging. ## Session Templates [#session-templates] **Session templates** let you save a provider configuration so you can create identical sessions quickly without repeating the setup each time. A template stores the providers list (with tool filters) and can be reused across your application. ```json POST /session-templates { "name": "Research agent", "providers": [ { "provider_deployment_id": "pdp_exa" }, { "provider_deployment_id": "pdp_brave" } ] } ```